From a98e1ddb9ce6be9e6ea26e9b60d9070eb02bf203 Mon Sep 17 00:00:00 2001 From: Greg Soltis Date: Fri, 25 Jan 2019 13:27:58 -0800 Subject: [PATCH 01/27] Implement FSTLevelDBMutationQueue in C++ (#2310) * Port leveldb mutation queue to C++ --- .../Local/FSTLevelDBMutationQueueTests.mm | 14 +- .../Source/Local/FSTLevelDBMutationQueue.h | 6 - .../Source/Local/FSTLevelDBMutationQueue.mm | 460 ++--------------- .../Source/Local/FSTMemoryMutationQueue.mm | 6 +- .../firestore/local/leveldb_mutation_queue.h | 152 ++++++ .../firestore/local/leveldb_mutation_queue.mm | 471 ++++++++++++++++++ .../firestore/local/memory_mutation_queue.h | 14 +- .../firestore/local/memory_mutation_queue.mm | 16 +- 8 files changed, 690 insertions(+), 449 deletions(-) create mode 100644 Firestore/core/src/firebase/firestore/local/leveldb_mutation_queue.h create mode 100644 Firestore/core/src/firebase/firestore/local/leveldb_mutation_queue.mm diff --git a/Firestore/Example/Tests/Local/FSTLevelDBMutationQueueTests.mm b/Firestore/Example/Tests/Local/FSTLevelDBMutationQueueTests.mm index fe5a17c05eb..a53c3c20c09 100644 --- a/Firestore/Example/Tests/Local/FSTLevelDBMutationQueueTests.mm +++ b/Firestore/Example/Tests/Local/FSTLevelDBMutationQueueTests.mm @@ -28,6 +28,7 @@ #include "Firestore/core/src/firebase/firestore/auth/user.h" #include "Firestore/core/src/firebase/firestore/local/leveldb_key.h" +#include "Firestore/core/src/firebase/firestore/local/leveldb_mutation_queue.h" #include "Firestore/core/src/firebase/firestore/local/reference_set.h" #include "Firestore/core/src/firebase/firestore/util/ordered_code.h" #include "absl/strings/string_view.h" @@ -37,6 +38,7 @@ using firebase::firestore::auth::User; using firebase::firestore::local::LevelDbMutationKey; +using firebase::firestore::local::LoadNextBatchIdFromDb; using firebase::firestore::local::ReferenceSet; using firebase::firestore::model::BatchId; using firebase::firestore::util::OrderedCode; @@ -85,21 +87,21 @@ - (void)setUp { - (void)testLoadNextBatchID_zeroWhenTotallyEmpty { // Initial seek is invalid - XCTAssertEqual([FSTLevelDBMutationQueue loadNextBatchIDFromDB:_db.ptr], 0); + XCTAssertEqual(LoadNextBatchIdFromDb(_db.ptr), 0); } - (void)testLoadNextBatchID_zeroWhenNoMutations { // Initial seek finds no mutations [self setDummyValueForKey:MutationLikeKey("mutationr", "foo", 20)]; [self setDummyValueForKey:MutationLikeKey("mutationsa", "foo", 10)]; - XCTAssertEqual([FSTLevelDBMutationQueue loadNextBatchIDFromDB:_db.ptr], 0); + XCTAssertEqual(LoadNextBatchIdFromDb(_db.ptr), 0); } - (void)testLoadNextBatchID_findsSingleRow { // Seeks off the end of the table altogether [self setDummyValueForKey:LevelDbMutationKey::Key("foo", 6)]; - XCTAssertEqual([FSTLevelDBMutationQueue loadNextBatchIDFromDB:_db.ptr], 7); + XCTAssertEqual(LoadNextBatchIdFromDb(_db.ptr), 7); } - (void)testLoadNextBatchID_findsSingleRowAmongNonMutations { @@ -107,7 +109,7 @@ - (void)testLoadNextBatchID_findsSingleRowAmongNonMutations { [self setDummyValueForKey:LevelDbMutationKey::Key("foo", 6)]; [self setDummyValueForKey:MutationLikeKey("mutationsa", "foo", 10)]; - XCTAssertEqual([FSTLevelDBMutationQueue loadNextBatchIDFromDB:_db.ptr], 7); + XCTAssertEqual(LoadNextBatchIdFromDb(_db.ptr), 7); } - (void)testLoadNextBatchID_findsMaxAcrossUsers { @@ -118,7 +120,7 @@ - (void)testLoadNextBatchID_findsMaxAcrossUsers { [self setDummyValueForKey:LevelDbMutationKey::Key("foo", 2)]; [self setDummyValueForKey:LevelDbMutationKey::Key("foo", 1)]; - XCTAssertEqual([FSTLevelDBMutationQueue loadNextBatchIDFromDB:_db.ptr], 7); + XCTAssertEqual(LoadNextBatchIdFromDb(_db.ptr), 7); } - (void)testLoadNextBatchID_onlyFindsMutations { @@ -135,7 +137,7 @@ - (void)testLoadNextBatchID_onlyFindsMutations { // None of the higher tables should match -- this is the only entry that's in the mutations // table - XCTAssertEqual([FSTLevelDBMutationQueue loadNextBatchIDFromDB:_db.ptr], 4); + XCTAssertEqual(LoadNextBatchIdFromDb(_db.ptr), 4); } - (void)testEmptyProtoCanBeUpgraded { diff --git a/Firestore/Source/Local/FSTLevelDBMutationQueue.h b/Firestore/Source/Local/FSTLevelDBMutationQueue.h index 924852ce7e4..20ff77d21f8 100644 --- a/Firestore/Source/Local/FSTLevelDBMutationQueue.h +++ b/Firestore/Source/Local/FSTLevelDBMutationQueue.h @@ -44,12 +44,6 @@ NS_ASSUME_NONNULL_BEGIN db:(FSTLevelDB *)db serializer:(FSTLocalSerializer *)serializer; -/** - * Returns one larger than the largest batch ID that has been stored. If there are no mutations - * returns 0. Note that batch IDs are global. - */ -+ (firebase::firestore::model::BatchId)loadNextBatchIDFromDB:(leveldb::DB *)db; - @end NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLevelDBMutationQueue.mm b/Firestore/Source/Local/FSTLevelDBMutationQueue.mm index 57d0b99be27..951fe7874be 100644 --- a/Firestore/Source/Local/FSTLevelDBMutationQueue.mm +++ b/Firestore/Source/Local/FSTLevelDBMutationQueue.mm @@ -31,6 +31,7 @@ #include "Firestore/core/src/firebase/firestore/auth/user.h" #include "Firestore/core/src/firebase/firestore/local/leveldb_key.h" +#include "Firestore/core/src/firebase/firestore/local/leveldb_mutation_queue.h" #include "Firestore/core/src/firebase/firestore/local/leveldb_transaction.h" #include "Firestore/core/src/firebase/firestore/local/leveldb_util.h" #include "Firestore/core/src/firebase/firestore/model/mutation_batch.h" @@ -38,6 +39,7 @@ #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" #include "Firestore/core/src/firebase/firestore/util/string_apple.h" #include "Firestore/core/src/firebase/firestore/util/string_util.h" +#include "absl/memory/memory.h" #include "absl/strings/match.h" #include "leveldb/db.h" #include "leveldb/write_batch.h" @@ -49,8 +51,10 @@ using firebase::firestore::local::DescribeKey; using firebase::firestore::local::LevelDbDocumentMutationKey; using firebase::firestore::local::LevelDbMutationKey; +using firebase::firestore::local::LevelDbMutationQueue; using firebase::firestore::local::LevelDbMutationQueueKey; using firebase::firestore::local::LevelDbTransaction; +using firebase::firestore::local::LoadNextBatchIdFromDb; using firebase::firestore::local::MakeStringView; using firebase::firestore::model::BatchId; using firebase::firestore::model::kBatchIdUnknown; @@ -65,34 +69,26 @@ using leveldb::WriteBatch; using leveldb::WriteOptions; +static NSArray *toNSArray(const std::vector &vec) { + NSMutableArray *copy = [NSMutableArray array]; + for (auto &batch : vec) { + [copy addObject:batch]; + } + return copy; +} + @interface FSTLevelDBMutationQueue () - (instancetype)initWithUserID:(std::string)userID db:(FSTLevelDB *)db - serializer:(FSTLocalSerializer *)serializer NS_DESIGNATED_INITIALIZER; - -/** - * Next value to use when assigning sequential IDs to each mutation batch. - * - * NOTE: There can only be one FSTLevelDBMutationQueue for a given db at a time, hence it is safe - * to track nextBatchID as an instance-level property. Should we ever relax this constraint we'll - * need to revisit this. - */ -@property(nonatomic, assign) BatchId nextBatchID; - -/** A write-through cache copy of the metadata describing the current queue. */ -@property(nonatomic, strong, nullable) FSTPBMutationQueue *metadata; - -@property(nonatomic, strong, readonly) FSTLocalSerializer *serializer; + serializer:(FSTLocalSerializer *)serializer + delegate:(std::unique_ptr)delegate + NS_DESIGNATED_INITIALIZER; @end @implementation FSTLevelDBMutationQueue { - // This instance is owned by FSTLevelDB; avoid a retain cycle. - __weak FSTLevelDB *_db; - - /** The normalized userID (e.g. nil UID => @"" userID) used in our LevelDB keys. */ - std::string _userID; + std::unique_ptr _delegate; } + (instancetype)mutationQueueWithUser:(const User &)user @@ -100,460 +96,80 @@ + (instancetype)mutationQueueWithUser:(const User &)user serializer:(FSTLocalSerializer *)serializer { std::string userID = user.is_authenticated() ? user.uid() : ""; - return [[FSTLevelDBMutationQueue alloc] initWithUserID:std::move(userID) - db:db - serializer:serializer]; + return [[FSTLevelDBMutationQueue alloc] + initWithUserID:std::move(userID) + db:db + serializer:serializer + delegate:absl::make_unique(user, db, serializer)]; } - (instancetype)initWithUserID:(std::string)userID db:(FSTLevelDB *)db - serializer:(FSTLocalSerializer *)serializer { + serializer:(FSTLocalSerializer *)serializer + delegate:(std::unique_ptr)delegate { if (self = [super init]) { - _userID = std::move(userID); - _db = db; - _serializer = serializer; + _delegate = std::move(delegate); } return self; } - (void)start { - self.nextBatchID = [FSTLevelDBMutationQueue loadNextBatchIDFromDB:_db.ptr]; - - std::string key = [self keyForCurrentMutationQueue]; - FSTPBMutationQueue *metadata = [self metadataForKey:key]; - if (!metadata) { - metadata = [FSTPBMutationQueue message]; - } - self.metadata = metadata; -} - -+ (BatchId)loadNextBatchIDFromDB:(DB *)db { - // TODO(gsoltis): implement Prev() and SeekToLast() on LevelDbTransaction::Iterator, then port - // this to a transaction. - std::unique_ptr it(db->NewIterator(LevelDbTransaction::DefaultReadOptions())); - - auto tableKey = LevelDbMutationKey::KeyPrefix(); - - LevelDbMutationKey rowKey; - BatchId maxBatchID = kBatchIdUnknown; - - BOOL moreUserIDs = NO; - std::string nextUserID; - - it->Seek(tableKey); - if (it->Valid() && rowKey.Decode(MakeStringView(it->key()))) { - moreUserIDs = YES; - nextUserID = rowKey.user_id(); - } - - // This loop assumes that nextUserId contains the next username at the start of the iteration. - while (moreUserIDs) { - // Compute the first key after the last mutation for nextUserID. - auto userEnd = LevelDbMutationKey::KeyPrefix(nextUserID); - userEnd = util::PrefixSuccessor(userEnd); - - // Seek to that key with the intent of finding the boundary between nextUserID's mutations - // and the one after that (if any). - it->Seek(userEnd); - - // At this point there are three possible cases to handle differently. Each case must prepare - // the next iteration (by assigning to nextUserID or setting moreUserIDs = NO) and seek the - // iterator to the last row in the current user's mutation sequence. - if (!it->Valid()) { - // The iterator is past the last row altogether (there are no additional userIDs and now - // rows in any table after mutations). The last row will have the highest batchID. - moreUserIDs = NO; - it->SeekToLast(); - - } else if (rowKey.Decode(MakeStringView(it->key()))) { - // The iterator is valid and the key decoded successfully so the next user was just decoded. - nextUserID = rowKey.user_id(); - it->Prev(); - - } else { - // The iterator is past the end of the mutations table but there are other rows. - moreUserIDs = NO; - it->Prev(); - } - - // In all the cases above there was at least one row for the current user and each case has - // set things up such that iterator points to it. - if (!rowKey.Decode(MakeStringView(it->key()))) { - HARD_FAIL("There should have been a key previous to %s", userEnd); - } - - if (rowKey.batch_id() > maxBatchID) { - maxBatchID = rowKey.batch_id(); - } - } - - return maxBatchID + 1; + _delegate->Start(); } - (BOOL)isEmpty { - std::string userKey = LevelDbMutationKey::KeyPrefix(_userID); - - auto it = _db.currentTransaction->NewIterator(); - it->Seek(userKey); - - BOOL empty = YES; - if (it->Valid() && absl::StartsWith(it->key(), userKey)) { - empty = NO; - } - - return empty; + return _delegate->IsEmpty(); } - (void)acknowledgeBatch:(FSTMutationBatch *)batch streamToken:(nullable NSData *)streamToken { - FSTPBMutationQueue *metadata = self.metadata; - metadata.lastStreamToken = streamToken; - - _db.currentTransaction->Put([self keyForCurrentMutationQueue], metadata); + _delegate->AcknowledgeBatch(batch, streamToken); } - (nullable NSData *)lastStreamToken { - return self.metadata.lastStreamToken; + return _delegate->GetLastStreamToken(); } - (void)setLastStreamToken:(nullable NSData *)streamToken { - FSTPBMutationQueue *metadata = self.metadata; - metadata.lastStreamToken = streamToken; - - _db.currentTransaction->Put([self keyForCurrentMutationQueue], metadata); -} - -- (std::string)keyForCurrentMutationQueue { - return LevelDbMutationQueueKey::Key(_userID); -} - -- (nullable FSTPBMutationQueue *)metadataForKey:(const std::string &)key { - std::string value; - Status status = _db.currentTransaction->Get(key, &value); - if (status.ok()) { - return [self parsedMetadata:value]; - } else if (status.IsNotFound()) { - return nil; - } else { - HARD_FAIL("metadataForKey: failed loading key %s with status: %s", key, status.ToString()); - } + _delegate->SetLastStreamToken(streamToken); } - (FSTMutationBatch *)addMutationBatchWithWriteTime:(FIRTimestamp *)localWriteTime mutations:(NSArray *)mutations { - BatchId batchID = self.nextBatchID; - self.nextBatchID += 1; - - FSTMutationBatch *batch = [[FSTMutationBatch alloc] initWithBatchID:batchID - localWriteTime:localWriteTime - mutations:mutations]; - std::string key = [self mutationKeyForBatch:batch]; - _db.currentTransaction->Put(key, [self.serializer encodedMutationBatch:batch]); - - // Store an empty value in the index which is equivalent to serializing a GPBEmpty message. In the - // future if we wanted to store some other kind of value here, we can parse these empty values as - // with some other protocol buffer (and the parser will see all default values). - std::string emptyBuffer; - - for (FSTMutation *mutation in mutations) { - key = LevelDbDocumentMutationKey::Key(_userID, mutation.key, batchID); - _db.currentTransaction->Put(key, emptyBuffer); - } - - return batch; + return _delegate->AddMutationBatch(localWriteTime, mutations); } - (nullable FSTMutationBatch *)lookupMutationBatch:(BatchId)batchID { - std::string key = [self mutationKeyForBatchID:batchID]; - - std::string value; - Status status = _db.currentTransaction->Get(key, &value); - if (!status.ok()) { - if (status.IsNotFound()) { - return nil; - } - HARD_FAIL("Lookup mutation batch (%s, %s) failed with status: %s", _userID, batchID, - status.ToString()); - } - - return [self decodedMutationBatch:value]; + return _delegate->LookupMutationBatch(batchID); } - (nullable FSTMutationBatch *)nextMutationBatchAfterBatchID:(BatchId)batchID { - BatchId nextBatchID = batchID + 1; - - std::string key = [self mutationKeyForBatchID:nextBatchID]; - auto it = _db.currentTransaction->NewIterator(); - it->Seek(key); - - LevelDbMutationKey rowKey; - if (!it->Valid() || !rowKey.Decode(it->key())) { - // Past the last row in the DB or out of the mutations table - return nil; - } - - if (rowKey.user_id() != _userID) { - // Jumped past the last mutation for this user - return nil; - } - - HARD_ASSERT(rowKey.batch_id() >= nextBatchID, "Should have found mutation after %s", nextBatchID); - return [self decodedMutationBatch:it->value()]; + return _delegate->NextMutationBatchAfterBatchId(batchID); } - (NSArray *)allMutationBatchesAffectingDocumentKey: (const DocumentKey &)documentKey { - // Scan the document-mutation index starting with a prefix starting with the given documentKey. - std::string indexPrefix = LevelDbDocumentMutationKey::KeyPrefix(_userID, documentKey.path()); - auto indexIterator = _db.currentTransaction->NewIterator(); - indexIterator->Seek(indexPrefix); - - // Simultaneously scan the mutation queue. This works because each (key, batchID) pair is unique - // and ordered, so when scanning a table prefixed by exactly key, all the batchIDs encountered - // will be unique and in order. - std::string mutationsPrefix = LevelDbMutationKey::KeyPrefix(_userID); - auto mutationIterator = _db.currentTransaction->NewIterator(); - - NSMutableArray *result = [NSMutableArray array]; - LevelDbDocumentMutationKey rowKey; - for (; indexIterator->Valid(); indexIterator->Next()) { - // Only consider rows matching exactly the specific key of interest. Index rows have this - // form (with markers in brackets): - // - // user collection doc 2 - // user collection doc 3 - // user collection doc sub doc 3 - // - // Note that Path markers sort after BatchId markers so this means that when searching for - // collection/doc, all the entries for it will be contiguous in the table, allowing a break - // after any mismatch. - if (!absl::StartsWith(indexIterator->key(), indexPrefix) || - !rowKey.Decode(indexIterator->key()) || rowKey.document_key() != documentKey) { - break; - } - - // Each row is a unique combination of key and batchID, so this foreign key reference can - // only occur once. - std::string mutationKey = LevelDbMutationKey::Key(_userID, rowKey.batch_id()); - mutationIterator->Seek(mutationKey); - if (!mutationIterator->Valid() || mutationIterator->key() != mutationKey) { - HARD_FAIL("Dangling document-mutation reference found: " - "%s points to %s; seeking there found %s", - DescribeKey(indexIterator), DescribeKey(mutationKey), - DescribeKey(mutationIterator)); - } - - [result addObject:[self decodedMutationBatch:mutationIterator->value()]]; - } - return result; + return toNSArray(_delegate->AllMutationBatchesAffectingDocumentKey(documentKey)); } - (NSArray *)allMutationBatchesAffectingDocumentKeys: (const DocumentKeySet &)documentKeys { - // Take a pass through the document keys and collect the set of unique mutation batchIDs that - // affect them all. Some batches can affect more than one key. - std::set batchIDs; - - auto indexIterator = _db.currentTransaction->NewIterator(); - LevelDbDocumentMutationKey rowKey; - for (const DocumentKey &documentKey : documentKeys) { - std::string indexPrefix = LevelDbDocumentMutationKey::KeyPrefix(_userID, documentKey.path()); - for (indexIterator->Seek(indexPrefix); indexIterator->Valid(); indexIterator->Next()) { - // Only consider rows matching exactly the specific key of interest. Index rows have this - // form (with markers in brackets): - // - // user collection doc 2 - // user collection doc 3 - // user collection doc sub doc 3 - // - // Note that Path markers sort after BatchId markers so this means that when searching for - // collection/doc, all the entries for it will be contiguous in the table, allowing a break - // after any mismatch. - if (!absl::StartsWith(indexIterator->key(), indexPrefix) || - !rowKey.Decode(indexIterator->key()) || rowKey.document_key() != documentKey) { - break; - } - - batchIDs.insert(rowKey.batch_id()); - } - } - - return [self allMutationBatchesWithBatchIDs:batchIDs]; + return toNSArray(_delegate->AllMutationBatchesAffectingDocumentKeys(documentKeys)); } - (NSArray *)allMutationBatchesAffectingQuery:(FSTQuery *)query { - HARD_ASSERT(![query isDocumentQuery], "Document queries shouldn't go down this path"); - - const ResourcePath &queryPath = query.path; - size_t immediateChildrenPathLength = queryPath.size() + 1; - - // TODO(mcg): Actually implement a single-collection query - // - // This is actually executing an ancestor query, traversing the whole subtree below the - // collection which can be horrifically inefficient for some structures. The right way to - // solve this is to implement the full value index, but that's not in the cards in the near - // future so this is the best we can do for the moment. - // - // Since we don't yet index the actual properties in the mutations, our current approach is to - // just return all mutation batches that affect documents in the collection being queried. - // - // Unlike allMutationBatchesAffectingDocumentKey, this iteration will scan the document-mutation - // index for more than a single document so the associated batchIDs will be neither necessarily - // unique nor in order. This means an efficient simultaneous scan isn't possible. - std::string indexPrefix = LevelDbDocumentMutationKey::KeyPrefix(_userID, queryPath); - auto indexIterator = _db.currentTransaction->NewIterator(); - indexIterator->Seek(indexPrefix); - - LevelDbDocumentMutationKey rowKey; - - // Collect up unique batchIDs encountered during a scan of the index. Use a set to - // accumulate batch IDs so they can be traversed in order in a scan of the main table. - // - // This method is faster than performing lookups of the keys with _db->Get and keeping a hash of - // batchIDs that have already been looked up. The performance difference is minor for small - // numbers of keys but > 30% faster for larger numbers of keys. - std::set uniqueBatchIDs; - for (; indexIterator->Valid(); indexIterator->Next()) { - if (!absl::StartsWith(indexIterator->key(), indexPrefix) || - !rowKey.Decode(indexIterator->key())) { - break; - } - - // Rows with document keys more than one segment longer than the query path can't be matches. - // For example, a query on 'rooms' can't match the document /rooms/abc/messages/xyx. - // TODO(mcg): we'll need a different scanner when we implement ancestor queries. - if (rowKey.document_key().path().size() != immediateChildrenPathLength) { - continue; - } - - uniqueBatchIDs.insert(rowKey.batch_id()); - } - - return [self allMutationBatchesWithBatchIDs:uniqueBatchIDs]; -} - -/** - * Constructs an array of matching batches, sorted by batchID to ensure that multiple mutations - * affecting the same document key are applied in order. - */ -- (NSArray *)allMutationBatchesWithBatchIDs: - (const std::set &)batchIDs { - NSMutableArray *result = [NSMutableArray array]; - - // Given an ordered set of unique batchIDs perform a skipping scan over the main table to find - // the mutation batches. - auto mutationIterator = _db.currentTransaction->NewIterator(); - for (BatchId batchID : batchIDs) { - std::string mutationKey = LevelDbMutationKey::Key(_userID, batchID); - mutationIterator->Seek(mutationKey); - if (!mutationIterator->Valid() || mutationIterator->key() != mutationKey) { - HARD_FAIL("Dangling document-mutation reference found: " - "Missing batch %s; seeking there found %s", - DescribeKey(mutationKey), DescribeKey(mutationIterator)); - } - - [result addObject:[self decodedMutationBatch:mutationIterator->value()]]; - } - return result; + return toNSArray(_delegate->AllMutationBatchesAffectingQuery(query)); } - (NSArray *)allMutationBatches { - std::string userKey = LevelDbMutationKey::KeyPrefix(_userID); - - auto it = _db.currentTransaction->NewIterator(); - it->Seek(userKey); - - NSMutableArray *result = [NSMutableArray array]; - for (; it->Valid() && absl::StartsWith(it->key(), userKey); it->Next()) { - [result addObject:[self decodedMutationBatch:it->value()]]; - } - - return result; + return toNSArray(_delegate->AllMutationBatches()); } - (void)removeMutationBatch:(FSTMutationBatch *)batch { - auto checkIterator = _db.currentTransaction->NewIterator(); - - BatchId batchID = batch.batchID; - std::string key = LevelDbMutationKey::Key(_userID, batchID); - - // As a sanity check, verify that the mutation batch exists before deleting it. - checkIterator->Seek(key); - HARD_ASSERT(checkIterator->Valid(), "Mutation batch %s did not exist", DescribeKey(key)); - - HARD_ASSERT(key == checkIterator->key(), "Mutation batch %s not found; found %s", - DescribeKey(key), DescribeKey(checkIterator)); - - _db.currentTransaction->Delete(key); - - for (FSTMutation *mutation in batch.mutations) { - key = LevelDbDocumentMutationKey::Key(_userID, mutation.key, batchID); - _db.currentTransaction->Delete(key); - [_db.referenceDelegate removeMutationReference:mutation.key]; - } + _delegate->RemoveMutationBatch(batch); } - (void)performConsistencyCheck { - if (![self isEmpty]) { - return; - } - - // Verify that there are no entries in the document-mutation index if the queue is empty. - std::string indexPrefix = LevelDbDocumentMutationKey::KeyPrefix(_userID); - auto indexIterator = _db.currentTransaction->NewIterator(); - indexIterator->Seek(indexPrefix); - - std::vector danglingMutationReferences; - - for (; indexIterator->Valid(); indexIterator->Next()) { - // Only consider rows matching this index prefix for the current user. - if (!absl::StartsWith(indexIterator->key(), indexPrefix)) { - break; - } - - danglingMutationReferences.push_back(DescribeKey(indexIterator)); - } - - HARD_ASSERT(danglingMutationReferences.empty(), - "Document leak -- detected dangling mutation references when queue " - "is empty. Dangling keys: %s", - util::ToString(danglingMutationReferences)); -} - -- (std::string)mutationKeyForBatch:(FSTMutationBatch *)batch { - return LevelDbMutationKey::Key(_userID, batch.batchID); -} - -- (std::string)mutationKeyForBatchID:(BatchId)batchID { - return LevelDbMutationKey::Key(_userID, batchID); -} - -/** Parses the MutationQueue metadata from the given LevelDB row contents. */ -- (FSTPBMutationQueue *)parsedMetadata:(Slice)slice { - NSData *data = [[NSData alloc] initWithBytesNoCopy:(void *)slice.data() - length:slice.size() - freeWhenDone:NO]; - - NSError *error; - FSTPBMutationQueue *proto = [FSTPBMutationQueue parseFromData:data error:&error]; - if (!proto) { - HARD_FAIL("FSTPBMutationQueue failed to parse: %s", error); - } - - return proto; -} - -- (FSTMutationBatch *)decodedMutationBatch:(absl::string_view)encoded { - NSData *data = [[NSData alloc] initWithBytesNoCopy:(void *)encoded.data() - length:encoded.size() - freeWhenDone:NO]; - - NSError *error; - FSTPBWriteBatch *proto = [FSTPBWriteBatch parseFromData:data error:&error]; - if (!proto) { - HARD_FAIL("FSTPBMutationBatch failed to parse: %s", error); - } - - return [self.serializer decodedMutationBatch:proto]; + _delegate->PerformConsistencyCheck(); } @end diff --git a/Firestore/Source/Local/FSTMemoryMutationQueue.mm b/Firestore/Source/Local/FSTMemoryMutationQueue.mm index fc16d9c6276..8a1c6221cae 100644 --- a/Firestore/Source/Local/FSTMemoryMutationQueue.mm +++ b/Firestore/Source/Local/FSTMemoryMutationQueue.mm @@ -65,11 +65,11 @@ - (instancetype)initWithPersistence:(FSTMemoryPersistence *)persistence { } - (void)setLastStreamToken:(NSData *_Nullable)streamToken { - _delegate->set_last_stream_token(streamToken); + _delegate->SetLastStreamToken(streamToken); } - (NSData *_Nullable)lastStreamToken { - return _delegate->last_stream_token(); + return _delegate->GetLastStreamToken(); } #pragma mark - FSTMutationQueue implementation @@ -79,7 +79,7 @@ - (void)start { } - (BOOL)isEmpty { - return _delegate->is_empty(); + return _delegate->IsEmpty(); } - (void)acknowledgeBatch:(FSTMutationBatch *)batch streamToken:(nullable NSData *)streamToken { diff --git a/Firestore/core/src/firebase/firestore/local/leveldb_mutation_queue.h b/Firestore/core/src/firebase/firestore/local/leveldb_mutation_queue.h new file mode 100644 index 00000000000..90ba81d3915 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/local/leveldb_mutation_queue.h @@ -0,0 +1,152 @@ +/* + * Copyright 2019 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. + */ + +#ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_LEVELDB_MUTATION_QUEUE_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_LEVELDB_MUTATION_QUEUE_H_ + +#if !defined(__OBJC__) +#error "For now, this file must only be included by ObjC source files." +#endif // !defined(__OBJC__) + +#import + +#include +#include +#include + +#import "Firestore/Source/Public/FIRTimestamp.h" + +#include "Firestore/core/src/firebase/firestore/auth/user.h" +#include "Firestore/core/src/firebase/firestore/local/leveldb_key.h" +#include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/model/document_key_set.h" +#include "Firestore/core/src/firebase/firestore/model/types.h" +#include "absl/strings/string_view.h" +#include "leveldb/db.h" + +@class FSTLevelDB; +@class FSTLocalSerializer; +@class FSTMutation; +@class FSTMutationBatch; +@class FSTPBMutationQueue; +@class FSTQuery; + +NS_ASSUME_NONNULL_BEGIN + +namespace firebase { +namespace firestore { +namespace local { + +/** + * Returns one larger than the largest batch ID that has been stored. If there + * are no mutations returns 0. Note that batch IDs are global. + */ +model::BatchId LoadNextBatchIdFromDb(leveldb::DB* db); + +class LevelDbMutationQueue { + public: + LevelDbMutationQueue(const auth::User& user, + FSTLevelDB* db, + FSTLocalSerializer* serializer); + + void Start(); + + bool IsEmpty(); + + void AcknowledgeBatch(FSTMutationBatch* batch, + NSData* _Nullable stream_token); + + FSTMutationBatch* AddMutationBatch(FIRTimestamp* local_write_time, + NSArray* mutations); + + void RemoveMutationBatch(FSTMutationBatch* batch); + + std::vector AllMutationBatches(); + + std::vector AllMutationBatchesAffectingDocumentKeys( + const model::DocumentKeySet& document_keys); + + std::vector AllMutationBatchesAffectingDocumentKey( + const model::DocumentKey& key); + + std::vector AllMutationBatchesAffectingQuery( + FSTQuery* query); + + FSTMutationBatch* _Nullable LookupMutationBatch(model::BatchId batch_id); + + FSTMutationBatch* _Nullable NextMutationBatchAfterBatchId( + model::BatchId batch_id); + + void PerformConsistencyCheck(); + + NSData* _Nullable GetLastStreamToken(); + + void SetLastStreamToken(NSData* _Nullable stream_token); + + private: + /** + * Constructs a vector of matching batches, sorted by batchID to ensure that + * multiple mutations affecting the same document key are applied in order. + */ + std::vector AllMutationBatchesWithIds( + const std::set& batch_ids); + + std::string mutation_queue_key() { + return LevelDbMutationQueueKey::Key(user_id_); + } + + std::string mutation_batch_key(model::BatchId batch_id) { + return LevelDbMutationKey::Key(user_id_, batch_id); + } + + /** Parses the MutationQueue metadata from the given LevelDB row contents. */ + FSTPBMutationQueue* _Nullable MetadataForKey(const std::string& key); + + FSTMutationBatch* ParseMutationBatch(absl::string_view encoded); + + // This instance is owned by FSTLevelDB; avoid a retain cycle. + __weak FSTLevelDB* db_; + + FSTLocalSerializer* serializer_; + + /** + * The normalized userID (e.g. nil UID => @"" userID) used in our LevelDB + * keys. + */ + std::string user_id_; + + /** + * Next value to use when assigning sequential IDs to each mutation batch. + * + * NOTE: There can only be one LevelDbMutationQueue for a given db at a time, + * hence it is safe to track next_batch_id_ as an instance-level property. + * Should we ever relax this constraint we'll need to revisit this. + */ + model::BatchId next_batch_id_; + + /** + * A write-through cache copy of the metadata describing the current queue. + */ + FSTPBMutationQueue* _Nullable metadata_; +}; + +} // namespace local +} // namespace firestore +} // namespace firebase + +NS_ASSUME_NONNULL_END + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_LEVELDB_MUTATION_QUEUE_H_ diff --git a/Firestore/core/src/firebase/firestore/local/leveldb_mutation_queue.mm b/Firestore/core/src/firebase/firestore/local/leveldb_mutation_queue.mm new file mode 100644 index 00000000000..b020c563dab --- /dev/null +++ b/Firestore/core/src/firebase/firestore/local/leveldb_mutation_queue.mm @@ -0,0 +1,471 @@ +/* + * Copyright 2019 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. + */ + +#include "Firestore/core/src/firebase/firestore/local/leveldb_mutation_queue.h" + +#include + +#import "Firestore/Protos/objc/firestore/local/Mutation.pbobjc.h" +#import "Firestore/Source/Core/FSTQuery.h" +#import "Firestore/Source/Local/FSTLevelDB.h" +#import "Firestore/Source/Local/FSTLocalSerializer.h" +#import "Firestore/Source/Model/FSTMutation.h" +#import "Firestore/Source/Model/FSTMutationBatch.h" + +#include "Firestore/core/src/firebase/firestore/local/leveldb_util.h" +#include "Firestore/core/src/firebase/firestore/model/mutation_batch.h" +#include "Firestore/core/src/firebase/firestore/model/resource_path.h" +#include "Firestore/core/src/firebase/firestore/util/string_util.h" +#include "absl/strings/match.h" + +NS_ASSUME_NONNULL_BEGIN + +namespace firebase { +namespace firestore { +namespace local { + +using auth::User; +using leveldb::DB; +using leveldb::Iterator; +using leveldb::Status; +using model::BatchId; +using model::DocumentKey; +using model::DocumentKeySet; +using model::kBatchIdUnknown; +using model::ResourcePath; + +BatchId LoadNextBatchIdFromDb(DB* db) { + // TODO(gsoltis): implement Prev() and SeekToLast() on + // LevelDbTransaction::Iterator, then port this to a transaction. + std::unique_ptr it( + db->NewIterator(LevelDbTransaction::DefaultReadOptions())); + + std::string table_key = LevelDbMutationKey::KeyPrefix(); + + LevelDbMutationKey row_key; + BatchId max_batch_id = kBatchIdUnknown; + + bool more_user_ids = false; + std::string next_user_id; + + it->Seek(table_key); + if (it->Valid() && row_key.Decode(MakeStringView(it->key()))) { + more_user_ids = true; + next_user_id = row_key.user_id(); + } + + // This loop assumes that nextUserId contains the next username at the start + // of the iteration. + while (more_user_ids) { + // Compute the first key after the last mutation for next_user_id. + std::string user_end = LevelDbMutationKey::KeyPrefix(next_user_id); + user_end = util::PrefixSuccessor(user_end); + + // Seek to that key with the intent of finding the boundary between + // next_user_id's mutations and the one after that (if any). + it->Seek(user_end); + + // At this point there are three possible cases to handle differently. Each + // case must prepare the next iteration (by assigning to next_user_id or + // setting more_user_ids = NO) and seek the iterator to the last row in the + // current user's mutation sequence. + if (!it->Valid()) { + // The iterator is past the last row altogether (there are no additional + // userIDs and now rows in any table after mutations). The last row will + // have the highest batchID. + more_user_ids = false; + it->SeekToLast(); + + } else if (row_key.Decode(MakeStringView(it->key()))) { + // The iterator is valid and the key decoded successfully so the next user + // was just decoded. + next_user_id = row_key.user_id(); + it->Prev(); + + } else { + // The iterator is past the end of the mutations table but there are other + // rows. + more_user_ids = false; + it->Prev(); + } + + // In all the cases above there was at least one row for the current user + // and each case has set things up such that iterator points to it. + if (!row_key.Decode(MakeStringView(it->key()))) { + HARD_FAIL("There should have been a key previous to %s", user_end); + } + + if (row_key.batch_id() > max_batch_id) { + max_batch_id = row_key.batch_id(); + } + } + + return max_batch_id + 1; +} + +LevelDbMutationQueue::LevelDbMutationQueue(const User& user, + FSTLevelDB* db, + FSTLocalSerializer* serializer) + : db_(db), + serializer_(serializer), + user_id_(user.is_authenticated() ? user.uid() : "") { +} + +void LevelDbMutationQueue::Start() { + next_batch_id_ = LoadNextBatchIdFromDb(db_.ptr); + + std::string key = mutation_queue_key(); + FSTPBMutationQueue* metadata = MetadataForKey(key); + if (!metadata) { + metadata = [FSTPBMutationQueue message]; + } + metadata_ = metadata; +} + +bool LevelDbMutationQueue::IsEmpty() { + std::string user_key = LevelDbMutationKey::KeyPrefix(user_id_); + + auto it = db_.currentTransaction->NewIterator(); + it->Seek(user_key); + + bool empty = true; + if (it->Valid() && absl::StartsWith(it->key(), user_key)) { + empty = false; + } + return empty; +} + +void LevelDbMutationQueue::AcknowledgeBatch(FSTMutationBatch* batch, + NSData* _Nullable stream_token) { + SetLastStreamToken(stream_token); +} + +FSTMutationBatch* LevelDbMutationQueue::AddMutationBatch( + FIRTimestamp* local_write_time, NSArray* mutations) { + BatchId batch_id = next_batch_id_; + next_batch_id_++; + + FSTMutationBatch* batch = + [[FSTMutationBatch alloc] initWithBatchID:batch_id + localWriteTime:local_write_time + mutations:mutations]; + std::string key = mutation_batch_key(batch_id); + db_.currentTransaction->Put(key, [serializer_ encodedMutationBatch:batch]); + + // Store an empty value in the index which is equivalent to serializing a + // GPBEmpty message. In the future if we wanted to store some other kind of + // value here, we can parse these empty values as with some other protocol + // buffer (and the parser will see all default values). + std::string empty_buffer; + + for (FSTMutation* mutation in mutations) { + key = LevelDbDocumentMutationKey::Key(user_id_, mutation.key, batch_id); + db_.currentTransaction->Put(key, empty_buffer); + } + + return batch; +} + +void LevelDbMutationQueue::RemoveMutationBatch(FSTMutationBatch* batch) { + auto check_iterator = db_.currentTransaction->NewIterator(); + + BatchId batch_id = batch.batchID; + std::string key = mutation_batch_key(batch_id); + + // As a sanity check, verify that the mutation batch exists before deleting + // it. + check_iterator->Seek(key); + HARD_ASSERT(check_iterator->Valid(), "Mutation batch %s did not exist", + DescribeKey(key)); + + HARD_ASSERT(key == check_iterator->key(), + "Mutation batch %s not found; found %s", DescribeKey(key), + DescribeKey(check_iterator->key())); + + db_.currentTransaction->Delete(key); + + for (FSTMutation* mutation in batch.mutations) { + key = LevelDbDocumentMutationKey::Key(user_id_, mutation.key, batch_id); + db_.currentTransaction->Delete(key); + [db_.referenceDelegate removeMutationReference:mutation.key]; + } +} + +std::vector LevelDbMutationQueue::AllMutationBatches() { + std::string user_key = LevelDbMutationKey::KeyPrefix(user_id_); + + auto it = db_.currentTransaction->NewIterator(); + it->Seek(user_key); + std::vector result; + for (; it->Valid() && absl::StartsWith(it->key(), user_key); it->Next()) { + result.push_back(ParseMutationBatch(it->value())); + } + return result; +} + +std::vector +LevelDbMutationQueue::AllMutationBatchesAffectingDocumentKeys( + const DocumentKeySet& document_keys) { + // Take a pass through the document keys and collect the set of unique + // mutation batch_ids that affect them all. Some batches can affect more than + // one key. + std::set batch_ids; + + auto index_iterator = db_.currentTransaction->NewIterator(); + LevelDbDocumentMutationKey row_key; + for (const DocumentKey& document_key : document_keys) { + std::string index_prefix = + LevelDbDocumentMutationKey::KeyPrefix(user_id_, document_key.path()); + for (index_iterator->Seek(index_prefix); index_iterator->Valid(); + index_iterator->Next()) { + // Only consider rows matching exactly the specific key of interest. Index + // rows have this form (with markers in brackets): + // + // user collection doc 2 + // user collection doc 3 + // user collection doc sub doc 3 + // + // + // Note that Path markers sort after BatchId markers so this means that + // when searching for collection/doc, all the entries for it will be + // contiguous in the table, allowing a break after any mismatch. + if (!absl::StartsWith(index_iterator->key(), index_prefix) || + !row_key.Decode(index_iterator->key()) || + row_key.document_key() != document_key) { + break; + } + + batch_ids.insert(row_key.batch_id()); + } + } + + return AllMutationBatchesWithIds(batch_ids); +} + +std::vector +LevelDbMutationQueue::AllMutationBatchesAffectingDocumentKey( + const DocumentKey& key) { + return AllMutationBatchesAffectingDocumentKeys(DocumentKeySet{key}); +} + +std::vector +LevelDbMutationQueue::AllMutationBatchesAffectingQuery(FSTQuery* query) { + HARD_ASSERT(![query isDocumentQuery], + "Document queries shouldn't go down this path"); + + const ResourcePath& query_path = query.path; + size_t immediate_children_path_length = query_path.size() + 1; + + // TODO(mcg): Actually implement a single-collection query + // + // This is actually executing an ancestor query, traversing the whole subtree + // below the collection which can be horrifically inefficient for some + // structures. The right way to solve this is to implement the full value + // index, but that's not in the cards in the near future so this is the best + // we can do for the moment. + // + // Since we don't yet index the actual properties in the mutations, our + // current approach is to just return all mutation batches that affect + // documents in the collection being queried. + // + // Unlike allMutationBatchesAffectingDocumentKey, this iteration will scan the + // document-mutation index for more than a single document so the associated + // batchIDs will be neither necessarily unique nor in order. This means an + // efficient simultaneous scan isn't possible. + std::string index_prefix = + LevelDbDocumentMutationKey::KeyPrefix(user_id_, query_path); + auto index_iterator = db_.currentTransaction->NewIterator(); + index_iterator->Seek(index_prefix); + + LevelDbDocumentMutationKey row_key; + + // Collect up unique batchIDs encountered during a scan of the index. Use a + // set to accumulate batch IDs so they can be traversed in order in a + // scan of the main table. + // + // This method is faster than performing lookups of the keys with _db->Get and + // keeping a hash of batchIDs that have already been looked up. The + // performance difference is minor for small numbers of keys but > 30% faster + // for larger numbers of keys. + std::set unique_batch_ids; + for (; index_iterator->Valid(); index_iterator->Next()) { + if (!absl::StartsWith(index_iterator->key(), index_prefix) || + !row_key.Decode(index_iterator->key())) { + break; + } + + // Rows with document keys more than one segment longer than the query path + // can't be matches. For example, a query on 'rooms' can't match the + // document /rooms/abc/messages/xyx. + // TODO(mcg): we'll need a different scanner when we implement ancestor + // queries. + if (row_key.document_key().path().size() != + immediate_children_path_length) { + continue; + } + + unique_batch_ids.insert(row_key.batch_id()); + } + + return AllMutationBatchesWithIds(unique_batch_ids); +} + +FSTMutationBatch* _Nullable LevelDbMutationQueue::LookupMutationBatch( + model::BatchId batch_id) { + std::string key = mutation_batch_key(batch_id); + + std::string value; + Status status = db_.currentTransaction->Get(key, &value); + if (!status.ok()) { + if (status.IsNotFound()) { + return nil; + } + HARD_FAIL("Lookup mutation batch (%s, %s) failed with status: %s", user_id_, + batch_id, status.ToString()); + } + + return ParseMutationBatch(value); +} + +FSTMutationBatch* _Nullable LevelDbMutationQueue::NextMutationBatchAfterBatchId( + model::BatchId batch_id) { + BatchId next_batch_id = batch_id + 1; + + std::string key = mutation_batch_key(next_batch_id); + auto it = db_.currentTransaction->NewIterator(); + it->Seek(key); + + LevelDbMutationKey row_key; + if (!it->Valid() || !row_key.Decode(it->key())) { + // Past the last row in the DB or out of the mutations table + return nil; + } + + if (row_key.user_id() != user_id_) { + // Jumped past the last mutation for this user + return nil; + } + + HARD_ASSERT(row_key.batch_id() >= next_batch_id, + "Should have found mutation after %s", next_batch_id); + return ParseMutationBatch(it->value()); +} + +void LevelDbMutationQueue::PerformConsistencyCheck() { + if (!IsEmpty()) { + return; + } + + // Verify that there are no entries in the document-mutation index if the + // queue is empty. + std::string index_prefix = LevelDbDocumentMutationKey::KeyPrefix(user_id_); + auto index_iterator = db_.currentTransaction->NewIterator(); + index_iterator->Seek(index_prefix); + + std::vector dangling_mutation_references; + + for (; index_iterator->Valid(); index_iterator->Next()) { + // Only consider rows matching this index prefix for the current user. + if (!absl::StartsWith(index_iterator->key(), index_prefix)) { + break; + } + + dangling_mutation_references.push_back(DescribeKey(index_iterator)); + } + + HARD_ASSERT( + dangling_mutation_references.empty(), + "Document leak -- detected dangling mutation references when queue " + "is empty. Dangling keys: %s", + util::ToString(dangling_mutation_references)); +} + +NSData* _Nullable LevelDbMutationQueue::GetLastStreamToken() { + return metadata_.lastStreamToken; +} + +void LevelDbMutationQueue::SetLastStreamToken(NSData* _Nullable stream_token) { + metadata_.lastStreamToken = stream_token; + + db_.currentTransaction->Put(mutation_queue_key(), metadata_); +} + +std::vector LevelDbMutationQueue::AllMutationBatchesWithIds( + const std::set& batch_ids) { + std::vector result; + + // Given an ordered set of unique batchIDs perform a skipping scan over the + // main table to find the mutation batches. + auto mutation_iterator = db_.currentTransaction->NewIterator(); + for (BatchId batch_id : batch_ids) { + std::string mutation_key = mutation_batch_key(batch_id); + mutation_iterator->Seek(mutation_key); + if (!mutation_iterator->Valid() || + mutation_iterator->key() != mutation_key) { + HARD_FAIL("Dangling document-mutation reference found: " + "Missing batch %s; seeking there found %s", + DescribeKey(mutation_key), DescribeKey(mutation_iterator)); + } + + result.push_back(ParseMutationBatch(mutation_iterator->value())); + } + return result; +} + +FSTPBMutationQueue* _Nullable LevelDbMutationQueue::MetadataForKey( + const std::string& key) { + std::string value; + Status status = db_.currentTransaction->Get(key, &value); + if (status.ok()) { + NSData* data = [[NSData alloc] initWithBytesNoCopy:(void*)value.data() + length:value.size() + freeWhenDone:NO]; + + NSError* error; + FSTPBMutationQueue* proto = [FSTPBMutationQueue parseFromData:data + error:&error]; + if (!proto) { + HARD_FAIL("FSTPBMutationQueue failed to parse: %s", error); + } + return proto; + } else if (status.IsNotFound()) { + return nil; + } else { + HARD_FAIL("MetadataForKey: failed loading key %s with status: %s", key, + status.ToString()); + } +} + +FSTMutationBatch* LevelDbMutationQueue::ParseMutationBatch( + absl::string_view encoded) { + NSData* data = [[NSData alloc] initWithBytesNoCopy:(void*)encoded.data() + length:encoded.size() + freeWhenDone:NO]; + + NSError* error; + FSTPBWriteBatch* proto = [FSTPBWriteBatch parseFromData:data error:&error]; + if (!proto) { + HARD_FAIL("FSTPBMutationBatch failed to parse: %s", error); + } + + return [serializer_ decodedMutationBatch:proto]; +} + +} // namespace local +} // namespace firestore +} // namespace firebase + +NS_ASSUME_NONNULL_END \ No newline at end of file diff --git a/Firestore/core/src/firebase/firestore/local/memory_mutation_queue.h b/Firestore/core/src/firebase/firestore/local/memory_mutation_queue.h index 34ab37d18fa..bdf4d769d3e 100644 --- a/Firestore/core/src/firebase/firestore/local/memory_mutation_queue.h +++ b/Firestore/core/src/firebase/firestore/local/memory_mutation_queue.h @@ -52,11 +52,7 @@ class MemoryMutationQueue { void Start(); - bool is_empty() { - // If the queue has any entries at all, the first entry must not be a - // tombstone (otherwise it would have been removed already). - return queue_.empty(); - } + bool IsEmpty(); void AcknowledgeBatch(FSTMutationBatch* batch, NSData* _Nullable stream_token); @@ -90,12 +86,8 @@ class MemoryMutationQueue { size_t CalculateByteSize(FSTLocalSerializer* serializer); - NSData* _Nullable last_stream_token() { - return last_stream_token_; - } - void set_last_stream_token(NSData* token) { - last_stream_token_ = token; - } + NSData* _Nullable GetLastStreamToken(); + void SetLastStreamToken(NSData* _Nullable token); private: using DocumentReferenceSet = diff --git a/Firestore/core/src/firebase/firestore/local/memory_mutation_queue.mm b/Firestore/core/src/firebase/firestore/local/memory_mutation_queue.mm index 3b45c264375..5979976d4fb 100644 --- a/Firestore/core/src/firebase/firestore/local/memory_mutation_queue.mm +++ b/Firestore/core/src/firebase/firestore/local/memory_mutation_queue.mm @@ -42,6 +42,12 @@ : persistence_(persistence) { } +bool MemoryMutationQueue::IsEmpty() { + // If the queue has any entries at all, the first entry must not be a + // tombstone (otherwise it would have been removed already). + return queue_.empty(); +} + void MemoryMutationQueue::AcknowledgeBatch(FSTMutationBatch* batch, NSData* _Nullable stream_token) { HARD_ASSERT(!queue_.empty(), "Cannot acknowledge batch on an empty queue"); @@ -60,7 +66,7 @@ // the queue for the duration of the app session in case a user logs out / // back in. To behave like the LevelDB-backed MutationQueue (and accommodate // tests that expect as much), we reset nextBatchID if the queue is empty. - if (is_empty()) { + if (IsEmpty()) { next_batch_id_ = 1; } } @@ -238,6 +244,14 @@ return count; } +NSData* _Nullable MemoryMutationQueue::GetLastStreamToken() { + return last_stream_token_; +} + +void MemoryMutationQueue::SetLastStreamToken(NSData* _Nullable token) { + last_stream_token_ = token; +} + std::vector MemoryMutationQueue::AllMutationBatchesWithIds( const std::set& batch_ids) { std::vector result; From 9789c614eaa74442ad81f835a29e29a9c3152a0d Mon Sep 17 00:00:00 2001 From: Konstantin Varlamov Date: Sun, 27 Jan 2019 16:45:15 -0500 Subject: [PATCH 02/27] C++ migration: port `FSTWatchChangeAggregator` and `FSTTargetState` (#2313) --- .../Example/Tests/Local/FSTLocalStoreTests.mm | 13 +- .../Tests/Remote/FSTRemoteEventTests.mm | 119 ++--- Firestore/Example/Tests/Util/FSTHelpers.mm | 25 +- Firestore/Source/Core/FSTSyncEngine.mm | 2 +- Firestore/Source/Remote/FSTRemoteEvent.h | 64 +-- Firestore/Source/Remote/FSTRemoteEvent.mm | 485 ------------------ Firestore/Source/Remote/FSTRemoteStore.mm | 28 +- .../firebase/firestore/remote/remote_event.h | 304 +++++++++++ .../firebase/firestore/remote/remote_event.mm | 415 +++++++++++++++ 9 files changed, 814 insertions(+), 641 deletions(-) create mode 100644 Firestore/core/src/firebase/firestore/remote/remote_event.h create mode 100644 Firestore/core/src/firebase/firestore/remote/remote_event.mm diff --git a/Firestore/Example/Tests/Local/FSTLocalStoreTests.mm b/Firestore/Example/Tests/Local/FSTLocalStoreTests.mm index 3c4042579fb..98835fad06f 100644 --- a/Firestore/Example/Tests/Local/FSTLocalStoreTests.mm +++ b/Firestore/Example/Tests/Local/FSTLocalStoreTests.mm @@ -37,6 +37,7 @@ #include "Firestore/core/src/firebase/firestore/auth/user.h" #include "Firestore/core/src/firebase/firestore/model/document_map.h" +#include "Firestore/core/src/firebase/firestore/remote/remote_event.h" #include "Firestore/core/src/firebase/firestore/remote/watch_change.h" #include "Firestore/core/src/firebase/firestore/util/status.h" #include "Firestore/core/test/firebase/firestore/testutil/testutil.h" @@ -50,6 +51,7 @@ using firebase::firestore::model::MaybeDocumentMap; using firebase::firestore::model::SnapshotVersion; using firebase::firestore::model::TargetId; +using firebase::firestore::remote::WatchChangeAggregator; using firebase::firestore::remote::WatchTargetChange; using firebase::firestore::remote::WatchTargetChangeState; using firebase::firestore::util::Status; @@ -904,12 +906,11 @@ - (void)testPersistsResumeTokens { NSData *resumeToken = FSTTestResumeTokenFromSnapshotVersion(1000); WatchTargetChange watchChange{WatchTargetChangeState::Current, {targetID}, resumeToken}; - FSTWatchChangeAggregator *aggregator = [[FSTWatchChangeAggregator alloc] - initWithTargetMetadataProvider:[FSTTestTargetMetadataProvider - providerWithSingleResultForKey:testutil::Key("foo/bar") - targets:{targetID}]]; - [aggregator handleTargetChange:watchChange]; - FSTRemoteEvent *remoteEvent = [aggregator remoteEventAtSnapshotVersion:testutil::Version(1000)]; + WatchChangeAggregator aggregator{[FSTTestTargetMetadataProvider + providerWithSingleResultForKey:testutil::Key("foo/bar") + targets:{targetID}]}; + aggregator.HandleTargetChange(watchChange); + FSTRemoteEvent *remoteEvent = aggregator.CreateRemoteEvent(testutil::Version(1000)); [self applyRemoteEvent:remoteEvent]; // Stop listening so that the query should become inactive (but persistent) diff --git a/Firestore/Example/Tests/Remote/FSTRemoteEventTests.mm b/Firestore/Example/Tests/Remote/FSTRemoteEventTests.mm index e7ba8df3183..b728ee94777 100644 --- a/Firestore/Example/Tests/Remote/FSTRemoteEventTests.mm +++ b/Firestore/Example/Tests/Remote/FSTRemoteEventTests.mm @@ -30,6 +30,7 @@ #include "Firestore/core/src/firebase/firestore/model/document_key.h" #include "Firestore/core/src/firebase/firestore/model/types.h" #include "Firestore/core/src/firebase/firestore/remote/existence_filter.h" +#include "Firestore/core/src/firebase/firestore/remote/remote_event.h" #include "Firestore/core/src/firebase/firestore/remote/watch_change.h" #import "Firestore/Example/Tests/Util/FSTHelpers.h" @@ -46,6 +47,7 @@ using firebase::firestore::remote::ExistenceFilter; using firebase::firestore::remote::ExistenceFilterWatchChange; using firebase::firestore::remote::WatchChange; +using firebase::firestore::remote::WatchChangeAggregator; using firebase::firestore::remote::WatchTargetChange; using firebase::firestore::remote::WatchTargetChangeState; using firebase::firestore::testutil::VectorOfUniquePtrs; @@ -135,7 +137,7 @@ - (void)setUp { /** * Creates an aggregator initialized with the set of provided `WatchChange`s. Tests can add further - * changes via `handleDocumentChange`, `handleTargetChange` and `handleExistenceFilterChange`. + * changes via `HandleDocumentChange`, `HandleTargetChange` and `HandleExistenceFilterChange`. * * @param targetMap A map of query data for all active targets. The map must include an entry for * every target referenced by any of the watch changes. @@ -147,13 +149,12 @@ - (void)setUp { * @param watchChanges The watch changes to apply before returning the aggregator. Supported * changes are `DocumentWatchChange` and `WatchTargetChange`. */ -- (FSTWatchChangeAggregator *) +- (WatchChangeAggregator) aggregatorWithTargetMap:(const std::unordered_map &)targetMap outstandingResponses:(const std::unordered_map &)outstandingResponses existingKeys:(DocumentKeySet)existingKeys changes:(const std::vector> &)watchChanges { - FSTWatchChangeAggregator *aggregator = - [[FSTWatchChangeAggregator alloc] initWithTargetMetadataProvider:_targetMetadataProvider]; + WatchChangeAggregator aggregator{_targetMetadataProvider}; std::vector targetIDs; for (const auto &kv : targetMap) { @@ -168,18 +169,18 @@ - (void)setUp { TargetId targetID = kv.first; int count = kv.second; for (int i = 0; i < count; ++i) { - [aggregator recordTargetRequest:targetID]; + aggregator.RecordPendingTargetRequest(targetID); } } for (const std::unique_ptr &change : watchChanges) { switch (change->type()) { case WatchChange::Type::Document: { - [aggregator handleDocumentChange:*static_cast(change.get())]; + aggregator.HandleDocumentChange(*static_cast(change.get())); break; } case WatchChange::Type::TargetChange: { - [aggregator handleTargetChange:*static_cast(change.get())]; + aggregator.HandleTargetChange(*static_cast(change.get())); break; } default: @@ -187,8 +188,8 @@ - (void)setUp { } } - [aggregator handleTargetChange:WatchTargetChange{WatchTargetChangeState::NoChange, targetIDs, - _resumeToken1}]; + aggregator.HandleTargetChange( + WatchTargetChange{WatchTargetChangeState::NoChange, targetIDs, _resumeToken1}); return aggregator; } @@ -213,11 +214,11 @@ - (void)setUp { outstandingResponses:(const std::unordered_map &)outstandingResponses existingKeys:(DocumentKeySet)existingKeys changes:(const std::vector> &)watchChanges { - FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargetMap:targetMap - outstandingResponses:outstandingResponses - existingKeys:existingKeys - changes:watchChanges]; - return [aggregator remoteEventAtSnapshotVersion:testutil::Version(snapshotVersion)]; + WatchChangeAggregator aggregator = [self aggregatorWithTargetMap:targetMap + outstandingResponses:outstandingResponses + existingKeys:existingKeys + changes:watchChanges]; + return aggregator.CreateRemoteEvent(testutil::Version(snapshotVersion)); } - (void)testWillAccumulateDocumentAddedAndRemovedEvents { @@ -378,13 +379,13 @@ - (void)testWillHandleSingleReset { // Reset target WatchTargetChange change{WatchTargetChangeState::Reset, {1}}; - FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargetMap:targetMap - outstandingResponses:_noOutstandingResponses - existingKeys:DocumentKeySet {} - changes:{}]; - [aggregator handleTargetChange:change]; + WatchChangeAggregator aggregator = [self aggregatorWithTargetMap:targetMap + outstandingResponses:_noOutstandingResponses + existingKeys:DocumentKeySet {} + changes:{}]; + aggregator.HandleTargetChange(change); - FSTRemoteEvent *event = [aggregator remoteEventAtSnapshotVersion:testutil::Version(3)]; + FSTRemoteEvent *event = aggregator.CreateRemoteEvent(testutil::Version(3)); XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); XCTAssertEqual(event.documentUpdates.size(), 0); @@ -493,15 +494,15 @@ - (void)testTargetAddedChangeWillResetPreviousState { - (void)testNoChangeWillStillMarkTheAffectedTargets { std::unordered_map targetMap{[self queryDataForTargets:{1}]}; - FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargetMap:targetMap - outstandingResponses:_noOutstandingResponses - existingKeys:DocumentKeySet {} - changes:{}]; + WatchChangeAggregator aggregator = [self aggregatorWithTargetMap:targetMap + outstandingResponses:_noOutstandingResponses + existingKeys:DocumentKeySet {} + changes:{}]; WatchTargetChange change{WatchTargetChangeState::NoChange, {1}, _resumeToken1}; - [aggregator handleTargetChange:change]; + aggregator.HandleTargetChange(change); - FSTRemoteEvent *event = [aggregator remoteEventAtSnapshotVersion:testutil::Version(3)]; + FSTRemoteEvent *event = aggregator.CreateRemoteEvent(testutil::Version(3)); XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); XCTAssertEqual(event.documentUpdates.size(), 0); @@ -521,13 +522,13 @@ - (void)testExistenceFilterMismatchClearsTarget { auto change2 = MakeDocChange({1}, {}, doc2.key, doc2); auto change3 = MakeTargetChange(WatchTargetChangeState::Current, {1}, _resumeToken1); - FSTWatchChangeAggregator *aggregator = [self + WatchChangeAggregator aggregator = [self aggregatorWithTargetMap:targetMap outstandingResponses:_noOutstandingResponses existingKeys:DocumentKeySet{doc1.key, doc2.key} changes:Changes(std::move(change1), std::move(change2), std::move(change3))]; - FSTRemoteEvent *event = [aggregator remoteEventAtSnapshotVersion:testutil::Version(3)]; + FSTRemoteEvent *event = aggregator.CreateRemoteEvent(testutil::Version(3)); XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); XCTAssertEqual(event.documentUpdates.size(), 2); @@ -547,9 +548,9 @@ - (void)testExistenceFilterMismatchClearsTarget { // The existence filter mismatch will remove the document from target 1, // but not synthesize a document delete. ExistenceFilterWatchChange change4{ExistenceFilter{1}, 1}; - [aggregator handleExistenceFilter:change4]; + aggregator.HandleExistenceFilter(change4); - event = [aggregator remoteEventAtSnapshotVersion:testutil::Version(4)]; + event = aggregator.CreateRemoteEvent(testutil::Version(4)); FSTTargetChange *targetChange3 = FSTTestTargetChange( DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{doc1.key, doc2.key}, [NSData data], NO); @@ -563,24 +564,24 @@ - (void)testExistenceFilterMismatchClearsTarget { - (void)testExistenceFilterMismatchRemovesCurrentChanges { std::unordered_map targetMap{[self queryDataForTargets:{1}]}; - FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargetMap:targetMap - outstandingResponses:_noOutstandingResponses - existingKeys:DocumentKeySet {} - changes:{}]; + WatchChangeAggregator aggregator = [self aggregatorWithTargetMap:targetMap + outstandingResponses:_noOutstandingResponses + existingKeys:DocumentKeySet {} + changes:{}]; WatchTargetChange markCurrent{WatchTargetChangeState::Current, {1}, _resumeToken1}; - [aggregator handleTargetChange:markCurrent]; + aggregator.HandleTargetChange(markCurrent); FSTDocument *doc1 = FSTTestDoc("docs/1", 1, @{@"value" : @1}, FSTDocumentStateSynced); DocumentWatchChange addDoc{{1}, {}, doc1.key, doc1}; - [aggregator handleDocumentChange:addDoc]; + aggregator.HandleDocumentChange(addDoc); // The existence filter mismatch will remove the document from target 1, but not synthesize a // document delete. ExistenceFilterWatchChange existenceFilter{ExistenceFilter{0}, 1}; - [aggregator handleExistenceFilter:existenceFilter]; + aggregator.HandleExistenceFilter(existenceFilter); - FSTRemoteEvent *event = [aggregator remoteEventAtSnapshotVersion:testutil::Version(3)]; + FSTRemoteEvent *event = aggregator.CreateRemoteEvent(testutil::Version(3)); XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); XCTAssertEqual(event.documentUpdates.size(), 1); @@ -602,13 +603,13 @@ - (void)testDocumentUpdate { FSTDocument *doc2 = FSTTestDoc("docs/2", 2, @{@"value" : @2}, FSTDocumentStateSynced); auto change2 = MakeDocChange({1}, {}, doc2.key, doc2); - FSTWatchChangeAggregator *aggregator = + WatchChangeAggregator aggregator = [self aggregatorWithTargetMap:targetMap outstandingResponses:_noOutstandingResponses existingKeys:DocumentKeySet {} changes:Changes(std::move(change1), std::move(change2))]; - FSTRemoteEvent *event = [aggregator remoteEventAtSnapshotVersion:testutil::Version(3)]; + FSTRemoteEvent *event = aggregator.CreateRemoteEvent(testutil::Version(3)); XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); XCTAssertEqual(event.documentUpdates.size(), 2); @@ -622,17 +623,17 @@ - (void)testDocumentUpdate { version:testutil::Version(3) hasCommittedMutations:NO]; DocumentWatchChange change3{{}, {1}, deletedDoc1.key, deletedDoc1}; - [aggregator handleDocumentChange:change3]; + aggregator.HandleDocumentChange(change3); FSTDocument *updatedDoc2 = FSTTestDoc("docs/2", 3, @{@"value" : @2}, FSTDocumentStateSynced); DocumentWatchChange change4{{1}, {}, updatedDoc2.key, updatedDoc2}; - [aggregator handleDocumentChange:change4]; + aggregator.HandleDocumentChange(change4); FSTDocument *doc3 = FSTTestDoc("docs/3", 3, @{@"value" : @3}, FSTDocumentStateSynced); DocumentWatchChange change5{{1}, {}, doc3.key, doc3}; - [aggregator handleDocumentChange:change5]; + aggregator.HandleDocumentChange(change5); - event = [aggregator remoteEventAtSnapshotVersion:testutil::Version(3)]; + event = aggregator.CreateRemoteEvent(testutil::Version(3)); XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); XCTAssertEqual(event.documentUpdates.size(), 3); @@ -655,19 +656,19 @@ - (void)testDocumentUpdate { - (void)testResumeTokensHandledPerTarget { std::unordered_map targetMap{[self queryDataForTargets:{1, 2}]}; - FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargetMap:targetMap - outstandingResponses:_noOutstandingResponses - existingKeys:DocumentKeySet {} - changes:{}]; + WatchChangeAggregator aggregator = [self aggregatorWithTargetMap:targetMap + outstandingResponses:_noOutstandingResponses + existingKeys:DocumentKeySet {} + changes:{}]; WatchTargetChange change1{WatchTargetChangeState::Current, {1}, _resumeToken1}; - [aggregator handleTargetChange:change1]; + aggregator.HandleTargetChange(change1); NSData *resumeToken2 = [@"resume2" dataUsingEncoding:NSUTF8StringEncoding]; WatchTargetChange change2{WatchTargetChangeState::Current, {2}, resumeToken2}; - [aggregator handleTargetChange:change2]; + aggregator.HandleTargetChange(change2); - FSTRemoteEvent *event = [aggregator remoteEventAtSnapshotVersion:testutil::Version(3)]; + FSTRemoteEvent *event = aggregator.CreateRemoteEvent(testutil::Version(3)); XCTAssertEqual(event.targetChanges.size(), 2); FSTTargetChange *targetChange1 = @@ -682,23 +683,23 @@ - (void)testResumeTokensHandledPerTarget { - (void)testLastResumeTokenWins { std::unordered_map targetMap{[self queryDataForTargets:{1, 2}]}; - FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargetMap:targetMap - outstandingResponses:_noOutstandingResponses - existingKeys:DocumentKeySet {} - changes:{}]; + WatchChangeAggregator aggregator = [self aggregatorWithTargetMap:targetMap + outstandingResponses:_noOutstandingResponses + existingKeys:DocumentKeySet {} + changes:{}]; WatchTargetChange change1{WatchTargetChangeState::Current, {1}, _resumeToken1}; - [aggregator handleTargetChange:change1]; + aggregator.HandleTargetChange(change1); NSData *resumeToken2 = [@"resume2" dataUsingEncoding:NSUTF8StringEncoding]; WatchTargetChange change2{WatchTargetChangeState::NoChange, {1}, resumeToken2}; - [aggregator handleTargetChange:change2]; + aggregator.HandleTargetChange(change2); NSData *resumeToken3 = [@"resume3" dataUsingEncoding:NSUTF8StringEncoding]; WatchTargetChange change3{WatchTargetChangeState::NoChange, {2}, resumeToken3}; - [aggregator handleTargetChange:change3]; + aggregator.HandleTargetChange(change3); - FSTRemoteEvent *event = [aggregator remoteEventAtSnapshotVersion:testutil::Version(3)]; + FSTRemoteEvent *event = aggregator.CreateRemoteEvent(testutil::Version(3)); XCTAssertEqual(event.targetChanges.size(), 2); FSTTargetChange *targetChange1 = diff --git a/Firestore/Example/Tests/Util/FSTHelpers.mm b/Firestore/Example/Tests/Util/FSTHelpers.mm index 2d02c5902f7..ffd04dd8c1b 100644 --- a/Firestore/Example/Tests/Util/FSTHelpers.mm +++ b/Firestore/Example/Tests/Util/FSTHelpers.mm @@ -49,6 +49,7 @@ #include "Firestore/core/src/firebase/firestore/model/precondition.h" #include "Firestore/core/src/firebase/firestore/model/resource_path.h" #include "Firestore/core/src/firebase/firestore/model/transform_operations.h" +#include "Firestore/core/src/firebase/firestore/remote/remote_event.h" #include "Firestore/core/src/firebase/firestore/remote/watch_change.h" #include "Firestore/core/src/firebase/firestore/util/string_apple.h" #include "Firestore/core/test/firebase/firestore/testutil/testutil.h" @@ -72,6 +73,7 @@ using firebase::firestore::model::TargetId; using firebase::firestore::model::TransformOperation; using firebase::firestore::remote::DocumentWatchChange; +using firebase::firestore::remote::WatchChangeAggregator; NS_ASSUME_NONNULL_BEGIN @@ -377,12 +379,10 @@ - (nullable FSTQueryData *)queryDataForTarget:(TargetId)targetID { HARD_ASSERT(![doc isKindOfClass:[FSTDocument class]] || ![(FSTDocument *)doc hasLocalMutations], "Docs from remote updates shouldn't have local changes."); DocumentWatchChange change{addedToTargets, {}, doc.key, doc}; - FSTWatchChangeAggregator *aggregator = [[FSTWatchChangeAggregator alloc] - initWithTargetMetadataProvider:[FSTTestTargetMetadataProvider - providerWithEmptyResultForKey:doc.key - targets:addedToTargets]]; - [aggregator handleDocumentChange:change]; - return [aggregator remoteEventAtSnapshotVersion:doc.version]; + WatchChangeAggregator aggregator{ + [FSTTestTargetMetadataProvider providerWithEmptyResultForKey:doc.key targets:addedToTargets]}; + aggregator.HandleDocumentChange(change); + return aggregator.CreateRemoteEvent(doc.version); } FSTTargetChange *FSTTestTargetChangeMarkCurrent() { @@ -425,13 +425,12 @@ - (nullable FSTQueryData *)queryDataForTarget:(TargetId)targetID { std::vector listens = updatedInTargets; listens.insert(listens.end(), removedFromTargets.begin(), removedFromTargets.end()); - FSTWatchChangeAggregator *aggregator = [[FSTWatchChangeAggregator alloc] - initWithTargetMetadataProvider:[FSTTestTargetMetadataProvider - providerWithSingleResultForKey:doc.key - listenTargets:listens - limboTargets:limboTargets]]; - [aggregator handleDocumentChange:change]; - return [aggregator remoteEventAtSnapshotVersion:doc.version]; + WatchChangeAggregator aggregator{[FSTTestTargetMetadataProvider + providerWithSingleResultForKey:doc.key + listenTargets:listens + limboTargets:limboTargets]}; + aggregator.HandleDocumentChange(change); + return aggregator.CreateRemoteEvent(doc.version); } FSTRemoteEvent *FSTTestUpdateRemoteEvent(FSTMaybeDocument *doc, diff --git a/Firestore/Source/Core/FSTSyncEngine.mm b/Firestore/Source/Core/FSTSyncEngine.mm index f766622a8bb..57ec5434316 100644 --- a/Firestore/Source/Core/FSTSyncEngine.mm +++ b/Firestore/Source/Core/FSTSyncEngine.mm @@ -134,7 +134,7 @@ explicit LimboResolution(const DocumentKey &key) : key{key} { /** * Set to true once we've received a document. This is used in remoteKeysForTarget and - * ultimately used by FSTWatchChangeAggregator to decide whether it needs to manufacture a delete + * ultimately used by `WatchChangeAggregator` to decide whether it needs to manufacture a delete * event for the target once the target is CURRENT. */ bool document_received = false; diff --git a/Firestore/Source/Remote/FSTRemoteEvent.h b/Firestore/Source/Remote/FSTRemoteEvent.h index e5c5d602686..3a97a4be138 100644 --- a/Firestore/Source/Remote/FSTRemoteEvent.h +++ b/Firestore/Source/Remote/FSTRemoteEvent.h @@ -25,6 +25,7 @@ #include "Firestore/core/src/firebase/firestore/model/document_key_set.h" #include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" #include "Firestore/core/src/firebase/firestore/model/types.h" +#include "Firestore/core/src/firebase/firestore/remote/remote_event.h" #include "Firestore/core/src/firebase/firestore/remote/watch_change.h" @class FSTMaybeDocument; @@ -32,24 +33,6 @@ NS_ASSUME_NONNULL_BEGIN -/** - * Interface implemented by RemoteStore to expose target metadata to the FSTWatchChangeAggregator. - */ -@protocol FSTTargetMetadataProvider - -/** - * Returns the set of remote document keys for the given target ID as of the last raised snapshot. - */ -- (firebase::firestore::model::DocumentKeySet)remoteKeysForTarget: - (firebase::firestore::model::TargetId)targetID; - -/** - * Returns the FSTQueryData for an active target ID or 'null' if this query has become inactive - */ -- (nullable FSTQueryData *)queryDataForTarget:(firebase::firestore::model::TargetId)targetID; - -@end - #pragma mark - FSTTargetChange /** @@ -155,49 +138,4 @@ NS_ASSUME_NONNULL_BEGIN @end -#pragma mark - FSTWatchChangeAggregator - -/** - * A helper class to accumulate watch changes into a FSTRemoteEvent and other target - * information. - */ -@interface FSTWatchChangeAggregator : NSObject - -- (instancetype)initWithTargetMetadataProvider:(id)targetMetadataProvider - NS_DESIGNATED_INITIALIZER; - -- (instancetype)init NS_UNAVAILABLE; - -/** Processes and adds the DocumentWatchChange to the current set of changes. */ -- (void)handleDocumentChange: - (const firebase::firestore::remote::DocumentWatchChange &)documentChange; - -/** Processes and adds the WatchTargetChange to the current set of changes. */ -- (void)handleTargetChange:(const firebase::firestore::remote::WatchTargetChange &)targetChange; - -/** Removes the in-memory state for the provided target. */ -- (void)removeTarget:(firebase::firestore::model::TargetId)targetID; - -/** - * Handles existence filters and synthesizes deletes for filter mismatches. Targets that are - * invalidated by filter mismatches are added to `targetMismatches`. - */ -- (void)handleExistenceFilter: - (const firebase::firestore::remote::ExistenceFilterWatchChange &)existenceFilter; - -/** - * Increment the number of acks needed from watch before we can consider the server to be 'in-sync' - * with the client's active targets. - */ -- (void)recordTargetRequest:(firebase::firestore::model::TargetId)targetID; - -/** - * Converts the current state into a remote event with the snapshot version taken from the - * initializer. - */ -- (FSTRemoteEvent *)remoteEventAtSnapshotVersion: - (const firebase::firestore::model::SnapshotVersion &)snapshotVersion; - -@end - NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Remote/FSTRemoteEvent.mm b/Firestore/Source/Remote/FSTRemoteEvent.mm index 00ea188e1da..408c2501ce8 100644 --- a/Firestore/Source/Remote/FSTRemoteEvent.mm +++ b/Firestore/Source/Remote/FSTRemoteEvent.mm @@ -98,149 +98,6 @@ - (BOOL)isEqual:(id)other { @end -#pragma mark - FSTTargetState - -/** Tracks the internal state of a Watch target. */ -@interface FSTTargetState : NSObject - -/** - * Whether this target has been marked 'current'. - * - * 'Current' has special meaning in the RPC protocol: It implies that the Watch backend has sent us - * all changes up to the point at which the target was added and that the target is consistent with - * the rest of the watch stream. - */ -@property(nonatomic) BOOL current; - -/** The last resume token sent to us for this target. */ -@property(nonatomic, readonly, strong) NSData *resumeToken; - -/** Whether we have modified any state that should trigger a snapshot. */ -@property(nonatomic, readonly) BOOL hasPendingChanges; - -/** Whether this target has pending target adds or target removes. */ -- (BOOL)isPending; - -/** - * Applies the resume token to the TargetChange, but only when it has a new value. Empty - * resumeTokens are discarded. - */ -- (void)updateResumeToken:(NSData *)resumeToken; - -/** Resets the document changes and sets `hasPendingChanges` to false. */ -- (void)clearPendingChanges; -/** - * Creates a target change from the current set of changes. - * - * To reset the document changes after raising this snapshot, call `clearPendingChanges()`. - */ -- (FSTTargetChange *)toTargetChange; - -- (void)recordTargetRequest; -- (void)recordTargetResponse; -- (void)markCurrent; -- (void)addDocumentChangeWithType:(DocumentViewChangeType)type - forKey:(const DocumentKey &)documentKey; -- (void)removeDocumentChangeForKey:(const DocumentKey &)documentKey; - -@end - -@implementation FSTTargetState { - /** - * The number of outstanding responses (adds or removes) that we are waiting on. We only consider - * targets active that have no outstanding responses. - */ - int _outstandingResponses; - - /** - * Keeps track of the document changes since the last raised snapshot. - * - * These changes are continuously updated as we receive document updates and always reflect the - * current set of changes against the last issued snapshot. - */ - std::unordered_map _documentChanges; -} - -- (instancetype)init { - if (self = [super init]) { - _resumeToken = [NSData data]; - _outstandingResponses = 0; - - // We initialize to 'true' so that newly-added targets are included in the next RemoteEvent. - _hasPendingChanges = YES; - } - return self; -} - -- (BOOL)isPending { - return _outstandingResponses != 0; -} - -- (void)updateResumeToken:(NSData *)resumeToken { - if (resumeToken.length > 0) { - _hasPendingChanges = YES; - _resumeToken = [resumeToken copy]; - } -} - -- (void)clearPendingChanges { - _hasPendingChanges = NO; - _documentChanges.clear(); -} - -- (void)recordTargetRequest { - _outstandingResponses += 1; -} - -- (void)recordTargetResponse { - _outstandingResponses -= 1; -} - -- (void)markCurrent { - _hasPendingChanges = YES; - _current = true; -} - -- (void)addDocumentChangeWithType:(DocumentViewChangeType)type - forKey:(const DocumentKey &)documentKey { - _hasPendingChanges = YES; - _documentChanges[documentKey] = type; -} - -- (void)removeDocumentChangeForKey:(const DocumentKey &)documentKey { - _hasPendingChanges = YES; - _documentChanges.erase(documentKey); -} - -- (FSTTargetChange *)toTargetChange { - DocumentKeySet addedDocuments; - DocumentKeySet modifiedDocuments; - DocumentKeySet removedDocuments; - - for (const auto &entry : _documentChanges) { - switch (entry.second) { - case DocumentViewChangeType::kAdded: - addedDocuments = addedDocuments.insert(entry.first); - break; - case DocumentViewChangeType::kModified: - modifiedDocuments = modifiedDocuments.insert(entry.first); - break; - case DocumentViewChangeType::kRemoved: - removedDocuments = removedDocuments.insert(entry.first); - break; - default: - HARD_FAIL("Encountered invalid change type: %s", entry.second); - } - } - - return [[FSTTargetChange alloc] initWithResumeToken:_resumeToken - current:_current - addedDocuments:std::move(addedDocuments) - modifiedDocuments:std::move(modifiedDocuments) - removedDocuments:std::move(removedDocuments)]; -} -@end - #pragma mark - FSTRemoteEvent @implementation FSTRemoteEvent { @@ -291,346 +148,4 @@ @implementation FSTRemoteEvent { @end -#pragma mark - FSTWatchChangeAggregator - -@implementation FSTWatchChangeAggregator { - /** The internal state of all tracked targets. */ - std::unordered_map _targetStates; - - /** Keeps track of document to update */ - std::unordered_map _pendingDocumentUpdates; - - /** A mapping of document keys to their set of target IDs. */ - std::unordered_map, DocumentKeyHash> - _pendingDocumentTargetMappings; - - /** - * A list of targets with existence filter mismatches. These targets are known to be inconsistent - * and their listens needs to be re-established by RemoteStore. - */ - std::unordered_set _pendingTargetResets; - - id _targetMetadataProvider; -} - -- (instancetype)initWithTargetMetadataProvider: - (id)targetMetadataProvider { - self = [super init]; - if (self) { - _targetMetadataProvider = targetMetadataProvider; - } - return self; -} - -- (void)handleDocumentChange:(const DocumentWatchChange &)documentChange { - for (TargetId targetID : documentChange.updated_target_ids()) { - if ([documentChange.new_document() isKindOfClass:[FSTDocument class]]) { - [self addDocument:documentChange.new_document() toTarget:targetID]; - } else if ([documentChange.new_document() isKindOfClass:[FSTDeletedDocument class]]) { - [self removeDocument:documentChange.new_document() - withKey:documentChange.document_key() - fromTarget:targetID]; - } - } - - for (TargetId targetID : documentChange.removed_target_ids()) { - [self removeDocument:documentChange.new_document() - withKey:documentChange.document_key() - fromTarget:targetID]; - } -} - -- (void)handleTargetChange:(const WatchTargetChange &)targetChange { - for (TargetId targetID : [self targetIdsForChange:targetChange]) { - FSTTargetState *targetState = [self ensureTargetStateForTarget:targetID]; - switch (targetChange.state()) { - case WatchTargetChangeState::NoChange: - if ([self isActiveTarget:targetID]) { - [targetState updateResumeToken:targetChange.resume_token()]; - } - break; - case WatchTargetChangeState::Added: - // We need to decrement the number of pending acks needed from watch for this targetId. - [targetState recordTargetResponse]; - if (!targetState.isPending) { - // We have a freshly added target, so we need to reset any state that we had previously. - // This can happen e.g. when remove and add back a target for existence filter mismatches. - [targetState clearPendingChanges]; - } - [targetState updateResumeToken:targetChange.resume_token()]; - break; - case WatchTargetChangeState::Removed: - // We need to keep track of removed targets to we can post-filter and remove any target - // changes. - [targetState recordTargetResponse]; - if (!targetState.isPending) { - [self removeTarget:targetID]; - } - HARD_ASSERT(targetChange.cause().ok(), - "WatchChangeAggregator does not handle errored targets"); - break; - case WatchTargetChangeState::Current: - if ([self isActiveTarget:targetID]) { - [targetState markCurrent]; - [targetState updateResumeToken:targetChange.resume_token()]; - } - break; - case WatchTargetChangeState::Reset: - if ([self isActiveTarget:targetID]) { - // Reset the target and synthesizes removes for all existing documents. The backend will - // re-add any documents that still match the target before it sends the next global - // snapshot. - [self resetTarget:targetID]; - [targetState updateResumeToken:targetChange.resume_token()]; - } - break; - default: - HARD_FAIL("Unknown target watch change state: %s", targetChange.state()); - } - } -} - -/** - * Returns all targetIds that the watch change applies to: either the targetIds explicitly listed - * in the change or the targetIds of all currently active targets. - */ -- (std::vector)targetIdsForChange:(const WatchTargetChange &)targetChange { - if (!targetChange.target_ids().empty()) { - return targetChange.target_ids(); - } - - std::vector result; - result.reserve(_targetStates.size()); - for (const auto &entry : _targetStates) { - result.push_back(entry.first); - } - - return result; -} - -- (void)removeTarget:(TargetId)targetID { - _targetStates.erase(targetID); -} - -- (void)handleExistenceFilter:(const ExistenceFilterWatchChange &)existenceFilter { - TargetId targetID = existenceFilter.target_id(); - int expectedCount = existenceFilter.filter().count(); - - FSTQueryData *queryData = [self queryDataForActiveTarget:targetID]; - if (queryData) { - FSTQuery *query = queryData.query; - if ([query isDocumentQuery]) { - if (expectedCount == 0) { - // The existence filter told us the document does not exist. We deduce that this document - // does not exist and apply a deleted document to our updates. Without applying this deleted - // document there might be another query that will raise this document as part of a snapshot - // until it is resolved, essentially exposing inconsistency between queries. - DocumentKey key{query.path}; - [self removeDocument:[FSTDeletedDocument documentWithKey:key - version:SnapshotVersion::None() - hasCommittedMutations:NO] - withKey:key - fromTarget:targetID]; - } else { - HARD_ASSERT(expectedCount == 1, "Single document existence filter with count: %s", - expectedCount); - } - } else { - int currentSize = [self currentDocumentCountForTarget:targetID]; - if (currentSize != expectedCount) { - // Existence filter mismatch: We reset the mapping and raise a new snapshot with - // `isFromCache:true`. - [self resetTarget:targetID]; - _pendingTargetResets.insert(targetID); - } - } - } -} - -- (int)currentDocumentCountForTarget:(TargetId)targetID { - FSTTargetState *targetState = [self ensureTargetStateForTarget:targetID]; - FSTTargetChange *targetChange = [targetState toTargetChange]; - return ([_targetMetadataProvider remoteKeysForTarget:targetID].size() + - targetChange.addedDocuments.size() - targetChange.removedDocuments.size()); -} - -/** - * Resets the state of a Watch target to its initial state (e.g. sets 'current' to false, clears the - * resume token and removes its target mapping from all documents). - */ -- (void)resetTarget:(TargetId)targetID { - auto currentTargetState = _targetStates.find(targetID); - HARD_ASSERT(currentTargetState != _targetStates.end() && !(currentTargetState->second.isPending), - "Should only reset active targets"); - - _targetStates[targetID] = [FSTTargetState new]; - - // Trigger removal for any documents currently mapped to this target. These removals will be part - // of the initial snapshot if Watch does not resend these documents. - DocumentKeySet existingKeys = [_targetMetadataProvider remoteKeysForTarget:targetID]; - - for (const DocumentKey &key : existingKeys) { - [self removeDocument:nil withKey:key fromTarget:targetID]; - } -} - -/** - * Adds the provided document to the internal list of document updates and its document key to the - * given target's mapping. - */ -- (void)addDocument:(FSTMaybeDocument *)document toTarget:(TargetId)targetID { - if (![self isActiveTarget:targetID]) { - return; - } - - DocumentViewChangeType changeType = [self containsDocument:document.key inTarget:targetID] - ? DocumentViewChangeType::kModified - : DocumentViewChangeType::kAdded; - - FSTTargetState *targetState = [self ensureTargetStateForTarget:targetID]; - [targetState addDocumentChangeWithType:changeType forKey:document.key]; - - _pendingDocumentUpdates[document.key] = document; - _pendingDocumentTargetMappings[document.key].insert(targetID); -} - -/** - * Removes the provided document from the target mapping. If the document no longer matches the - * target, but the document's state is still known (e.g. we know that the document was deleted or we - * received the change that caused the filter mismatch), the new document can be provided to update - * the remote document cache. - */ -- (void)removeDocument:(FSTMaybeDocument *_Nullable)document - withKey:(const DocumentKey &)key - fromTarget:(TargetId)targetID { - if (![self isActiveTarget:targetID]) { - return; - } - - FSTTargetState *targetState = [self ensureTargetStateForTarget:targetID]; - - if ([self containsDocument:key inTarget:targetID]) { - [targetState addDocumentChangeWithType:DocumentViewChangeType::kRemoved forKey:key]; - } else { - // The document may have entered and left the target before we raised a snapshot, so we can just - // ignore the change. - [targetState removeDocumentChangeForKey:key]; - } - _pendingDocumentTargetMappings[key].insert(targetID); - - if (document) { - _pendingDocumentUpdates[key] = document; - } -} - -/** - * Returns whether the LocalStore considers the document to be part of the specified target. - */ -- (BOOL)containsDocument:(const DocumentKey &)key inTarget:(TargetId)targetID { - const DocumentKeySet &existingKeys = [_targetMetadataProvider remoteKeysForTarget:targetID]; - return existingKeys.contains(key); -} - -- (FSTTargetState *)ensureTargetStateForTarget:(TargetId)targetID { - if (!_targetStates[targetID]) { - _targetStates[targetID] = [FSTTargetState new]; - } - - return _targetStates[targetID]; -} - -/** - * Returns YES if the given targetId is active. Active targets are those for which there are no - * pending requests to add a listen and are in the current list of targets the client cares about. - * - * Clients can repeatedly listen and stop listening to targets, so this check is useful in - * preventing in preventing race conditions for a target where events arrive but the server hasn't - * yet acknowledged the intended change in state. - */ -- (BOOL)isActiveTarget:(TargetId)targetID { - return [self queryDataForActiveTarget:targetID] != nil; -} - -- (nullable FSTQueryData *)queryDataForActiveTarget:(TargetId)targetID { - auto targetState = _targetStates.find(targetID); - return targetState != _targetStates.end() && targetState->second.isPending - ? nil - : [_targetMetadataProvider queryDataForTarget:targetID]; -} - -- (FSTRemoteEvent *)remoteEventAtSnapshotVersion:(const SnapshotVersion &)snapshotVersion { - std::unordered_map targetChanges; - - for (const auto &entry : _targetStates) { - TargetId targetID = entry.first; - FSTTargetState *targetState = entry.second; - - FSTQueryData *queryData = [self queryDataForActiveTarget:targetID]; - if (queryData) { - if (targetState.current && [queryData.query isDocumentQuery]) { - // Document queries for document that don't exist can produce an empty result set. To update - // our local cache, we synthesize a document delete if we have not previously received the - // document. This resolves the limbo state of the document, removing it from - // limboDocumentRefs. - DocumentKey key{queryData.query.path}; - if (_pendingDocumentUpdates.find(key) == _pendingDocumentUpdates.end() && - ![self containsDocument:key inTarget:targetID]) { - [self removeDocument:[FSTDeletedDocument documentWithKey:key - version:snapshotVersion - hasCommittedMutations:NO] - withKey:key - fromTarget:targetID]; - } - } - - if (targetState.hasPendingChanges) { - targetChanges[targetID] = [targetState toTargetChange]; - [targetState clearPendingChanges]; - } - } - } - - DocumentKeySet resolvedLimboDocuments; - - // We extract the set of limbo-only document updates as the GC logic special-cases documents that - // do not appear in the query cache. - // - // TODO(gsoltis): Expand on this comment. - for (const auto &entry : _pendingDocumentTargetMappings) { - BOOL isOnlyLimboTarget = YES; - - for (TargetId targetID : entry.second) { - FSTQueryData *queryData = [self queryDataForActiveTarget:targetID]; - if (queryData && queryData.purpose != FSTQueryPurposeLimboResolution) { - isOnlyLimboTarget = NO; - break; - } - } - - if (isOnlyLimboTarget) { - resolvedLimboDocuments = resolvedLimboDocuments.insert(entry.first); - } - } - - FSTRemoteEvent *remoteEvent = - [[FSTRemoteEvent alloc] initWithSnapshotVersion:snapshotVersion - targetChanges:targetChanges - targetMismatches:_pendingTargetResets - documentUpdates:_pendingDocumentUpdates - limboDocuments:resolvedLimboDocuments]; - - _pendingDocumentUpdates.clear(); - _pendingDocumentTargetMappings.clear(); - _pendingTargetResets.clear(); - - return remoteEvent; -} - -- (void)recordTargetRequest:(TargetId)targetID { - // For each request we get we need to record we need a response for it. - FSTTargetState *targetState = [self ensureTargetStateForTarget:targetID]; - [targetState recordTargetRequest]; -} -@end - NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Remote/FSTRemoteStore.mm b/Firestore/Source/Remote/FSTRemoteStore.mm index 92893b9efa3..507c4c6ec6f 100644 --- a/Firestore/Source/Remote/FSTRemoteStore.mm +++ b/Firestore/Source/Remote/FSTRemoteStore.mm @@ -36,6 +36,7 @@ #include "Firestore/core/src/firebase/firestore/model/document_key.h" #include "Firestore/core/src/firebase/firestore/model/mutation_batch.h" #include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" +#include "Firestore/core/src/firebase/firestore/remote/remote_event.h" #include "Firestore/core/src/firebase/firestore/remote/stream.h" #include "Firestore/core/src/firebase/firestore/util/error_apple.h" #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" @@ -60,6 +61,7 @@ using firebase::firestore::remote::DocumentWatchChange; using firebase::firestore::remote::ExistenceFilterWatchChange; using firebase::firestore::remote::WatchChange; +using firebase::firestore::remote::WatchChangeAggregator; using firebase::firestore::remote::WatchTargetChange; using firebase::firestore::remote::WatchTargetChangeState; using util::AsyncQueue; @@ -87,8 +89,6 @@ @interface FSTRemoteStore () @property(nonatomic, strong, readonly) FSTOnlineStateTracker *onlineStateTracker; -@property(nonatomic, strong, nullable) FSTWatchChangeAggregator *watchChangeAggregator; - /** * A list of up to kMaxPendingWrites writes that we have fetched from the LocalStore via * fillWritePipeline and have or will send to the write stream. @@ -107,7 +107,9 @@ @interface FSTRemoteStore () @end @implementation FSTRemoteStore { + std::unique_ptr _watchChangeAggregator; /** The client-side proxy for interacting with the backend. */ + std::shared_ptr _datastore; /** * A mapping of watched targets that the client cares about tracking and the @@ -241,7 +243,7 @@ - (void)credentialDidChange { - (void)startWatchStream { HARD_ASSERT([self shouldStartWatchStream], "startWatchStream: called when shouldStartWatchStream: is false."); - _watchChangeAggregator = [[FSTWatchChangeAggregator alloc] initWithTargetMetadataProvider:self]; + _watchChangeAggregator = absl::make_unique(self); _watchStream->Start(); [self.onlineStateTracker handleWatchStreamStart]; @@ -262,7 +264,7 @@ - (void)listenToTargetWithQueryData:(FSTQueryData *)queryData { } - (void)sendWatchRequestWithQueryData:(FSTQueryData *)queryData { - [self.watchChangeAggregator recordTargetRequest:queryData.targetID]; + _watchChangeAggregator->RecordPendingTargetRequest(queryData.targetID); _watchStream->WatchQuery(queryData); } @@ -287,7 +289,7 @@ - (void)stopListeningToTargetID:(TargetId)targetID { } - (void)sendUnwatchRequestForTargetID:(TargetId)targetID { - [self.watchChangeAggregator recordTargetRequest:targetID]; + _watchChangeAggregator->RecordPendingTargetRequest(targetID); _watchStream->UnwatchTargetId(targetID); } @@ -300,7 +302,7 @@ - (BOOL)shouldStartWatchStream { } - (void)cleanUpWatchStreamState { - _watchChangeAggregator = nil; + _watchChangeAggregator.reset(); } - (void)watchStreamDidOpen { @@ -322,16 +324,15 @@ - (void)watchStreamDidChange:(const WatchChange &)change // There was an error on a target, don't wait for a consistent snapshot to raise events return [self processTargetErrorForWatchChange:watchTargetChange]; } else { - [self.watchChangeAggregator handleTargetChange:watchTargetChange]; + _watchChangeAggregator->HandleTargetChange(watchTargetChange); } } else if (change.type() == WatchChange::Type::Document) { - [self.watchChangeAggregator - handleDocumentChange:static_cast(change)]; + _watchChangeAggregator->HandleDocumentChange(static_cast(change)); } else { HARD_ASSERT(change.type() == WatchChange::Type::ExistenceFilter, "Expected watchChange to be an instance of ExistenceFilterWatchChange"); - [self.watchChangeAggregator - handleExistenceFilter:static_cast(change)]; + _watchChangeAggregator->HandleExistenceFilter( + static_cast(change)); } if (snapshotVersion != SnapshotVersion::None() && @@ -371,8 +372,7 @@ - (void)raiseWatchSnapshotWithSnapshotVersion:(const SnapshotVersion &)snapshotV HARD_ASSERT(snapshotVersion != SnapshotVersion::None(), "Can't raise event for unknown SnapshotVersion"); - FSTRemoteEvent *remoteEvent = - [self.watchChangeAggregator remoteEventAtSnapshotVersion:snapshotVersion]; + FSTRemoteEvent *remoteEvent = _watchChangeAggregator->CreateRemoteEvent(snapshotVersion); // Update in-memory resume tokens. FSTLocalStore will update the persistent view of these when // applying the completed FSTRemoteEvent. @@ -436,7 +436,7 @@ - (void)processTargetErrorForWatchChange:(const WatchTargetChange &)change { auto found = _listenTargets.find(targetID); if (found != _listenTargets.end()) { _listenTargets.erase(found); - [self.watchChangeAggregator removeTarget:targetID]; + _watchChangeAggregator->RemoveTarget(targetID); [self.syncEngine rejectListenWithTargetID:targetID error:util::MakeNSError(change.cause())]; } } diff --git a/Firestore/core/src/firebase/firestore/remote/remote_event.h b/Firestore/core/src/firebase/firestore/remote/remote_event.h new file mode 100644 index 00000000000..e19a9badc06 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/remote/remote_event.h @@ -0,0 +1,304 @@ +/* + * Copyright 2019 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. + */ + +#ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_REMOTE_REMOTE_EVENT_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_REMOTE_REMOTE_EVENT_H_ + +#if !defined(__OBJC__) +// TODO(varconst): the only dependencies are `FSTMaybeDocument` and `NSData` +// (the latter is used to represent the resume token). +#error "This header only supports Objective-C++" +#endif // !defined(__OBJC__) + +#import + +#include +#include +#include +#include + +#include "Firestore/core/src/firebase/firestore/core/view_snapshot.h" +#include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/model/document_key_set.h" +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" +#include "Firestore/core/src/firebase/firestore/model/types.h" +#include "Firestore/core/src/firebase/firestore/remote/watch_change.h" + +@class FSTMaybeDocument; +@class FSTQueryData; +@class FSTRemoteEvent; +@class FSTTargetChange; + +NS_ASSUME_NONNULL_BEGIN + +/** + * Interface implemented by RemoteStore to expose target metadata to the + * `WatchChangeAggregator`. + */ +@protocol FSTTargetMetadataProvider + +/** + * Returns the set of remote document keys for the given target ID as of the + * last raised snapshot. + */ +- (firebase::firestore::model::DocumentKeySet)remoteKeysForTarget: + (firebase::firestore::model::TargetId)targetID; + +/** + * Returns the FSTQueryData for an active target ID or 'null' if this query has + * become inactive + */ +- (nullable FSTQueryData*)queryDataForTarget: + (firebase::firestore::model::TargetId)targetID; + +@end + +namespace firebase { +namespace firestore { +namespace remote { + +/** Tracks the internal state of a Watch target. */ +class TargetState { + public: + TargetState(); + + /** + * Whether this target has been marked 'current'. + * + * 'Current' has special meaning in the RPC protocol: It implies that the + * Watch backend has sent us all changes up to the point at which the target + * was added and that the target is consistent with the rest of the watch + * stream. + */ + bool Current() const { + return current_; + } + + /** The last resume token sent to us for this target. */ + NSData* resume_token() const { + return resume_token_; + } + + /** Whether this target has pending target adds or target removes. */ + bool IsPending() const { + return outstanding_responses_ != 0; + } + + /** Whether we have modified any state that should trigger a snapshot. */ + bool HasPendingChanges() const { + return has_pending_changes_; + } + + /** + * Applies the resume token to the `TargetChange`, but only when it has a new + * value. Empty resume tokens are discarded. + */ + void UpdateResumeToken(NSData* resume_token); + + /** + * Creates a target change from the current set of changes. + * + * To reset the document changes after raising this snapshot, call + * `ClearPendingChanges()`. + */ + FSTTargetChange* ToTargetChange() const; + + /** Resets the document changes and sets `HasPendingChanges` to false. */ + void ClearPendingChanges(); + + void AddDocumentChange(const model::DocumentKey& document_key, + core::DocumentViewChangeType type); + void RemoveDocumentChange(const model::DocumentKey& document_key); + void RecordPendingTargetRequest(); + void RecordTargetResponse(); + void MarkCurrent(); + + private: + /** + * The number of outstanding responses (adds or removes) that we are waiting + * on. We only consider targets active that have no outstanding responses. + */ + int outstanding_responses_ = 0; + + /** + * Keeps track of the document changes since the last raised snapshot. + * + * These changes are continuously updated as we receive document updates and + * always reflect the current set of changes against the last issued snapshot. + */ + std::unordered_map + document_changes_; + + NSData* resume_token_; + + bool current_ = false; + + /** + * Whether this target state should be included in the next snapshot. We + * initialize to true so that newly-added targets are included in the next + * RemoteEvent. + */ + bool has_pending_changes_ = true; +}; + +/** + * A helper class to accumulate watch changes into a `RemoteEvent` and other + * target information. + */ +class WatchChangeAggregator { + public: + explicit WatchChangeAggregator( + id target_metadata_provider) + : target_metadata_provider_{target_metadata_provider} { + } + + /** + * Processes and adds the `DocumentWatchChange` to the current set of changes. + */ + void HandleDocumentChange(const DocumentWatchChange& document_change); + + /** + * Processes and adds the `WatchTargetChange` to the current set of changes. + */ + void HandleTargetChange(const WatchTargetChange& target_change); + + /** + * Handles existence filters and synthesizes deletes for filter mismatches. + * Targets that are invalidated by filter mismatches are added to + * `pending_target_resets_`. + */ + void HandleExistenceFilter( + const ExistenceFilterWatchChange& existence_filter); + + /** + * Converts the current state into a remote event with the snapshot version + * taken from the initializer. Resets the accumulated changes before + * returning. + */ + FSTRemoteEvent* CreateRemoteEvent( + const model::SnapshotVersion& snapshot_version); + + /** Removes the in-memory state for the provided target. */ + void RemoveTarget(model::TargetId target_id); + + /** + * Increment the number of acks needed from watch before we can consider the + * server to be 'in-sync' with the client's active targets. + */ + void RecordPendingTargetRequest(model::TargetId target_id); + + private: + /** + * Returns all `targetId`s that the watch change applies to: either the + * `targetId`s explicitly listed in the change or the `targetId`s of all + * currently active targets. + */ + std::vector GetTargetIds( + const WatchTargetChange& target_change) const; + + /** + * Adds the provided document to the internal list of document updates and its + * document key to the given target's mapping. + */ + void AddDocumentToTarget(model::TargetId target_id, + FSTMaybeDocument* document); + + /** + * Removes the provided document from the target mapping. If the document no + * longer matches the target, but the document's state is still known (e.g. we + * know that the document was deleted or we received the change that caused + * the filter mismatch), the new document can be provided to update the remote + * document cache. + */ + void RemoveDocumentFromTarget(model::TargetId target_id, + const model::DocumentKey& key, + FSTMaybeDocument* _Nullable updated_document); + + /** + * Returns the current count of documents in the target. This includes both + * the number of documents that the LocalStore considers to be part of the + * target as well as any accumulated changes. + */ + int GetCurrentDocumentCountForTarget(model::TargetId target_id); + + // PORTING NOTE: this method exists only for consistency with other platforms; + // in C++, it's pretty much unnecessary. + TargetState& EnsureTargetState(model::TargetId target_id); + + /** + * Returns true if the given `target_id` is active. Active targets are those + * for which there are no pending requests to add a listen and are in the + * current list of targets the client cares about. + * + * Clients can repeatedly listen and stop listening to targets, so this check + * is useful in preventing race conditions for a target where events arrive + * but the server hasn't yet acknowledged the intended change in state. + */ + bool IsActiveTarget(model::TargetId target_id) const; + + /** + * Returns the `FSTQueryData` for an active target (i.e., a target that the + * user is still interested in that has no outstanding target change + * requests). + */ + FSTQueryData* QueryDataForActiveTarget(model::TargetId target_id) const; + + /** + * Resets the state of a Watch target to its initial state (e.g. sets + * 'current' to false, clears the resume token and removes its target mapping + * from all documents). + */ + void ResetTarget(model::TargetId target_id); + + /** Returns whether the local store considers the document to be part of the + * specified target. */ + bool TargetContainsDocument(model::TargetId target_id, + const model::DocumentKey& key); + + /** The internal state of all tracked targets. */ + std::unordered_map target_states_; + + /** Keeps track of the documents to update since the last raised snapshot. */ + std::unordered_map + pending_document_updates_; + + /** A mapping of document keys to their set of target IDs. */ + std::unordered_map, + model::DocumentKeyHash> + pending_document_target_mappings_; + + /** + * A list of targets with existence filter mismatches. These targets are known + * to be inconsistent and their listens needs to be re-established by + * `RemoteStore`. + */ + std::unordered_set pending_target_resets_; + + id target_metadata_provider_; +}; + +} // namespace remote +} // namespace firestore +} // namespace firebase + +NS_ASSUME_NONNULL_END + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_REMOTE_REMOTE_EVENT_H_ diff --git a/Firestore/core/src/firebase/firestore/remote/remote_event.mm b/Firestore/core/src/firebase/firestore/remote/remote_event.mm new file mode 100644 index 00000000000..d9203f532c8 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/remote/remote_event.mm @@ -0,0 +1,415 @@ +/* + * Copyright 2019 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. + */ + +#include "Firestore/core/src/firebase/firestore/remote/remote_event.h" + +#include + +#import "Firestore/Source/Core/FSTQuery.h" +#import "Firestore/Source/Local/FSTQueryData.h" +#import "Firestore/Source/Model/FSTDocument.h" +#import "Firestore/Source/Remote/FSTRemoteEvent.h" + +using firebase::firestore::core::DocumentViewChangeType; +using firebase::firestore::model::DocumentKey; +using firebase::firestore::model::DocumentKeySet; +using firebase::firestore::model::SnapshotVersion; +using firebase::firestore::model::TargetId; + +namespace firebase { +namespace firestore { +namespace remote { + +// TargetState + +TargetState::TargetState() : resume_token_{[NSData data]} { +} + +void TargetState::UpdateResumeToken(NSData* resume_token) { + if (resume_token.length > 0) { + has_pending_changes_ = true; + resume_token_ = [resume_token copy]; + } +} + +FSTTargetChange* TargetState::ToTargetChange() const { + DocumentKeySet added_documents; + DocumentKeySet modified_documents; + DocumentKeySet removed_documents; + + for (const auto& entry : document_changes_) { + const DocumentKey& document_key = entry.first; + DocumentViewChangeType change_type = entry.second; + + switch (change_type) { + case DocumentViewChangeType::kAdded: + added_documents = added_documents.insert(document_key); + break; + case DocumentViewChangeType::kModified: + modified_documents = modified_documents.insert(document_key); + break; + case DocumentViewChangeType::kRemoved: + removed_documents = removed_documents.insert(document_key); + break; + default: + HARD_FAIL("Encountered invalid change type: %s", change_type); + } + } + + return [[FSTTargetChange alloc] + initWithResumeToken:resume_token() + current:Current() + addedDocuments:std::move(added_documents) + modifiedDocuments:std::move(modified_documents) + removedDocuments:std::move(removed_documents)]; +} + +void TargetState::ClearPendingChanges() { + has_pending_changes_ = false; + document_changes_.clear(); +} + +void TargetState::RecordPendingTargetRequest() { + ++outstanding_responses_; +} + +void TargetState::RecordTargetResponse() { + --outstanding_responses_; +} + +void TargetState::MarkCurrent() { + has_pending_changes_ = true; + current_ = true; +} + +void TargetState::AddDocumentChange(const DocumentKey& document_key, + DocumentViewChangeType type) { + has_pending_changes_ = true; + document_changes_[document_key] = type; +} + +void TargetState::RemoveDocumentChange(const DocumentKey& document_key) { + has_pending_changes_ = true; + document_changes_.erase(document_key); +} + +// WatchChangeAggregator + +void WatchChangeAggregator::HandleDocumentChange( + const DocumentWatchChange& document_change) { + for (TargetId target_id : document_change.updated_target_ids()) { + if ([document_change.new_document() isKindOfClass:[FSTDocument class]]) { + AddDocumentToTarget(target_id, document_change.new_document()); + } else if ([document_change.new_document() + isKindOfClass:[FSTDeletedDocument class]]) { + RemoveDocumentFromTarget(target_id, document_change.document_key(), + document_change.new_document()); + } + } + + for (TargetId target_id : document_change.removed_target_ids()) { + RemoveDocumentFromTarget(target_id, document_change.document_key(), + document_change.new_document()); + } +} + +void WatchChangeAggregator::HandleTargetChange( + const WatchTargetChange& target_change) { + for (TargetId target_id : GetTargetIds(target_change)) { + TargetState& target_state = EnsureTargetState(target_id); + + switch (target_change.state()) { + case WatchTargetChangeState::NoChange: + if (IsActiveTarget(target_id)) { + target_state.UpdateResumeToken(target_change.resume_token()); + } + continue; + case WatchTargetChangeState::Added: + // We need to decrement the number of pending acks needed from watch for + // this target_id. + target_state.RecordTargetResponse(); + if (!target_state.IsPending()) { + // We have a freshly added target, so we need to reset any state that + // we had previously. This can happen e.g. when remove and add back a + // target for existence filter mismatches. + target_state.ClearPendingChanges(); + } + target_state.UpdateResumeToken(target_change.resume_token()); + continue; + case WatchTargetChangeState::Removed: + // We need to keep track of removed targets to we can post-filter and + // remove any target changes. + // We need to decrement the number of pending acks needed from watch for + // this targetId. + target_state.RecordTargetResponse(); + if (!target_state.IsPending()) { + RemoveTarget(target_id); + } + HARD_ASSERT(target_change.cause().ok(), + "WatchChangeAggregator does not handle errored targets"); + continue; + case WatchTargetChangeState::Current: + if (IsActiveTarget(target_id)) { + target_state.MarkCurrent(); + target_state.UpdateResumeToken(target_change.resume_token()); + } + continue; + case WatchTargetChangeState::Reset: + if (IsActiveTarget(target_id)) { + // Reset the target and synthesizes removes for all existing + // documents. The backend will re-add any documents that still match + // the target before it sends the next global snapshot. + ResetTarget(target_id); + target_state.UpdateResumeToken(target_change.resume_token()); + } + continue; + } + HARD_FAIL("Unknown target watch change state: %s", target_change.state()); + } +} + +std::vector WatchChangeAggregator::GetTargetIds( + const WatchTargetChange& target_change) const { + if (!target_change.target_ids().empty()) { + return target_change.target_ids(); + } + + std::vector result; + result.reserve(target_states_.size()); + for (const auto& entry : target_states_) { + result.push_back(entry.first); + } + + return result; +} + +void WatchChangeAggregator::HandleExistenceFilter( + const ExistenceFilterWatchChange& existence_filter) { + TargetId target_id = existence_filter.target_id(); + int expected_count = existence_filter.filter().count(); + + FSTQueryData* query_data = QueryDataForActiveTarget(target_id); + if (query_data) { + FSTQuery* query = query_data.query; + if ([query isDocumentQuery]) { + if (expected_count == 0) { + // The existence filter told us the document does not exist. We deduce + // that this document does not exist and apply a deleted document to our + // updates. Without applying this deleted document there might be + // another query that will raise this document as part of a snapshot + // until it is resolved, essentially exposing inconsistency between + // queries. + DocumentKey key{query.path}; + RemoveDocumentFromTarget( + target_id, key, + [FSTDeletedDocument documentWithKey:key + version:SnapshotVersion::None() + hasCommittedMutations:NO]); + } else { + HARD_ASSERT(expected_count == 1, + "Single document existence filter with count: %s", + expected_count); + } + } else { + int current_size = GetCurrentDocumentCountForTarget(target_id); + if (current_size != expected_count) { + // Existence filter mismatch: We reset the mapping and raise a new + // snapshot with `isFromCache:true`. + ResetTarget(target_id); + pending_target_resets_.insert(target_id); + } + } + } +} + +FSTRemoteEvent* WatchChangeAggregator::CreateRemoteEvent( + const SnapshotVersion& snapshot_version) { + std::unordered_map target_changes; + + for (auto& entry : target_states_) { + TargetId target_id = entry.first; + TargetState& target_state = entry.second; + + FSTQueryData* query_data = QueryDataForActiveTarget(target_id); + if (query_data) { + if (target_state.Current() && [query_data.query isDocumentQuery]) { + // Document queries for document that don't exist can produce an empty + // result set. To update our local cache, we synthesize a document + // delete if we have not previously received the document. This resolves + // the limbo state of the document, removing it from limboDocumentRefs. + DocumentKey key{query_data.query.path}; + if (pending_document_updates_.find(key) == + pending_document_updates_.end() && + !TargetContainsDocument(target_id, key)) { + RemoveDocumentFromTarget( + target_id, key, + [FSTDeletedDocument documentWithKey:key + version:snapshot_version + hasCommittedMutations:NO]); + } + } + + if (target_state.HasPendingChanges()) { + target_changes[target_id] = target_state.ToTargetChange(); + target_state.ClearPendingChanges(); + } + } + } + + DocumentKeySet resolved_limbo_documents; + + // We extract the set of limbo-only document updates as the GC logic + // special-cases documents that do not appear in the query cache. + // + // TODO(gsoltis): Expand on this comment. + for (const auto& entry : pending_document_target_mappings_) { + bool is_only_limbo_target = true; + + for (TargetId target_id : entry.second) { + FSTQueryData* query_data = QueryDataForActiveTarget(target_id); + if (query_data && query_data.purpose != FSTQueryPurposeLimboResolution) { + is_only_limbo_target = false; + break; + } + } + + if (is_only_limbo_target) { + resolved_limbo_documents = resolved_limbo_documents.insert(entry.first); + } + } + + FSTRemoteEvent* remote_event = + [[FSTRemoteEvent alloc] initWithSnapshotVersion:snapshot_version + targetChanges:target_changes + targetMismatches:pending_target_resets_ + documentUpdates:pending_document_updates_ + limboDocuments:resolved_limbo_documents]; + + // Re-initialize the current state to ensure that we do not modify the + // generated `RemoteEvent`. + pending_document_updates_.clear(); + pending_document_target_mappings_.clear(); + pending_target_resets_.clear(); + + return remote_event; +} + +void WatchChangeAggregator::AddDocumentToTarget(TargetId target_id, + FSTMaybeDocument* document) { + if (!IsActiveTarget(target_id)) { + return; + } + + DocumentViewChangeType change_type = + TargetContainsDocument(target_id, document.key) + ? DocumentViewChangeType::kModified + : DocumentViewChangeType::kAdded; + + TargetState& target_state = EnsureTargetState(target_id); + target_state.AddDocumentChange(document.key, change_type); + + pending_document_updates_[document.key] = document; + pending_document_target_mappings_[document.key].insert(target_id); +} + +void WatchChangeAggregator::RemoveDocumentFromTarget( + TargetId target_id, + const DocumentKey& key, + FSTMaybeDocument* _Nullable updated_document) { + if (!IsActiveTarget(target_id)) { + return; + } + + TargetState& target_state = EnsureTargetState(target_id); + if (TargetContainsDocument(target_id, key)) { + target_state.AddDocumentChange(key, DocumentViewChangeType::kRemoved); + } else { + // The document may have entered and left the target before we raised a + // snapshot, so we can just ignore the change. + target_state.RemoveDocumentChange(key); + } + pending_document_target_mappings_[key].insert(target_id); + + if (updated_document) { + pending_document_updates_[key] = updated_document; + } +} + +void WatchChangeAggregator::RemoveTarget(TargetId target_id) { + target_states_.erase(target_id); +} + +int WatchChangeAggregator::GetCurrentDocumentCountForTarget( + TargetId target_id) { + TargetState& target_state = EnsureTargetState(target_id); + FSTTargetChange* target_change = target_state.ToTargetChange(); + return ([target_metadata_provider_ remoteKeysForTarget:target_id].size() + + target_change.addedDocuments.size() - + target_change.removedDocuments.size()); +} + +void WatchChangeAggregator::RecordPendingTargetRequest(TargetId target_id) { + // For each request we get we need to record we need a response for it. + TargetState& target_state = EnsureTargetState(target_id); + target_state.RecordPendingTargetRequest(); +} + +TargetState& WatchChangeAggregator::EnsureTargetState(TargetId target_id) { + return target_states_[target_id]; +} + +bool WatchChangeAggregator::IsActiveTarget(TargetId target_id) const { + return QueryDataForActiveTarget(target_id) != nil; +} + +FSTQueryData* WatchChangeAggregator::QueryDataForActiveTarget( + TargetId target_id) const { + auto target_state = target_states_.find(target_id); + return target_state != target_states_.end() && + target_state->second.IsPending() + ? nil + : [target_metadata_provider_ queryDataForTarget:target_id]; +} + +void WatchChangeAggregator::ResetTarget(TargetId target_id) { + auto current_target_state = target_states_.find(target_id); + HARD_ASSERT(current_target_state != target_states_.end() && + !(current_target_state->second.IsPending()), + "Should only reset active targets"); + + target_states_[target_id] = {}; + + // Trigger removal for any documents currently mapped to this target. These + // removals will be part of the initial snapshot if Watch does not resend + // these documents. + DocumentKeySet existingKeys = + [target_metadata_provider_ remoteKeysForTarget:target_id]; + + for (const DocumentKey& key : existingKeys) { + RemoveDocumentFromTarget(target_id, key, nil); + } +} + +bool WatchChangeAggregator::TargetContainsDocument(TargetId target_id, + const DocumentKey& key) { + const DocumentKeySet& existing_keys = + [target_metadata_provider_ remoteKeysForTarget:target_id]; + return existing_keys.contains(key); +} + +} // namespace remote +} // namespace firestore +} // namespace firebase From d8e370436084e6f44c5e6589794be657d1101c35 Mon Sep 17 00:00:00 2001 From: Greg Soltis Date: Mon, 28 Jan 2019 10:21:20 -0800 Subject: [PATCH 03/27] Add MutationQueue interface (#2317) * Implement mutation queue interface and port tests --- .../Local/FSTLevelDBMutationQueueTests.mm | 6 +- .../Local/FSTMemoryMutationQueueTests.mm | 2 +- .../Tests/Local/FSTMutationQueueTests.h | 9 +- .../Tests/Local/FSTMutationQueueTests.mm | 218 +++++++++--------- .../Source/Local/FSTLevelDBMutationQueue.h | 3 + .../Source/Local/FSTLevelDBMutationQueue.mm | 4 + .../Source/Local/FSTMemoryMutationQueue.mm | 10 + Firestore/Source/Local/FSTMutationQueue.h | 4 + .../firestore/local/leveldb_mutation_queue.h | 32 +-- .../firestore/local/memory_mutation_queue.h | 32 +-- .../firebase/firestore/local/mutation_queue.h | 162 +++++++++++++ 11 files changed, 336 insertions(+), 146 deletions(-) create mode 100644 Firestore/core/src/firebase/firestore/local/mutation_queue.h diff --git a/Firestore/Example/Tests/Local/FSTLevelDBMutationQueueTests.mm b/Firestore/Example/Tests/Local/FSTLevelDBMutationQueueTests.mm index a53c3c20c09..917f163c179 100644 --- a/Firestore/Example/Tests/Local/FSTLevelDBMutationQueueTests.mm +++ b/Firestore/Example/Tests/Local/FSTLevelDBMutationQueueTests.mm @@ -38,6 +38,7 @@ using firebase::firestore::auth::User; using firebase::firestore::local::LevelDbMutationKey; +using firebase::firestore::local::LevelDbMutationQueue; using firebase::firestore::local::LoadNextBatchIdFromDb; using firebase::firestore::local::ReferenceSet; using firebase::firestore::model::BatchId; @@ -79,10 +80,11 @@ - (void)setUp { [super setUp]; _db = [FSTPersistenceTestHelpers levelDBPersistence]; [_db.referenceDelegate addInMemoryPins:&_additionalReferences]; - self.mutationQueue = [_db mutationQueueForUser:User("user")]; + + self.mutationQueue = [_db mutationQueueForUser:User("user")].mutationQueue; self.persistence = _db; - self.persistence.run("Setup", [&]() { [self.mutationQueue start]; }); + self.persistence.run("Setup", [&]() { self.mutationQueue->Start(); }); } - (void)testLoadNextBatchID_zeroWhenTotallyEmpty { diff --git a/Firestore/Example/Tests/Local/FSTMemoryMutationQueueTests.mm b/Firestore/Example/Tests/Local/FSTMemoryMutationQueueTests.mm index 97b94afc20d..bb2ff6c4018 100644 --- a/Firestore/Example/Tests/Local/FSTMemoryMutationQueueTests.mm +++ b/Firestore/Example/Tests/Local/FSTMemoryMutationQueueTests.mm @@ -43,7 +43,7 @@ - (void)setUp { self.persistence = [FSTPersistenceTestHelpers eagerGCMemoryPersistence]; [self.persistence.referenceDelegate addInMemoryPins:&_additionalReferences]; - self.mutationQueue = [self.persistence mutationQueueForUser:User("user")]; + self.mutationQueue = [self.persistence mutationQueueForUser:User("user")].mutationQueue; } @end diff --git a/Firestore/Example/Tests/Local/FSTMutationQueueTests.h b/Firestore/Example/Tests/Local/FSTMutationQueueTests.h index 0193c36ba1e..78e9f8b7c4f 100644 --- a/Firestore/Example/Tests/Local/FSTMutationQueueTests.h +++ b/Firestore/Example/Tests/Local/FSTMutationQueueTests.h @@ -16,22 +16,23 @@ #import -@protocol FSTMutationQueue; +#include "Firestore/core/src/firebase/firestore/local/mutation_queue.h" + @protocol FSTPersistence; NS_ASSUME_NONNULL_BEGIN /** - * These are tests for any implementation of the FSTMutationQueue protocol. + * These are tests for any implementation of the MutationQueue interface. * - * To test a specific implementation of FSTMutationQueue: + * To test a specific implementation of MutationQueue: * * + Subclass FSTMutationQueueTests * + override -setUp, assigning to mutationQueue and persistence * + override -tearDown, cleaning up mutationQueue and persistence */ @interface FSTMutationQueueTests : XCTestCase -@property(nonatomic, strong, nullable) id mutationQueue; +@property(nonatomic, nullable) firebase::firestore::local::MutationQueue *mutationQueue; @property(nonatomic, strong, nullable) id persistence; @end diff --git a/Firestore/Example/Tests/Local/FSTMutationQueueTests.mm b/Firestore/Example/Tests/Local/FSTMutationQueueTests.mm index a066c289d9f..c975bf70cd8 100644 --- a/Firestore/Example/Tests/Local/FSTMutationQueueTests.mm +++ b/Firestore/Example/Tests/Local/FSTMutationQueueTests.mm @@ -19,9 +19,9 @@ #import #include +#include #import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Local/FSTMutationQueue.h" #import "Firestore/Source/Local/FSTPersistence.h" #import "Firestore/Source/Model/FSTMutation.h" #import "Firestore/Source/Model/FSTMutationBatch.h" @@ -50,6 +50,14 @@ - (void)tearDown { [super tearDown]; } +- (void)assertVector:(const std::vector &)actual + matchesExpected:(const std::vector &)expected { + XCTAssertEqual(actual.size(), expected.size(), @"Vector length mismatch"); + for (int i = 0; i < expected.size(); i++) { + XCTAssertEqualObjects(actual[i], expected[i]); + } +} + /** * Xcode will run tests from any class that extends XCTestCase, but this doesn't work for * FSTMutationQueueTests since it is incomplete without the implementations supplied by its @@ -64,21 +72,21 @@ - (void)testCountBatches { self.persistence.run("testCountBatches", [&]() { XCTAssertEqual(0, [self batchCount]); - XCTAssertTrue([self.mutationQueue isEmpty]); + XCTAssertTrue(self.mutationQueue->IsEmpty()); FSTMutationBatch *batch1 = [self addMutationBatch]; XCTAssertEqual(1, [self batchCount]); - XCTAssertFalse([self.mutationQueue isEmpty]); + XCTAssertFalse(self.mutationQueue->IsEmpty()); FSTMutationBatch *batch2 = [self addMutationBatch]; XCTAssertEqual(2, [self batchCount]); - [self.mutationQueue removeMutationBatch:batch1]; + self.mutationQueue->RemoveMutationBatch(batch1); XCTAssertEqual(1, [self batchCount]); - [self.mutationQueue removeMutationBatch:batch2]; + self.mutationQueue->RemoveMutationBatch(batch2); XCTAssertEqual(0, [self batchCount]); - XCTAssertTrue([self.mutationQueue isEmpty]); + XCTAssertTrue(self.mutationQueue->IsEmpty()); }); } @@ -97,17 +105,17 @@ - (void)testAcknowledgeBatchID { XCTAssertEqual([self batchCount], 3); - [self.mutationQueue acknowledgeBatch:batch1 streamToken:nil]; - [self.mutationQueue removeMutationBatch:batch1]; + self.mutationQueue->AcknowledgeBatch(batch1, nil); + self.mutationQueue->RemoveMutationBatch(batch1); XCTAssertEqual([self batchCount], 2); - [self.mutationQueue acknowledgeBatch:batch2 streamToken:nil]; + self.mutationQueue->AcknowledgeBatch(batch2, nil); XCTAssertEqual([self batchCount], 2); - [self.mutationQueue removeMutationBatch:batch2]; + self.mutationQueue->RemoveMutationBatch(batch2); XCTAssertEqual([self batchCount], 1); - [self.mutationQueue removeMutationBatch:batch3]; + self.mutationQueue->RemoveMutationBatch(batch3); XCTAssertEqual([self batchCount], 0); }); } @@ -118,8 +126,8 @@ - (void)testAcknowledgeThenRemove { self.persistence.run("testAcknowledgeThenRemove", [&]() { FSTMutationBatch *batch1 = [self addMutationBatch]; - [self.mutationQueue acknowledgeBatch:batch1 streamToken:nil]; - [self.mutationQueue removeMutationBatch:batch1]; + self.mutationQueue->AcknowledgeBatch(batch1, nil); + self.mutationQueue->RemoveMutationBatch(batch1); XCTAssertEqual([self batchCount], 0); }); @@ -130,26 +138,26 @@ - (void)testLookupMutationBatch { // Searching on an empty queue should not find a non-existent batch self.persistence.run("testLookupMutationBatch", [&]() { - FSTMutationBatch *notFound = [self.mutationQueue lookupMutationBatch:42]; + FSTMutationBatch *notFound = self.mutationQueue->LookupMutationBatch(42); XCTAssertNil(notFound); - NSMutableArray *batches = [self createBatches:10]; - NSArray *removed = [self removeFirstBatches:3 inBatches:batches]; + std::vector batches = [self createBatches:10]; + std::vector removed = [self removeFirstBatches:3 inBatches:&batches]; // After removing, a batch should not be found - for (NSUInteger i = 0; i < removed.count; i++) { - notFound = [self.mutationQueue lookupMutationBatch:removed[i].batchID]; + for (size_t i = 0; i < removed.size(); i++) { + notFound = self.mutationQueue->LookupMutationBatch(removed[i].batchID); XCTAssertNil(notFound); } // Remaining entries should still be found - for (FSTMutationBatch *batch in batches) { - FSTMutationBatch *found = [self.mutationQueue lookupMutationBatch:batch.batchID]; + for (FSTMutationBatch *batch : batches) { + FSTMutationBatch *found = self.mutationQueue->LookupMutationBatch(batch.batchID); XCTAssertEqual(found.batchID, batch.batchID); } // Even on a nonempty queue searching should not find a non-existent batch - notFound = [self.mutationQueue lookupMutationBatch:42]; + notFound = self.mutationQueue->LookupMutationBatch(42); XCTAssertNil(notFound); }); } @@ -158,29 +166,29 @@ - (void)testNextMutationBatchAfterBatchID { if ([self isTestBaseClass]) return; self.persistence.run("testNextMutationBatchAfterBatchID", [&]() { - NSMutableArray *batches = [self createBatches:10]; - NSArray *removed = [self removeFirstBatches:3 inBatches:batches]; + std::vector batches = [self createBatches:10]; + std::vector removed = [self removeFirstBatches:3 inBatches:&batches]; - for (NSUInteger i = 0; i < batches.count - 1; i++) { + for (size_t i = 0; i < batches.size() - 1; i++) { FSTMutationBatch *current = batches[i]; FSTMutationBatch *next = batches[i + 1]; - FSTMutationBatch *found = [self.mutationQueue nextMutationBatchAfterBatchID:current.batchID]; + FSTMutationBatch *found = self.mutationQueue->NextMutationBatchAfterBatchId(current.batchID); XCTAssertEqual(found.batchID, next.batchID); } - for (NSUInteger i = 0; i < removed.count; i++) { + for (size_t i = 0; i < removed.size(); i++) { FSTMutationBatch *current = removed[i]; FSTMutationBatch *next = batches[0]; - FSTMutationBatch *found = [self.mutationQueue nextMutationBatchAfterBatchID:current.batchID]; + FSTMutationBatch *found = self.mutationQueue->NextMutationBatchAfterBatchId(current.batchID); XCTAssertEqual(found.batchID, next.batchID); } FSTMutationBatch *first = batches[0]; - FSTMutationBatch *found = [self.mutationQueue nextMutationBatchAfterBatchID:first.batchID - 42]; + FSTMutationBatch *found = self.mutationQueue->NextMutationBatchAfterBatchId(first.batchID - 42); XCTAssertEqual(found.batchID, first.batchID); - FSTMutationBatch *last = batches[batches.count - 1]; - FSTMutationBatch *notFound = [self.mutationQueue nextMutationBatchAfterBatchID:last.batchID]; + FSTMutationBatch *last = batches[batches.size() - 1]; + FSTMutationBatch *notFound = self.mutationQueue->NextMutationBatchAfterBatchId(last.batchID); XCTAssertNil(notFound); }); } @@ -200,16 +208,15 @@ - (void)testAllMutationBatchesAffectingDocumentKey { NSMutableArray *batches = [NSMutableArray array]; for (FSTMutation *mutation in mutations) { FSTMutationBatch *batch = - [self.mutationQueue addMutationBatchWithWriteTime:[FIRTimestamp timestamp] - mutations:@[ mutation ]]; + self.mutationQueue->AddMutationBatch([FIRTimestamp timestamp], @[ mutation ]); [batches addObject:batch]; } - NSArray *expected = @[ batches[1], batches[2] ]; - NSArray *matches = - [self.mutationQueue allMutationBatchesAffectingDocumentKey:testutil::Key("foo/bar")]; + std::vector expected{batches[1], batches[2]}; + std::vector matches = + self.mutationQueue->AllMutationBatchesAffectingDocumentKey(testutil::Key("foo/bar")); - XCTAssertEqualObjects(matches, expected); + [self assertVector:matches matchesExpected:expected]; }); } @@ -228,8 +235,7 @@ - (void)testAllMutationBatchesAffectingDocumentKeys { NSMutableArray *batches = [NSMutableArray array]; for (FSTMutation *mutation in mutations) { FSTMutationBatch *batch = - [self.mutationQueue addMutationBatchWithWriteTime:[FIRTimestamp timestamp] - mutations:@[ mutation ]]; + self.mutationQueue->AddMutationBatch([FIRTimestamp timestamp], @[ mutation ]); [batches addObject:batch]; } @@ -238,11 +244,11 @@ - (void)testAllMutationBatchesAffectingDocumentKeys { Key("foo/baz"), }; - NSArray *expected = @[ batches[1], batches[2], batches[4] ]; - NSArray *matches = - [self.mutationQueue allMutationBatchesAffectingDocumentKeys:keys]; + std::vector expected{batches[1], batches[2], batches[4]}; + std::vector matches = + self.mutationQueue->AllMutationBatchesAffectingDocumentKeys(keys); - XCTAssertEqualObjects(matches, expected); + [self assertVector:matches matchesExpected:expected]; }); } @@ -255,29 +261,27 @@ - (void)testAllMutationBatchesAffectingDocumentKeys_handlesOverlap { FSTTestSetMutation(@"foo/baz", @{@"a" : @1}), ]; FSTMutationBatch *batch1 = - [self.mutationQueue addMutationBatchWithWriteTime:[FIRTimestamp timestamp] - mutations:group1]; + self.mutationQueue->AddMutationBatch([FIRTimestamp timestamp], group1); NSArray *group2 = @[ FSTTestSetMutation(@"food/bar", @{@"a" : @1}) ]; - [self.mutationQueue addMutationBatchWithWriteTime:[FIRTimestamp timestamp] mutations:group2]; + self.mutationQueue->AddMutationBatch([FIRTimestamp timestamp], group2); NSArray *group3 = @[ FSTTestSetMutation(@"foo/bar", @{@"b" : @1}), ]; FSTMutationBatch *batch3 = - [self.mutationQueue addMutationBatchWithWriteTime:[FIRTimestamp timestamp] - mutations:group3]; + self.mutationQueue->AddMutationBatch([FIRTimestamp timestamp], group3); DocumentKeySet keys{ Key("foo/bar"), Key("foo/baz"), }; - NSArray *expected = @[ batch1, batch3 ]; - NSArray *matches = - [self.mutationQueue allMutationBatchesAffectingDocumentKeys:keys]; + std::vector expected{batch1, batch3}; + std::vector matches = + self.mutationQueue->AllMutationBatchesAffectingDocumentKeys(keys); - XCTAssertEqualObjects(matches, expected); + [self assertVector:matches matchesExpected:expected]; }); } @@ -296,17 +300,16 @@ - (void)testAllMutationBatchesAffectingQuery { NSMutableArray *batches = [NSMutableArray array]; for (FSTMutation *mutation in mutations) { FSTMutationBatch *batch = - [self.mutationQueue addMutationBatchWithWriteTime:[FIRTimestamp timestamp] - mutations:@[ mutation ]]; + self.mutationQueue->AddMutationBatch([FIRTimestamp timestamp], @[ mutation ]); [batches addObject:batch]; } - NSArray *expected = @[ batches[1], batches[2], batches[4] ]; + std::vector expected = {batches[1], batches[2], batches[4]}; FSTQuery *query = FSTTestQuery("foo"); - NSArray *matches = - [self.mutationQueue allMutationBatchesAffectingQuery:query]; + std::vector matches = + self.mutationQueue->AllMutationBatchesAffectingQuery(query); - XCTAssertEqualObjects(matches, expected); + [self assertVector:matches matchesExpected:expected]; }); } @@ -314,57 +317,56 @@ - (void)testRemoveMutationBatches { if ([self isTestBaseClass]) return; self.persistence.run("testRemoveMutationBatches", [&]() { - NSMutableArray *batches = [self createBatches:10]; + std::vector batches = [self createBatches:10]; - [self.mutationQueue removeMutationBatch:batches[0]]; - [batches removeObjectAtIndex:0]; + self.mutationQueue->RemoveMutationBatch(batches[0]); + batches.erase(batches.begin()); XCTAssertEqual([self batchCount], 9); - NSArray *found; + std::vector found; - found = [self.mutationQueue allMutationBatches]; - XCTAssertEqualObjects(found, batches); - XCTAssertEqual(found.count, 9); + found = self.mutationQueue->AllMutationBatches(); + [self assertVector:found matchesExpected:batches]; + XCTAssertEqual(found.size(), 9); - [self.mutationQueue removeMutationBatch:batches[0]]; - [self.mutationQueue removeMutationBatch:batches[1]]; - [self.mutationQueue removeMutationBatch:batches[2]]; - [batches removeObjectsInRange:NSMakeRange(0, 3)]; + self.mutationQueue->RemoveMutationBatch(batches[0]); + self.mutationQueue->RemoveMutationBatch(batches[1]); + self.mutationQueue->RemoveMutationBatch(batches[2]); + batches.erase(batches.begin(), batches.begin() + 3); XCTAssertEqual([self batchCount], 6); - found = [self.mutationQueue allMutationBatches]; - XCTAssertEqualObjects(found, batches); - XCTAssertEqual(found.count, 6); + found = self.mutationQueue->AllMutationBatches(); + [self assertVector:found matchesExpected:batches]; + XCTAssertEqual(found.size(), 6); - [self.mutationQueue removeMutationBatch:batches[0]]; - [batches removeObjectAtIndex:0]; + self.mutationQueue->RemoveMutationBatch(batches[0]); + batches.erase(batches.begin()); XCTAssertEqual([self batchCount], 5); - found = [self.mutationQueue allMutationBatches]; - XCTAssertEqualObjects(found, batches); - XCTAssertEqual(found.count, 5); + found = self.mutationQueue->AllMutationBatches(); + [self assertVector:found matchesExpected:batches]; + XCTAssertEqual(found.size(), 5); - [self.mutationQueue removeMutationBatch:batches[0]]; - [batches removeObjectAtIndex:0]; + self.mutationQueue->RemoveMutationBatch(batches[0]); + batches.erase(batches.begin()); XCTAssertEqual([self batchCount], 4); - [self.mutationQueue removeMutationBatch:batches[0]]; - [batches removeObjectAtIndex:0]; + self.mutationQueue->RemoveMutationBatch(batches[0]); + batches.erase(batches.begin()); XCTAssertEqual([self batchCount], 3); - found = [self.mutationQueue allMutationBatches]; - XCTAssertEqualObjects(found, batches); - XCTAssertEqual(found.count, 3); - XCTAssertFalse([self.mutationQueue isEmpty]); + found = self.mutationQueue->AllMutationBatches(); + [self assertVector:found matchesExpected:batches]; + XCTAssertEqual(found.size(), 3); + XCTAssertFalse(self.mutationQueue->IsEmpty()); - for (FSTMutationBatch *batch in batches) { - [self.mutationQueue removeMutationBatch:batch]; + for (FSTMutationBatch *batch : batches) { + self.mutationQueue->RemoveMutationBatch(batch); } - found = [self.mutationQueue allMutationBatches]; - XCTAssertEqualObjects(found, @[]); - XCTAssertEqual(found.count, 0); - XCTAssertTrue([self.mutationQueue isEmpty]); + found = self.mutationQueue->AllMutationBatches(); + XCTAssertEqual(found.size(), 0); + XCTAssertTrue(self.mutationQueue->IsEmpty()); }); } @@ -375,15 +377,15 @@ - (void)testStreamToken { NSData *streamToken2 = [@"token2" dataUsingEncoding:NSUTF8StringEncoding]; self.persistence.run("testStreamToken", [&]() { - [self.mutationQueue setLastStreamToken:streamToken1]; + self.mutationQueue->SetLastStreamToken(streamToken1); FSTMutationBatch *batch1 = [self addMutationBatch]; [self addMutationBatch]; - XCTAssertEqualObjects([self.mutationQueue lastStreamToken], streamToken1); + XCTAssertEqualObjects(self.mutationQueue->GetLastStreamToken(), streamToken1); - [self.mutationQueue acknowledgeBatch:batch1 streamToken:streamToken2]; - XCTAssertEqualObjects([self.mutationQueue lastStreamToken], streamToken2); + self.mutationQueue->AcknowledgeBatch(batch1, streamToken2); + XCTAssertEqualObjects(self.mutationQueue->GetLastStreamToken(), streamToken2); }); } @@ -402,8 +404,7 @@ - (FSTMutationBatch *)addMutationBatchWithKey:(NSString *)key { FSTSetMutation *mutation = FSTTestSetMutation(key, @{@"a" : @1}); FSTMutationBatch *batch = - [self.mutationQueue addMutationBatchWithWriteTime:[FIRTimestamp timestamp] - mutations:@[ mutation ]]; + self.mutationQueue->AddMutationBatch([FIRTimestamp timestamp], @[ mutation ]); return batch; } @@ -411,20 +412,20 @@ - (FSTMutationBatch *)addMutationBatchWithKey:(NSString *)key { * Creates an array of batches containing @a number dummy FSTMutationBatches. Each has a different * batchID. */ -- (NSMutableArray *)createBatches:(int)number { - NSMutableArray *batches = [NSMutableArray array]; +- (std::vector)createBatches:(int)number { + std::vector batches; for (int i = 0; i < number; i++) { FSTMutationBatch *batch = [self addMutationBatch]; - [batches addObject:batch]; + batches.push_back(batch); } return batches; } /** Returns the number of mutation batches in the mutation queue. */ -- (NSUInteger)batchCount { - return [self.mutationQueue allMutationBatches].count; +- (size_t)batchCount { + return self.mutationQueue->AllMutationBatches().size(); } /** @@ -434,15 +435,14 @@ - (NSUInteger)batchCount { * @param batches The array to mutate, removing entries from it. * @return A new array containing all the entries that were removed from @a batches. */ -- (NSArray *)removeFirstBatches:(NSUInteger)n - inBatches:(NSMutableArray *)batches { - NSArray *removed = [batches subarrayWithRange:NSMakeRange(0, n)]; - [batches removeObjectsInRange:NSMakeRange(0, n)]; - - [removed enumerateObjectsUsingBlock:^(FSTMutationBatch *batch, NSUInteger idx, BOOL *stop) { - [self.mutationQueue removeMutationBatch:batch]; - }]; +- (std::vector)removeFirstBatches:(size_t)n + inBatches:(std::vector *)batches { + std::vector removed(batches->begin(), batches->begin() + n); + batches->erase(batches->begin(), batches->begin() + n); + for (FSTMutationBatch *batch : removed) { + self.mutationQueue->RemoveMutationBatch(batch); + } return removed; } diff --git a/Firestore/Source/Local/FSTLevelDBMutationQueue.h b/Firestore/Source/Local/FSTLevelDBMutationQueue.h index 20ff77d21f8..72fde7d1ba8 100644 --- a/Firestore/Source/Local/FSTLevelDBMutationQueue.h +++ b/Firestore/Source/Local/FSTLevelDBMutationQueue.h @@ -21,6 +21,7 @@ #import "Firestore/Source/Local/FSTMutationQueue.h" #include "Firestore/core/src/firebase/firestore/auth/user.h" +#include "Firestore/core/src/firebase/firestore/local/leveldb_mutation_queue.h" #include "Firestore/core/src/firebase/firestore/model/types.h" #include "leveldb/db.h" @@ -44,6 +45,8 @@ NS_ASSUME_NONNULL_BEGIN db:(FSTLevelDB *)db serializer:(FSTLocalSerializer *)serializer; +- (firebase::firestore::local::LevelDbMutationQueue *)mutationQueue; + @end NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLevelDBMutationQueue.mm b/Firestore/Source/Local/FSTLevelDBMutationQueue.mm index 951fe7874be..7753db22f12 100644 --- a/Firestore/Source/Local/FSTLevelDBMutationQueue.mm +++ b/Firestore/Source/Local/FSTLevelDBMutationQueue.mm @@ -172,6 +172,10 @@ - (void)performConsistencyCheck { _delegate->PerformConsistencyCheck(); } +- (LevelDbMutationQueue *)mutationQueue { + return _delegate.get(); +} + @end NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTMemoryMutationQueue.mm b/Firestore/Source/Local/FSTMemoryMutationQueue.mm index 8a1c6221cae..a8a50732fa3 100644 --- a/Firestore/Source/Local/FSTMemoryMutationQueue.mm +++ b/Firestore/Source/Local/FSTMemoryMutationQueue.mm @@ -53,6 +53,12 @@ return copy; } +@interface FSTMemoryMutationQueue () + +- (MemoryMutationQueue *)mutationQueue; + +@end + @implementation FSTMemoryMutationQueue { std::unique_ptr _delegate; } @@ -137,6 +143,10 @@ - (size_t)byteSizeWithSerializer:(FSTLocalSerializer *)serializer { return _delegate->CalculateByteSize(serializer); } +- (MemoryMutationQueue *)mutationQueue { + return _delegate.get(); +} + @end NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTMutationQueue.h b/Firestore/Source/Local/FSTMutationQueue.h index bf39a6fe11d..6540e745b20 100644 --- a/Firestore/Source/Local/FSTMutationQueue.h +++ b/Firestore/Source/Local/FSTMutationQueue.h @@ -16,6 +16,7 @@ #import +#include "Firestore/core/src/firebase/firestore/local/mutation_queue.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" #include "Firestore/core/src/firebase/firestore/model/document_key_set.h" #include "Firestore/core/src/firebase/firestore/model/types.h" @@ -124,6 +125,9 @@ NS_ASSUME_NONNULL_BEGIN /** Performs a consistency check, examining the mutation queue for any leaks, if possible. */ - (void)performConsistencyCheck; +// Visible for testing +- (firebase::firestore::local::MutationQueue *)mutationQueue; + @end NS_ASSUME_NONNULL_END diff --git a/Firestore/core/src/firebase/firestore/local/leveldb_mutation_queue.h b/Firestore/core/src/firebase/firestore/local/leveldb_mutation_queue.h index 90ba81d3915..ef205ef276b 100644 --- a/Firestore/core/src/firebase/firestore/local/leveldb_mutation_queue.h +++ b/Firestore/core/src/firebase/firestore/local/leveldb_mutation_queue.h @@ -31,6 +31,7 @@ #include "Firestore/core/src/firebase/firestore/auth/user.h" #include "Firestore/core/src/firebase/firestore/local/leveldb_key.h" +#include "Firestore/core/src/firebase/firestore/local/mutation_queue.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" #include "Firestore/core/src/firebase/firestore/model/document_key_set.h" #include "Firestore/core/src/firebase/firestore/model/types.h" @@ -56,45 +57,46 @@ namespace local { */ model::BatchId LoadNextBatchIdFromDb(leveldb::DB* db); -class LevelDbMutationQueue { +class LevelDbMutationQueue : public MutationQueue { public: LevelDbMutationQueue(const auth::User& user, FSTLevelDB* db, FSTLocalSerializer* serializer); - void Start(); + void Start() override; - bool IsEmpty(); + bool IsEmpty() override; void AcknowledgeBatch(FSTMutationBatch* batch, - NSData* _Nullable stream_token); + NSData* _Nullable stream_token) override; FSTMutationBatch* AddMutationBatch(FIRTimestamp* local_write_time, - NSArray* mutations); + NSArray* mutations) override; - void RemoveMutationBatch(FSTMutationBatch* batch); + void RemoveMutationBatch(FSTMutationBatch* batch) override; - std::vector AllMutationBatches(); + std::vector AllMutationBatches() override; std::vector AllMutationBatchesAffectingDocumentKeys( - const model::DocumentKeySet& document_keys); + const model::DocumentKeySet& document_keys) override; std::vector AllMutationBatchesAffectingDocumentKey( - const model::DocumentKey& key); + const model::DocumentKey& key) override; std::vector AllMutationBatchesAffectingQuery( - FSTQuery* query); + FSTQuery* query) override; - FSTMutationBatch* _Nullable LookupMutationBatch(model::BatchId batch_id); + FSTMutationBatch* _Nullable LookupMutationBatch( + model::BatchId batch_id) override; FSTMutationBatch* _Nullable NextMutationBatchAfterBatchId( - model::BatchId batch_id); + model::BatchId batch_id) override; - void PerformConsistencyCheck(); + void PerformConsistencyCheck() override; - NSData* _Nullable GetLastStreamToken(); + NSData* _Nullable GetLastStreamToken() override; - void SetLastStreamToken(NSData* _Nullable stream_token); + void SetLastStreamToken(NSData* _Nullable stream_token) override; private: /** diff --git a/Firestore/core/src/firebase/firestore/local/memory_mutation_queue.h b/Firestore/core/src/firebase/firestore/local/memory_mutation_queue.h index bdf4d769d3e..a47a2899576 100644 --- a/Firestore/core/src/firebase/firestore/local/memory_mutation_queue.h +++ b/Firestore/core/src/firebase/firestore/local/memory_mutation_queue.h @@ -30,6 +30,7 @@ #include "Firestore/core/src/firebase/firestore/immutable/sorted_set.h" #include "Firestore/core/src/firebase/firestore/local/document_reference.h" +#include "Firestore/core/src/firebase/firestore/local/mutation_queue.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" #include "Firestore/core/src/firebase/firestore/model/document_key_set.h" #include "Firestore/core/src/firebase/firestore/model/types.h" @@ -46,48 +47,49 @@ namespace firebase { namespace firestore { namespace local { -class MemoryMutationQueue { +class MemoryMutationQueue : public MutationQueue { public: explicit MemoryMutationQueue(FSTMemoryPersistence* persistence); - void Start(); + void Start() override; - bool IsEmpty(); + bool IsEmpty() override; void AcknowledgeBatch(FSTMutationBatch* batch, - NSData* _Nullable stream_token); + NSData* _Nullable stream_token) override; FSTMutationBatch* AddMutationBatch(FIRTimestamp* local_write_time, - NSArray* mutations); + NSArray* mutations) override; - void RemoveMutationBatch(FSTMutationBatch* batch); + void RemoveMutationBatch(FSTMutationBatch* batch) override; - const std::vector& AllMutationBatches() { + std::vector AllMutationBatches() override { return queue_; } std::vector AllMutationBatchesAffectingDocumentKeys( - const model::DocumentKeySet& document_keys); + const model::DocumentKeySet& document_keys) override; std::vector AllMutationBatchesAffectingDocumentKey( - const model::DocumentKey& key); + const model::DocumentKey& key) override; std::vector AllMutationBatchesAffectingQuery( - FSTQuery* query); + FSTQuery* query) override; - FSTMutationBatch* _Nullable LookupMutationBatch(model::BatchId batch_id); + FSTMutationBatch* _Nullable LookupMutationBatch( + model::BatchId batch_id) override; FSTMutationBatch* _Nullable NextMutationBatchAfterBatchId( - model::BatchId batch_id); + model::BatchId batch_id) override; - void PerformConsistencyCheck(); + void PerformConsistencyCheck() override; bool ContainsKey(const model::DocumentKey& key); size_t CalculateByteSize(FSTLocalSerializer* serializer); - NSData* _Nullable GetLastStreamToken(); - void SetLastStreamToken(NSData* _Nullable token); + NSData* _Nullable GetLastStreamToken() override; + void SetLastStreamToken(NSData* _Nullable token) override; private: using DocumentReferenceSet = diff --git a/Firestore/core/src/firebase/firestore/local/mutation_queue.h b/Firestore/core/src/firebase/firestore/local/mutation_queue.h new file mode 100644 index 00000000000..784a7e55f56 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/local/mutation_queue.h @@ -0,0 +1,162 @@ +/* + * Copyright 2019 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. + */ + +#ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_MUTATION_QUEUE_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_MUTATION_QUEUE_H_ + +#if !defined(__OBJC__) +#error "For now, this file must only be included by ObjC source files." +#endif // !defined(__OBJC__) + +#import + +#include + +#include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/model/document_key_set.h" +#include "Firestore/core/src/firebase/firestore/model/types.h" + +@class FIRTimestamp; +@class FSTMutation; +@class FSTMutationBatch; +@class FSTQuery; + +NS_ASSUME_NONNULL_BEGIN + +namespace firebase { +namespace firestore { +namespace local { + +/** A queue of mutations to apply to the remote store. */ +class MutationQueue { + public: + virtual ~MutationQueue() { + } + + /** + * Starts the mutation queue, performing any initial reads that might be + * required to establish invariants, etc. + */ + virtual void Start() = 0; + + /** Returns true if this queue contains no mutation batches. */ + virtual bool IsEmpty() = 0; + + /** Acknowledges the given batch. */ + virtual void AcknowledgeBatch(FSTMutationBatch* batch, + NSData* _Nullable stream_token) = 0; + + /** Creates a new mutation batch and adds it to this mutation queue. */ + virtual FSTMutationBatch* AddMutationBatch( + FIRTimestamp* local_write_time, NSArray* mutations) = 0; + + /** + * Removes the given mutation batch from the queue. This is useful in two + * circumstances: + * + * + Removing applied mutations from the head of the queue + * + Removing rejected mutations from anywhere in the queue + */ + virtual void RemoveMutationBatch(FSTMutationBatch* batch) = 0; + + /** Gets all mutation batches in the mutation queue. */ + // TODO(mikelehen): PERF: Current consumer only needs mutated keys; if we can + // provide that cheaply, we should replace this. + virtual std::vector AllMutationBatches() = 0; + + /** + * Finds all mutation batches that could @em possibly affect the given + * document keys. Not all mutations in a batch will necessarily affect each + * key, so when looping through the batches you'll need to check that the + * mutation itself matches the key. + * + * Note that because of this requirement implementations are free to return + * mutation batches that don't contain any of the given document keys at all + * if it's convenient. + */ + // TODO(mcg): This should really return an iterator + virtual std::vector + AllMutationBatchesAffectingDocumentKeys( + const model::DocumentKeySet& document_keys) = 0; + + /** + * Finds all mutation batches that could @em possibly affect the given + * document key. Not all mutations in a batch will necessarily affect the + * document key, so when looping through the batch you'll need to check that + * the mutation itself matches the key. + * + * Note that because of this requirement implementations are free to return + * mutation batches that don't contain the document key at all if it's + * convenient. + */ + // TODO(mcg): This should really return an iterator + virtual std::vector AllMutationBatchesAffectingDocumentKey( + const model::DocumentKey& key) = 0; + + /** + * Finds all mutation batches that could affect the results for the given + * query. Not all mutations in a batch will necessarily affect the query, so + * when looping through the batch you'll need to check that the mutation + * itself matches the query. + * + * Note that because of this requirement implementations are free to return + * mutation batches that don't match the query at all if it's convenient. + * + * NOTE: A FSTPatchMutation does not need to include all fields in the query + * filter criteria in order to be a match (but any fields it does contain do + * need to match). + */ + // TODO(mikelehen): This should perhaps return an iterator, though I'm not + // sure we can avoid loading them all in memory. + virtual std::vector AllMutationBatchesAffectingQuery( + FSTQuery* query) = 0; + + /** Loads the mutation batch with the given batch_id. */ + virtual FSTMutationBatch* _Nullable LookupMutationBatch( + model::BatchId batch_id) = 0; + + /** + * Gets the first unacknowledged mutation batch after the passed in batchId in + * the mutation queue or nil if empty. + * + * @param batch_id The batch to search after, or kBatchIdUnknown for the first + * mutation in the queue. + * + * @return the next mutation or nil if there wasn't one. + */ + virtual FSTMutationBatch* _Nullable NextMutationBatchAfterBatchId( + model::BatchId batch_id) = 0; + + /** + * Performs a consistency check, examining the mutation queue for any leaks, + * if possible. + */ + virtual void PerformConsistencyCheck() = 0; + + /** Returns the current stream token for this mutation queue. */ + virtual NSData* _Nullable GetLastStreamToken() = 0; + + /** Sets the stream token for this mutation queue. */ + virtual void SetLastStreamToken(NSData* _Nullable stream_token) = 0; +}; + +} // namespace local +} // namespace firestore +} // namespace firebase + +NS_ASSUME_NONNULL_END + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_MUTATION_QUEUE_H_ From 5982981e12b1b74575a0fea06725b4845e94bfd9 Mon Sep 17 00:00:00 2001 From: Ryan Wilson Date: Mon, 28 Jan 2019 18:05:16 -0500 Subject: [PATCH 04/27] Prevent Messaging and IID singleton usage during tests. (#2250) * Prevent Messaging and IID singleton usage during tests. * Remove other uses of static Messaging instance. * Removed usages of messagingForTests. * Remove unused imports, unify testing code. * Use local import path instead of framework import. --- Example/Firebase.xcodeproj/project.pbxproj | 6 ++ .../Tests/FIRMessagingLinkHandlingTest.m | 9 ++- .../Tests/FIRMessagingReceiverTest.m | 17 +++-- .../Messaging/Tests/FIRMessagingServiceTest.m | 9 ++- Example/Messaging/Tests/FIRMessagingTest.m | 21 +++--- .../Tests/FIRMessagingTestUtilities.h | 44 +++++++++++++ .../Tests/FIRMessagingTestUtilities.m | 64 +++++++++++++++++++ Firebase/Messaging/FIRMessaging.m | 27 ++------ Firebase/Messaging/FIRMessagingReceiver.h | 14 +++- Firebase/Messaging/FIRMessagingReceiver.m | 23 +++++-- 10 files changed, 187 insertions(+), 47 deletions(-) create mode 100644 Example/Messaging/Tests/FIRMessagingTestUtilities.h create mode 100644 Example/Messaging/Tests/FIRMessagingTestUtilities.m diff --git a/Example/Firebase.xcodeproj/project.pbxproj b/Example/Firebase.xcodeproj/project.pbxproj index 7debc404cc0..829c7cdfad8 100644 --- a/Example/Firebase.xcodeproj/project.pbxproj +++ b/Example/Firebase.xcodeproj/project.pbxproj @@ -617,6 +617,7 @@ EDD53E2A211B08A300376BFF /* FIRComponentTestUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = EDD53E28211B08A300376BFF /* FIRComponentTestUtilities.m */; }; EDD53E2B211B08A300376BFF /* FIRComponentTestUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = EDD53E28211B08A300376BFF /* FIRComponentTestUtilities.m */; }; EDD53E2C211B08A300376BFF /* FIRComponentTestUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = EDD53E28211B08A300376BFF /* FIRComponentTestUtilities.m */; }; + EDF5242C21EA37AA00BB24C6 /* FIRMessagingTestUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = EDF5242B21EA364600BB24C6 /* FIRMessagingTestUtilities.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -1345,6 +1346,8 @@ EDD53E24211A442D00376BFF /* FIRAuthInteropFake.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRAuthInteropFake.m; path = Shared/FIRAuthInteropFake.m; sourceTree = ""; }; EDD53E28211B08A300376BFF /* FIRComponentTestUtilities.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRComponentTestUtilities.m; path = Shared/FIRComponentTestUtilities.m; sourceTree = ""; }; EDD53E29211B08A300376BFF /* FIRComponentTestUtilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FIRComponentTestUtilities.h; path = Shared/FIRComponentTestUtilities.h; sourceTree = ""; }; + EDF5242A21EA364600BB24C6 /* FIRMessagingTestUtilities.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FIRMessagingTestUtilities.h; sourceTree = ""; }; + EDF5242B21EA364600BB24C6 /* FIRMessagingTestUtilities.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRMessagingTestUtilities.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -2340,6 +2343,8 @@ DE9315D71E8738B70083EDBF /* FIRMessagingTestNotificationUtilities.m */, DE37C63A2163D5F30025D03E /* FIRMessagingAnalyticsTest.m */, DE9315D81E8738B70083EDBF /* Info.plist */, + EDF5242A21EA364600BB24C6 /* FIRMessagingTestUtilities.h */, + EDF5242B21EA364600BB24C6 /* FIRMessagingTestUtilities.m */, ); path = Tests; sourceTree = ""; @@ -4419,6 +4424,7 @@ DE9315F91E8738E60083EDBF /* FIRMessagingFakeConnection.m in Sources */, DE9316021E8738E60083EDBF /* FIRMessagingServiceTest.m in Sources */, DE9315FE1E8738E60083EDBF /* FIRMessagingRegistrarTest.m in Sources */, + EDF5242C21EA37AA00BB24C6 /* FIRMessagingTestUtilities.m in Sources */, DE9316031E8738E60083EDBF /* FIRMessagingSyncMessageManagerTest.m in Sources */, DE9315FF1E8738E60083EDBF /* FIRMessagingRemoteNotificationsProxyTest.m in Sources */, DEF61BFD216E8B1100A738D4 /* FIRMessagingReceiverTest.m in Sources */, diff --git a/Example/Messaging/Tests/FIRMessagingLinkHandlingTest.m b/Example/Messaging/Tests/FIRMessagingLinkHandlingTest.m index 33797a2f197..b8a2937e606 100644 --- a/Example/Messaging/Tests/FIRMessagingLinkHandlingTest.m +++ b/Example/Messaging/Tests/FIRMessagingLinkHandlingTest.m @@ -21,10 +21,12 @@ #import "FIRMessaging.h" #import "FIRMessagingConstants.h" #import "FIRMessagingTestNotificationUtilities.h" +#import "FIRMessagingTestUtilities.h" + +NSString *const kFIRMessagingTestsLinkHandlingSuiteName = @"com.messaging.test_linkhandling"; @interface FIRMessaging () -+ (FIRMessaging *)messagingForTests; - (NSURL *)linkURLFromMessage:(NSDictionary *)message; @end @@ -39,10 +41,13 @@ @implementation FIRMessagingLinkHandlingTest - (void)setUp { [super setUp]; - _messaging = [FIRMessaging messagingForTests]; + + NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:kFIRMessagingTestsLinkHandlingSuiteName]; + _messaging = [FIRMessagingTestUtilities messagingForTestsWithUserDefaults:defaults]; } - (void)tearDown { + [self.messaging.messagingUserDefaults removePersistentDomainForName:kFIRMessagingTestsLinkHandlingSuiteName]; _messaging = nil; [super tearDown]; } diff --git a/Example/Messaging/Tests/FIRMessagingReceiverTest.m b/Example/Messaging/Tests/FIRMessagingReceiverTest.m index 02818b754b7..95e6dd9c497 100644 --- a/Example/Messaging/Tests/FIRMessagingReceiverTest.m +++ b/Example/Messaging/Tests/FIRMessagingReceiverTest.m @@ -22,10 +22,9 @@ #import "FIRMessaging.h" #import "FIRMessaging_Private.h" +#import "FIRMessagingTestUtilities.h" -@interface FIRMessaging () -+ (FIRMessaging *)messagingForTests; -@end +NSString *const kFIRMessagingTestsReceiverSuiteName = @"com.messaging.test_receiverTest"; @interface FIRMessagingReceiverTest : XCTestCase @property(nonatomic, readonly, strong) FIRMessaging *messaging; @@ -36,9 +35,15 @@ @implementation FIRMessagingReceiverTest - (void)setUp { [super setUp]; - _messaging = [FIRMessaging messagingForTests]; - [[NSUserDefaults standardUserDefaults] - removePersistentDomainForName:[NSBundle mainBundle].bundleIdentifier]; + NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:kFIRMessagingTestsReceiverSuiteName]; + _messaging = [FIRMessagingTestUtilities messagingForTestsWithUserDefaults:defaults]; +} + +- (void)tearDown { + [self.messaging.messagingUserDefaults removePersistentDomainForName:kFIRMessagingTestsReceiverSuiteName]; + _messaging = nil; + + [super tearDown]; } - (void)testUseMessagingDelegate { diff --git a/Example/Messaging/Tests/FIRMessagingServiceTest.m b/Example/Messaging/Tests/FIRMessagingServiceTest.m index bb447472951..65019806c66 100644 --- a/Example/Messaging/Tests/FIRMessagingServiceTest.m +++ b/Example/Messaging/Tests/FIRMessagingServiceTest.m @@ -22,6 +22,7 @@ #import "FIRMessaging.h" #import "FIRMessagingClient.h" #import "FIRMessagingPubSub.h" +#import "FIRMessagingTestUtilities.h" #import "FIRMessagingTopicsCommon.h" #import "InternalHeaders/FIRMessagingInternalUtilities.h" #import "NSError+FIRMessaging.h" @@ -31,8 +32,9 @@ @"yUTTzK6dhIvLqzqqCSabaa4TQVM0pGTmF6r7tmMHPe6VYiGMHuCwJFgj5v97xl78sUNMLwuPPhoci8z_" @"QGlCrTbxCFGzEUfvA3fGpGgIVQU2W6"; +NSString *const kFIRMessagingTestsServiceSuiteName = @"com.messaging.test_serviceTest"; + @interface FIRMessaging () -+ (FIRMessaging *)messagingForTests; @property(nonatomic, readwrite, strong) FIRMessagingClient *client; @property(nonatomic, readwrite, strong) FIRMessagingPubSub *pubsub; @property(nonatomic, readwrite, strong) NSString *defaultFcmToken; @@ -55,7 +57,8 @@ @interface FIRMessagingServiceTest : XCTestCase { @implementation FIRMessagingServiceTest - (void)setUp { - _messaging = [FIRMessaging messagingForTests]; + NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:kFIRMessagingTestsServiceSuiteName]; + _messaging = [FIRMessagingTestUtilities messagingForTestsWithUserDefaults:defaults]; _messaging.defaultFcmToken = kFakeToken; _mockPubSub = OCMPartialMock(_messaging.pubsub); [_mockPubSub setClient:nil]; @@ -63,6 +66,8 @@ - (void)setUp { } - (void)tearDown { + [_messaging.messagingUserDefaults removePersistentDomainForName:kFIRMessagingTestsServiceSuiteName]; + _messaging = nil; [_mockPubSub stopMocking]; [super tearDown]; } diff --git a/Example/Messaging/Tests/FIRMessagingTest.m b/Example/Messaging/Tests/FIRMessagingTest.m index 66357e8b102..3fb0477ff45 100644 --- a/Example/Messaging/Tests/FIRMessagingTest.m +++ b/Example/Messaging/Tests/FIRMessagingTest.m @@ -20,25 +20,22 @@ #import #import +#import #import "FIRMessaging.h" #import "FIRMessaging_Private.h" +#import "FIRMessagingTestUtilities.h" extern NSString *const kFIRMessagingFCMTokenFetchAPNSOption; -@interface FIRInstanceID (ExposedForTest) - -+ (FIRInstanceID *)instanceIDForTests; - -@end +/// The NSUserDefaults domain for testing. +NSString *const kFIRMessagingDefaultsTestDomain = @"com.messaging.tests"; @interface FIRMessaging () -+ (FIRMessaging *)messagingForTests; @property(nonatomic, readwrite, strong) NSString *defaultFcmToken; @property(nonatomic, readwrite, strong) NSData *apnsTokenData; @property(nonatomic, readwrite, strong) FIRInstanceID *instanceID; -@property(nonatomic, readwrite, strong) NSUserDefaults *messagingUserDefaults; // Direct Channel Methods - (void)updateAutomaticClientConnection; @@ -59,8 +56,12 @@ @implementation FIRMessagingTest - (void)setUp { [super setUp]; - _messaging = [FIRMessaging messagingForTests]; - _messaging.instanceID = [FIRInstanceID instanceIDForTests]; + + // Create the messaging instance with all the necessary dependencies. + NSUserDefaults *defaults = + [[NSUserDefaults alloc] initWithSuiteName:kFIRMessagingDefaultsTestDomain]; + _messaging = [FIRMessagingTestUtilities messagingForTestsWithUserDefaults:defaults]; + _mockFirebaseApp = OCMClassMock([FIRApp class]); OCMStub([_mockFirebaseApp defaultApp]).andReturn(_mockFirebaseApp); _mockInstanceID = OCMPartialMock(self.messaging.instanceID); @@ -69,12 +70,14 @@ - (void)setUp { } - (void)tearDown { + [self.messaging.messagingUserDefaults removePersistentDomainForName:kFIRMessagingDefaultsTestDomain]; self.messaging.shouldEstablishDirectChannel = NO; self.messaging.defaultFcmToken = nil; self.messaging.apnsTokenData = nil; [_mockMessaging stopMocking]; [_mockInstanceID stopMocking]; [_mockFirebaseApp stopMocking]; + _messaging = nil; [super tearDown]; } diff --git a/Example/Messaging/Tests/FIRMessagingTestUtilities.h b/Example/Messaging/Tests/FIRMessagingTestUtilities.h new file mode 100644 index 00000000000..1226cd17c02 --- /dev/null +++ b/Example/Messaging/Tests/FIRMessagingTestUtilities.h @@ -0,0 +1,44 @@ +/* + * Copyright 2019 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 + +#import "FIRMessaging.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRMessaging (TestUtilities) +// Surface the user defaults instance to clean up after tests. +@property(nonatomic, strong) NSUserDefaults *messagingUserDefaults; +@end + +@interface FIRMessagingTestUtilities : NSObject + +/** + Creates an instance of FIRMessaging to use with tests, and will instantiate a new instance of + InstanceID. + + Note: This does not create a FIRApp instance and call `configureWithApp:`. If required, it's up to + each test to do so. + + @param userDefaults The user defaults to be used for Messaging. + @return An instance of FIRMessaging with everything initialized. + */ ++ (FIRMessaging *)messagingForTestsWithUserDefaults:(NSUserDefaults *)userDefaults; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Example/Messaging/Tests/FIRMessagingTestUtilities.m b/Example/Messaging/Tests/FIRMessagingTestUtilities.m new file mode 100644 index 00000000000..729587587c5 --- /dev/null +++ b/Example/Messaging/Tests/FIRMessagingTestUtilities.m @@ -0,0 +1,64 @@ +/* + * Copyright 2019 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 "FIRMessagingTestUtilities.h" + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRInstanceID (ExposedForTest) + +/// Private initializer to avoid singleton usage. +- (FIRInstanceID *)initPrivately; + +/// Starts fetching and configuration of InstanceID. This is necessary after the `initPrivately` +/// call. +- (void)start; + +@end + +@interface FIRMessaging (ExposedForTest) + +/// Surface internal initializer to avoid singleton usage during tests. +- (instancetype)initWithAnalytics:(nullable id)analytics + withInstanceID:(FIRInstanceID *)instanceID + withUserDefaults:(NSUserDefaults *)defaults; + +/// Kicks off required calls for some messaging tests. +- (void)start; + +@end + +@implementation FIRMessagingTestUtilities + ++ (FIRMessaging *)messagingForTestsWithUserDefaults:(NSUserDefaults *)userDefaults { + // Create the messaging instance with all the necessary dependencies. + FIRInstanceID *instanceID = [[FIRInstanceID alloc] initPrivately]; + [instanceID start]; + + // Create the messaging instance and call `start`. + FIRMessaging *messaging = [[FIRMessaging alloc] initWithAnalytics:nil + withInstanceID:instanceID + withUserDefaults:userDefaults]; + [messaging start]; + return messaging; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Messaging/FIRMessaging.m b/Firebase/Messaging/FIRMessaging.m index 7aa439191ed..57e1df55829 100644 --- a/Firebase/Messaging/FIRMessaging.m +++ b/Firebase/Messaging/FIRMessaging.m @@ -162,32 +162,19 @@ @interface FIRMessaging () @implementation FIRMessaging -// File static to support InstanceID tests that call [FIRMessaging messaging] after -// [FIRMessaging messagingForTests]. -static FIRMessaging *sMessaging; - + (FIRMessaging *)messaging { - if (sMessaging != nil) { - return sMessaging; - } FIRApp *defaultApp = [FIRApp defaultApp]; // Missing configure will be logged here. - id messaging = + id instance = FIR_COMPONENT(FIRMessagingInstanceProvider, defaultApp.container); + // We know the instance coming from the container is a FIRMessaging instance, cast it and move on. + FIRMessaging *messaging = (FIRMessaging *)instance; + static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - [(FIRMessaging *)messaging start]; + [messaging start]; }); - sMessaging = (FIRMessaging *)messaging; - return sMessaging; -} - -+ (FIRMessaging *)messagingForTests { - sMessaging = [[FIRMessaging alloc] initWithAnalytics:nil - withInstanceID:[FIRInstanceID instanceID] - withUserDefaults:[NSUserDefaults standardUserDefaults]]; - [sMessaging start]; - return sMessaging; + return messaging; } - (instancetype)initWithAnalytics:(nullable id)analytics @@ -322,7 +309,7 @@ - (void)setupNotificationListeners { } - (void)setupReceiver { - self.receiver = [[FIRMessagingReceiver alloc] init]; + self.receiver = [[FIRMessagingReceiver alloc] initWithUserDefaults:self.messagingUserDefaults]; self.receiver.delegate = self; } diff --git a/Firebase/Messaging/FIRMessagingReceiver.h b/Firebase/Messaging/FIRMessagingReceiver.h index e312420449f..8b5aa585bc2 100644 --- a/Firebase/Messaging/FIRMessagingReceiver.h +++ b/Firebase/Messaging/FIRMessagingReceiver.h @@ -17,18 +17,28 @@ #import "FIRMessagingDataMessageManager.h" #import "FIRMessaging.h" +NS_ASSUME_NONNULL_BEGIN + @class FIRMessagingReceiver; @protocol FIRMessagingReceiverDelegate -- (void)receiver:(nonnull FIRMessagingReceiver *)receiver - receivedRemoteMessage:(nonnull FIRMessagingRemoteMessage *)remoteMessage; +- (void)receiver:(FIRMessagingReceiver *)receiver + receivedRemoteMessage:(FIRMessagingRemoteMessage *)remoteMessage; @end @interface FIRMessagingReceiver : NSObject +/// Default initializer for creating the messaging receiver. +- (instancetype)initWithUserDefaults:(NSUserDefaults *)defaults NS_DESIGNATED_INITIALIZER; + +/// Use `initWithUserDefaults:` instead. +- (instancetype)init NS_UNAVAILABLE; + @property(nonatomic, weak, nullable) id delegate; /// Whether to use direct channel for direct channel message callback handler in all iOS versions. @property(nonatomic, assign) BOOL useDirectChannel; @end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Messaging/FIRMessagingReceiver.m b/Firebase/Messaging/FIRMessagingReceiver.m index ac8a6837561..05e6a75bc13 100644 --- a/Firebase/Messaging/FIRMessagingReceiver.m +++ b/Firebase/Messaging/FIRMessagingReceiver.m @@ -36,8 +36,22 @@ static int downstreamMessageID = 0; +@interface FIRMessagingReceiver () +@property(nonatomic, strong) NSUserDefaults *defaults; +@end + @implementation FIRMessagingReceiver +#pragma mark - Initializer + +- (instancetype)initWithUserDefaults:(NSUserDefaults *)defaults { + self = [super init]; + if (self != nil) { + _defaults = defaults; + } + return self; +} + #pragma mark - FIRMessagingDataMessageManager protocol - (void)didReceiveMessage:(NSDictionary *)message withIdentifier:(nullable NSString *)messageID { @@ -152,9 +166,8 @@ + (NSString *)nextMessageID { - (BOOL)useDirectChannel { // Check storage - NSUserDefaults *messagingDefaults = [NSUserDefaults standardUserDefaults]; id shouldUseMessagingDelegate = - [messagingDefaults objectForKey:kFIRMessagingUserDefaultsKeyUseMessagingDelegate]; + [_defaults objectForKey:kFIRMessagingUserDefaultsKeyUseMessagingDelegate]; if (shouldUseMessagingDelegate) { return [shouldUseMessagingDelegate boolValue]; } @@ -170,12 +183,10 @@ - (BOOL)useDirectChannel { } - (void)setUseDirectChannel:(BOOL)useDirectChannel { - NSUserDefaults *messagingDefaults = [NSUserDefaults standardUserDefaults]; BOOL shouldUseMessagingDelegate = [self useDirectChannel]; if (useDirectChannel != shouldUseMessagingDelegate) { - [messagingDefaults setBool:useDirectChannel - forKey:kFIRMessagingUserDefaultsKeyUseMessagingDelegate]; - [messagingDefaults synchronize]; + [_defaults setBool:useDirectChannel forKey:kFIRMessagingUserDefaultsKeyUseMessagingDelegate]; + [_defaults synchronize]; } } From 7c512158823021094ef338336d8309d4d2bc9b18 Mon Sep 17 00:00:00 2001 From: Greg Soltis Date: Mon, 28 Jan 2019 16:01:30 -0800 Subject: [PATCH 05/27] Port FSTListenSequence (#2322) --- Firestore/Source/Core/FSTListenSequence.h | 38 -------------- Firestore/Source/Core/FSTListenSequence.mm | 52 ------------------- Firestore/Source/Local/FSTLevelDB.mm | 10 ++-- Firestore/Source/Local/FSTLocalStore.mm | 1 - .../Source/Local/FSTMemoryPersistence.mm | 10 ++-- .../firestore/local/listen_sequence.h | 49 +++++++++++++++++ 6 files changed, 61 insertions(+), 99 deletions(-) delete mode 100644 Firestore/Source/Core/FSTListenSequence.h delete mode 100644 Firestore/Source/Core/FSTListenSequence.mm create mode 100644 Firestore/core/src/firebase/firestore/local/listen_sequence.h diff --git a/Firestore/Source/Core/FSTListenSequence.h b/Firestore/Source/Core/FSTListenSequence.h deleted file mode 100644 index c9f798cc875..00000000000 --- a/Firestore/Source/Core/FSTListenSequence.h +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2018 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 - -#include "Firestore/core/src/firebase/firestore/model/types.h" - -NS_ASSUME_NONNULL_BEGIN - -/** - * FSTListenSequence is a monotonic sequence. It is initialized with a minimum value to - * exceed. All subsequent calls to next will return increasing values. - */ -@interface FSTListenSequence : NSObject - -- (instancetype)initStartingAfter:(firebase::firestore::model::ListenSequenceNumber)after - NS_DESIGNATED_INITIALIZER; - -- (id)init NS_UNAVAILABLE; - -- (firebase::firestore::model::ListenSequenceNumber)next; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTListenSequence.mm b/Firestore/Source/Core/FSTListenSequence.mm deleted file mode 100644 index f96568b6849..00000000000 --- a/Firestore/Source/Core/FSTListenSequence.mm +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2018 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 "Firestore/Source/Core/FSTListenSequence.h" - -using firebase::firestore::model::ListenSequenceNumber; - -NS_ASSUME_NONNULL_BEGIN - -#pragma mark - FSTListenSequence - -@interface FSTListenSequence () { - ListenSequenceNumber _previousSequenceNumber; -} - -@end - -@implementation FSTListenSequence - -#pragma mark - Constructors - -- (instancetype)initStartingAfter:(ListenSequenceNumber)after { - self = [super init]; - if (self) { - _previousSequenceNumber = after; - } - return self; -} - -#pragma mark - Public methods - -- (ListenSequenceNumber)next { - _previousSequenceNumber++; - return _previousSequenceNumber; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLevelDB.mm b/Firestore/Source/Local/FSTLevelDB.mm index 2e245c2d298..c451aa9b262 100644 --- a/Firestore/Source/Local/FSTLevelDB.mm +++ b/Firestore/Source/Local/FSTLevelDB.mm @@ -21,7 +21,6 @@ #include #import "FIRFirestoreErrors.h" -#import "Firestore/Source/Core/FSTListenSequence.h" #import "Firestore/Source/Local/FSTLRUGarbageCollector.h" #import "Firestore/Source/Local/FSTLevelDBMutationQueue.h" #import "Firestore/Source/Remote/FSTSerializerBeta.h" @@ -35,6 +34,7 @@ #include "Firestore/core/src/firebase/firestore/local/leveldb_remote_document_cache.h" #include "Firestore/core/src/firebase/firestore/local/leveldb_transaction.h" #include "Firestore/core/src/firebase/firestore/local/leveldb_util.h" +#include "Firestore/core/src/firebase/firestore/local/listen_sequence.h" #include "Firestore/core/src/firebase/firestore/local/reference_set.h" #include "Firestore/core/src/firebase/firestore/local/remote_document_cache.h" #include "Firestore/core/src/firebase/firestore/model/database_id.h" @@ -66,6 +66,7 @@ using firebase::firestore::local::LevelDbQueryCache; using firebase::firestore::local::LevelDbRemoteDocumentCache; using firebase::firestore::local::LevelDbTransaction; +using firebase::firestore::local::ListenSequence; using firebase::firestore::local::LruParams; using firebase::firestore::local::ReferenceSet; using firebase::firestore::local::RemoteDocumentCache; @@ -119,7 +120,8 @@ @implementation FSTLevelDBLRUDelegate { __weak FSTLevelDB *_db; ReferenceSet *_additionalReferences; ListenSequenceNumber _currentSequenceNumber; - FSTListenSequence *_listenSequence; + // PORTING NOTE: doesn't need to be a pointer once this class is ported to C++. + std::unique_ptr _listenSequence; } - (instancetype)initWithPersistence:(FSTLevelDB *)persistence lruParams:(LruParams)lruParams { @@ -133,13 +135,13 @@ - (instancetype)initWithPersistence:(FSTLevelDB *)persistence lruParams:(LruPara - (void)start { ListenSequenceNumber highestSequenceNumber = _db.queryCache->highest_listen_sequence_number(); - _listenSequence = [[FSTListenSequence alloc] initStartingAfter:highestSequenceNumber]; + _listenSequence = absl::make_unique(highestSequenceNumber); } - (void)transactionWillStart { HARD_ASSERT(_currentSequenceNumber == kFSTListenSequenceNumberInvalid, "Previous sequence number is still in effect"); - _currentSequenceNumber = [_listenSequence next]; + _currentSequenceNumber = _listenSequence->Next(); } - (void)transactionWillCommit { diff --git a/Firestore/Source/Local/FSTLocalStore.mm b/Firestore/Source/Local/FSTLocalStore.mm index 11f0aec0898..64e452b825c 100644 --- a/Firestore/Source/Local/FSTLocalStore.mm +++ b/Firestore/Source/Local/FSTLocalStore.mm @@ -21,7 +21,6 @@ #include #import "FIRTimestamp.h" -#import "Firestore/Source/Core/FSTListenSequence.h" #import "Firestore/Source/Core/FSTQuery.h" #import "Firestore/Source/Local/FSTLRUGarbageCollector.h" #import "Firestore/Source/Local/FSTLocalDocumentsView.h" diff --git a/Firestore/Source/Local/FSTMemoryPersistence.mm b/Firestore/Source/Local/FSTMemoryPersistence.mm index e69f9d82502..f90d9f3c826 100644 --- a/Firestore/Source/Local/FSTMemoryPersistence.mm +++ b/Firestore/Source/Local/FSTMemoryPersistence.mm @@ -21,11 +21,11 @@ #include #include -#import "Firestore/Source/Core/FSTListenSequence.h" #import "Firestore/Source/Local/FSTMemoryMutationQueue.h" #include "absl/memory/memory.h" #include "Firestore/core/src/firebase/firestore/auth/user.h" +#include "Firestore/core/src/firebase/firestore/local/listen_sequence.h" #include "Firestore/core/src/firebase/firestore/local/memory_query_cache.h" #include "Firestore/core/src/firebase/firestore/local/memory_remote_document_cache.h" #include "Firestore/core/src/firebase/firestore/local/reference_set.h" @@ -34,6 +34,7 @@ using firebase::firestore::auth::HashUser; using firebase::firestore::auth::User; +using firebase::firestore::local::ListenSequence; using firebase::firestore::local::LruParams; using firebase::firestore::local::MemoryQueryCache; using firebase::firestore::local::MemoryRemoteDocumentCache; @@ -161,7 +162,8 @@ @implementation FSTMemoryLRUReferenceDelegate { std::unordered_map _sequenceNumbers; ReferenceSet *_additionalReferences; FSTLRUGarbageCollector *_gc; - FSTListenSequence *_listenSequence; + // PORTING NOTE: when this class is ported to C++, this does not need to be a pointer + std::unique_ptr _listenSequence; ListenSequenceNumber _currentSequenceNumber; FSTLocalSerializer *_serializer; } @@ -176,7 +178,7 @@ - (instancetype)initWithPersistence:(FSTMemoryPersistence *)persistence // Theoretically this is always 0, since this is all in-memory... ListenSequenceNumber highestSequenceNumber = _persistence.queryCache->highest_listen_sequence_number(); - _listenSequence = [[FSTListenSequence alloc] initStartingAfter:highestSequenceNumber]; + _listenSequence = absl::make_unique(highestSequenceNumber); _serializer = serializer; } return self; @@ -210,7 +212,7 @@ - (void)limboDocumentUpdated:(const DocumentKey &)key { } - (void)startTransaction:(absl::string_view)label { - _currentSequenceNumber = [_listenSequence next]; + _currentSequenceNumber = _listenSequence->Next(); } - (void)commitTransaction { diff --git a/Firestore/core/src/firebase/firestore/local/listen_sequence.h b/Firestore/core/src/firebase/firestore/local/listen_sequence.h new file mode 100644 index 00000000000..6172e977f6a --- /dev/null +++ b/Firestore/core/src/firebase/firestore/local/listen_sequence.h @@ -0,0 +1,49 @@ +/* + * Copyright 2019 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. + */ + +#ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_LISTEN_SEQUENCE_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_LISTEN_SEQUENCE_H_ + +#include "Firestore/core/src/firebase/firestore/model/types.h" + +namespace firebase { +namespace firestore { +namespace local { + +/** + * ListenSequence is a monotonic sequence. It is initialized with a minimum + * value to exceed. All subsequent calls to next will return increasing values. + */ +class ListenSequence { + public: + explicit ListenSequence(model::ListenSequenceNumber starting_after) + : previous_sequence_number_(starting_after) { + } + + model::ListenSequenceNumber Next() { + previous_sequence_number_++; + return previous_sequence_number_; + } + + private: + model::ListenSequenceNumber previous_sequence_number_; +}; + +} // namespace local +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_LISTEN_SEQUENCE_H_ From 2ac7c2f2ea5808aa0a2bec692a69be79dc0463ae Mon Sep 17 00:00:00 2001 From: Gil Date: Mon, 28 Jan 2019 17:21:49 -0800 Subject: [PATCH 06/27] Verify large write batches support (#2321) * Reorder tests to match Android * Add missing NS_ASSUME_NONNULL_BEGIN * Add missing [self awaitExpectations] * Port can write very large batches from Android --- .../Integration/API/FIRWriteBatchTests.mm | 119 ++++++++++++------ 1 file changed, 84 insertions(+), 35 deletions(-) diff --git a/Firestore/Example/Tests/Integration/API/FIRWriteBatchTests.mm b/Firestore/Example/Tests/Integration/API/FIRWriteBatchTests.mm index 472b8ac565e..7347e182e33 100644 --- a/Firestore/Example/Tests/Integration/API/FIRWriteBatchTests.mm +++ b/Firestore/Example/Tests/Integration/API/FIRWriteBatchTests.mm @@ -24,6 +24,14 @@ #import "Firestore/Example/Tests/Util/FSTEventAccumulator.h" #import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h" +#include "Firestore/core/src/firebase/firestore/util/autoid.h" +#include "Firestore/core/src/firebase/firestore/util/string_apple.h" + +using firebase::firestore::util::CreateAutoId; +using firebase::firestore::util::WrapNSString; + +NS_ASSUME_NONNULL_BEGIN + @interface FIRWriteBatchTests : FSTIntegrationTestCase @end @@ -124,6 +132,52 @@ - (void)testCannotUpdateNonexistentDocuments { XCTAssertFalse(result.exists); } +- (void)testUpdateFieldsWithDots { + FIRDocumentReference *doc = [self documentRef]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"testUpdateFieldsWithDots"]; + FIRWriteBatch *batch = [doc.firestore batch]; + [batch setData:@{@"a.b" : @"old", @"c.d" : @"old"} forDocument:doc]; + [batch updateData:@{[[FIRFieldPath alloc] initWithFields:@[ @"a.b" ]] : @"new"} forDocument:doc]; + + [batch commitWithCompletion:^(NSError *_Nullable error) { + XCTAssertNil(error); + [doc getDocumentWithCompletion:^(FIRDocumentSnapshot *snapshot, NSError *error) { + XCTAssertNil(error); + XCTAssertEqualObjects(snapshot.data, (@{@"a.b" : @"new", @"c.d" : @"old"})); + }]; + [expectation fulfill]; + }]; + + [self awaitExpectations]; +} + +- (void)testUpdateNestedFields { + FIRDocumentReference *doc = [self documentRef]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"testUpdateNestedFields"]; + FIRWriteBatch *batch = [doc.firestore batch]; + [batch setData:@{@"a" : @{@"b" : @"old"}, @"c" : @{@"d" : @"old"}, @"e" : @{@"f" : @"old"}} + forDocument:doc]; + [batch + updateData:@{@"a.b" : @"new", [[FIRFieldPath alloc] initWithFields:@[ @"c", @"d" ]] : @"new"} + forDocument:doc]; + [batch commitWithCompletion:^(NSError *_Nullable error) { + XCTAssertNil(error); + [doc getDocumentWithCompletion:^(FIRDocumentSnapshot *snapshot, NSError *error) { + XCTAssertNil(error); + XCTAssertEqualObjects(snapshot.data, (@{ + @"a" : @{@"b" : @"new"}, + @"c" : @{@"d" : @"new"}, + @"e" : @{@"f" : @"old"} + })); + }]; + [expectation fulfill]; + }]; + + [self awaitExpectations]; +} + - (void)testDeleteDocuments { FIRDocumentReference *doc = [self documentRef]; [self writeDocumentRef:doc data:@{@"foo" : @"bar"}]; @@ -161,6 +215,7 @@ - (void)testBatchesCommitAtomicallyRaisingCorrectEvents { XCTAssertNil(error); [expectation fulfill]; }]; + [self awaitExpectations]; FIRQuerySnapshot *localSnap = [accumulator awaitEventWithName:@"local event"]; XCTAssertTrue(localSnap.metadata.hasPendingWrites); @@ -192,6 +247,7 @@ - (void)testBatchesFailAtomicallyRaisingCorrectEvents { XCTAssertEqual(error.code, FIRFirestoreErrorCodeNotFound); [expectation fulfill]; }]; + [self awaitExpectations]; // Local event with the set document. FIRQuerySnapshot *localSnap = [accumulator awaitEventWithName:@"local event"]; @@ -223,6 +279,7 @@ - (void)testWriteTheSameServerTimestampAcrossWrites { XCTAssertNil(error); [expectation fulfill]; }]; + [self awaitExpectations]; FIRQuerySnapshot *localSnap = [accumulator awaitEventWithName:@"local event"]; XCTAssertTrue(localSnap.metadata.hasPendingWrites); @@ -254,6 +311,7 @@ - (void)testCanWriteTheSameDocumentMultipleTimes { XCTAssertNil(error); [expectation fulfill]; }]; + [self awaitExpectations]; FIRDocumentSnapshot *localSnap = [accumulator awaitEventWithName:@"local event"]; XCTAssertTrue(localSnap.metadata.hasPendingWrites); @@ -265,50 +323,39 @@ - (void)testCanWriteTheSameDocumentMultipleTimes { XCTAssertEqualObjects(serverSnap.data, (@{@"a" : @1, @"b" : @2, @"when" : when})); } -- (void)testUpdateFieldsWithDots { - FIRDocumentReference *doc = [self documentRef]; +- (void)testCanWriteVeryLargeBatches { + // On Android, SQLite Cursors are limited reading no more than 2 MB per row (despite being able + // to write very large values). This test verifies that the local MutationQueue is not subject + // to this limitation. + + // Create a map containing nearly 1 MB of data. Note that if you use 1024 below this will create + // a document larger than 1 MB, which will be rejected by the backend as too large. + NSString *kb = [@"" stringByPaddingToLength:1000 withString:@"a" startingAtIndex:0]; + NSMutableDictionary *values = [NSMutableDictionary dictionary]; + for (int i = 0; i < 1000; i++) { + values[WrapNSString(CreateAutoId())] = kb; + } - XCTestExpectation *expectation = [self expectationWithDescription:@"testUpdateFieldsWithDots"]; + FIRDocumentReference *doc = [self documentRef]; FIRWriteBatch *batch = [doc.firestore batch]; - [batch setData:@{@"a.b" : @"old", @"c.d" : @"old"} forDocument:doc]; - [batch updateData:@{[[FIRFieldPath alloc] initWithFields:@[ @"a.b" ]] : @"new"} forDocument:doc]; - - [batch commitWithCompletion:^(NSError *_Nullable error) { - XCTAssertNil(error); - [doc getDocumentWithCompletion:^(FIRDocumentSnapshot *snapshot, NSError *error) { - XCTAssertNil(error); - XCTAssertEqualObjects(snapshot.data, (@{@"a.b" : @"new", @"c.d" : @"old"})); - }]; - [expectation fulfill]; - }]; - [self awaitExpectations]; -} - -- (void)testUpdateNestedFields { - FIRDocumentReference *doc = [self documentRef]; + // Write a batch containing 3 copies of the data, creating a ~3 MB batch. Writing to the same + // document in a batch is allowed and so long as the net size of the document is under 1 MB the + // batch is allowed. + [batch setData:values forDocument:doc]; + for (int i = 0; i < 2; i++) { + [batch updateData:values forDocument:doc]; + } - XCTestExpectation *expectation = [self expectationWithDescription:@"testUpdateNestedFields"]; - FIRWriteBatch *batch = [doc.firestore batch]; - [batch setData:@{@"a" : @{@"b" : @"old"}, @"c" : @{@"d" : @"old"}, @"e" : @{@"f" : @"old"}} - forDocument:doc]; - [batch - updateData:@{@"a.b" : @"new", [[FIRFieldPath alloc] initWithFields:@[ @"c", @"d" ]] : @"new"} - forDocument:doc]; + XCTestExpectation *expectation = [self expectationWithDescription:@"batch written"]; [batch commitWithCompletion:^(NSError *_Nullable error) { XCTAssertNil(error); - [doc getDocumentWithCompletion:^(FIRDocumentSnapshot *snapshot, NSError *error) { - XCTAssertNil(error); - XCTAssertEqualObjects(snapshot.data, (@{ - @"a" : @{@"b" : @"new"}, - @"c" : @{@"d" : @"new"}, - @"e" : @{@"f" : @"old"} - })); - }]; [expectation fulfill]; }]; - [self awaitExpectations]; + + FIRDocumentSnapshot *snap = [self readDocumentForRef:doc]; + XCTAssertEqualObjects(values, snap.data); } // Returns how much memory the test application is currently using, in megabytes (fractional part is @@ -363,3 +410,5 @@ - (void)testReasonableMemoryUsageForLotsOfMutations { } @end + +NS_ASSUME_NONNULL_END From 06d9e6dec3ebaa9b31a5814c15df4caa6f3480e3 Mon Sep 17 00:00:00 2001 From: Gil Date: Tue, 29 Jan 2019 09:50:09 -0800 Subject: [PATCH 07/27] Update CHANGELOG for Firestore v1.0.1 (#2323) --- Firestore/CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Firestore/CHANGELOG.md b/Firestore/CHANGELOG.md index 3d9a0a77cc9..95bd02acabc 100644 --- a/Firestore/CHANGELOG.md +++ b/Firestore/CHANGELOG.md @@ -1,5 +1,8 @@ # Unreleased +# v1.0.1 +- [changed] Internal improvements. + # v1.0.0 - [changed] **Breaking change:** The `areTimestampsInSnapshotsEnabled` setting is now enabled by default. Timestamp fields that read from a From f08a8bc6dd82a34dd3e6199fce88f86585127400 Mon Sep 17 00:00:00 2001 From: Konstantin Varlamov Date: Tue, 29 Jan 2019 15:10:18 -0500 Subject: [PATCH 08/27] C++ migration: port `FSTTargetChange` (#2318) --- .../Tests/Core/FSTQueryListenerTests.mm | 48 +++--- Firestore/Example/Tests/Core/FSTViewTests.mm | 20 +-- .../Tests/Remote/FSTRemoteEventTests.mm | 149 +++++++++--------- Firestore/Example/Tests/Util/FSTHelpers.h | 22 ++- Firestore/Example/Tests/Util/FSTHelpers.mm | 43 ++--- Firestore/Source/Core/FSTSyncEngine.mm | 16 +- Firestore/Source/Core/FSTView.h | 19 ++- Firestore/Source/Core/FSTView.mm | 22 +-- Firestore/Source/Local/FSTLocalStore.mm | 20 +-- Firestore/Source/Remote/FSTRemoteEvent.h | 64 +------- Firestore/Source/Remote/FSTRemoteEvent.mm | 71 ++------- Firestore/Source/Remote/FSTRemoteStore.mm | 4 +- .../firebase/firestore/remote/remote_event.h | 86 +++++++++- .../firebase/firestore/remote/remote_event.mm | 43 ++--- 14 files changed, 306 insertions(+), 321 deletions(-) diff --git a/Firestore/Example/Tests/Core/FSTQueryListenerTests.mm b/Firestore/Example/Tests/Core/FSTQueryListenerTests.mm index 12b4f9ecaf0..d0d8f45703d 100644 --- a/Firestore/Example/Tests/Core/FSTQueryListenerTests.mm +++ b/Firestore/Example/Tests/Core/FSTQueryListenerTests.mm @@ -28,12 +28,14 @@ #import "Firestore/Example/Tests/Util/FSTHelpers.h" #include "Firestore/core/src/firebase/firestore/model/types.h" +#include "Firestore/core/src/firebase/firestore/remote/remote_event.h" #include "Firestore/core/src/firebase/firestore/util/executor_libdispatch.h" #include "absl/memory/memory.h" using firebase::firestore::core::DocumentViewChangeType; using firebase::firestore::model::DocumentKeySet; using firebase::firestore::model::OnlineState; +using firebase::firestore::remote::TargetChange; using firebase::firestore::util::ExecutorLibdispatch; NS_ASSUME_NONNULL_BEGIN @@ -84,8 +86,8 @@ - (void)testRaisesCollectionEvents { FSTQueryListener *otherListener = [self listenToQuery:query accumulatingSnapshots:otherAccum]; FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; - FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); - FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc2prime ], nil); + FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1, doc2 ], absl::nullopt); + FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc2prime ], absl::nullopt); FSTDocumentViewChange *change1 = [FSTDocumentViewChange changeWithDocument:doc1 type:DocumentViewChangeType::kAdded]; @@ -142,7 +144,7 @@ - (void)testRaisesEventForEmptyCollectionAfterSync { accumulatingSnapshots:accum]; FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; - FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[], nil); + FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[], absl::nullopt); FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[], FSTTestTargetChangeMarkCurrent()); [listener queryDidChangeViewSnapshot:snap1]; @@ -167,8 +169,8 @@ - (void)testMutingAsyncListenerPreventsAllSubsequentEvents { }]; FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; - FSTViewSnapshot *viewSnapshot1 = FSTTestApplyChanges(view, @[ doc1 ], nil); - FSTViewSnapshot *viewSnapshot2 = FSTTestApplyChanges(view, @[ doc2 ], nil); + FSTViewSnapshot *viewSnapshot1 = FSTTestApplyChanges(view, @[ doc1 ], absl::nullopt); + FSTViewSnapshot *viewSnapshot2 = FSTTestApplyChanges(view, @[ doc2 ], absl::nullopt); FSTViewSnapshotHandler handler = listener.asyncSnapshotHandler; handler(viewSnapshot1, nil); @@ -204,11 +206,11 @@ - (void)testDoesNotRaiseEventsForMetadataChangesUnlessSpecified { accumulatingSnapshots:fullAccum]; FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; - FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1 ], nil); + FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1 ], absl::nullopt); - FSTTargetChange *ackTarget = FSTTestTargetChangeAckDocuments({doc1.key}); + TargetChange ackTarget = FSTTestTargetChangeAckDocuments({doc1.key}); FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[], ackTarget); - FSTViewSnapshot *snap3 = FSTTestApplyChanges(view, @[ doc2 ], nil); + FSTViewSnapshot *snap3 = FSTTestApplyChanges(view, @[ doc2 ], absl::nullopt); [filteredListener queryDidChangeViewSnapshot:snap1]; // local event [filteredListener queryDidChangeViewSnapshot:snap2]; // no event @@ -248,9 +250,9 @@ - (void)testRaisesDocumentMetadataEventsOnlyWhenSpecified { accumulatingSnapshots:fullAccum]; FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; - FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); - FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc1Prime ], nil); - FSTViewSnapshot *snap3 = FSTTestApplyChanges(view, @[ doc3 ], nil); + FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1, doc2 ], absl::nullopt); + FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc1Prime ], absl::nullopt); + FSTViewSnapshot *snap3 = FSTTestApplyChanges(view, @[ doc3 ], absl::nullopt); FSTDocumentViewChange *change1 = [FSTDocumentViewChange changeWithDocument:doc1 type:DocumentViewChangeType::kAdded]; @@ -303,10 +305,10 @@ - (void)testRaisesQueryMetadataEventsOnlyWhenHasPendingWritesOnTheQueryChanges { accumulatingSnapshots:fullAccum]; FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; - FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); - FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc1Prime ], nil); - FSTViewSnapshot *snap3 = FSTTestApplyChanges(view, @[ doc3 ], nil); - FSTViewSnapshot *snap4 = FSTTestApplyChanges(view, @[ doc2Prime ], nil); + FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1, doc2 ], absl::nullopt); + FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc1Prime ], absl::nullopt); + FSTViewSnapshot *snap3 = FSTTestApplyChanges(view, @[ doc3 ], absl::nullopt); + FSTViewSnapshot *snap4 = FSTTestApplyChanges(view, @[ doc2Prime ], absl::nullopt); [fullListener queryDidChangeViewSnapshot:snap1]; [fullListener queryDidChangeViewSnapshot:snap2]; // Emits no events. @@ -343,8 +345,8 @@ - (void)testMetadataOnlyDocumentChangesAreFilteredOutWhenIncludeDocumentMetadata accumulatingSnapshots:filteredAccum]; FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; - FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); - FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc1Prime, doc3 ], nil); + FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1, doc2 ], absl::nullopt); + FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc1Prime, doc3 ], absl::nullopt); FSTDocumentViewChange *change3 = [FSTDocumentViewChange changeWithDocument:doc3 type:DocumentViewChangeType::kAdded]; @@ -378,8 +380,8 @@ - (void)testWillWaitForSyncIfOnline { accumulatingSnapshots:events]; FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; - FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1 ], nil); - FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc2 ], nil); + FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1 ], absl::nullopt); + FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc2 ], absl::nullopt); FSTViewSnapshot *snap3 = FSTTestApplyChanges(view, @[], FSTTestTargetChangeAckDocuments({doc1.key, doc2.key})); @@ -420,8 +422,8 @@ - (void)testWillRaiseInitialEventWhenGoingOffline { accumulatingSnapshots:events]; FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; - FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1 ], nil); - FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc2 ], nil); + FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1 ], absl::nullopt); + FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc2 ], absl::nullopt); [listener applyChangedOnlineState:OnlineState::Online]; // no event [listener queryDidChangeViewSnapshot:snap1]; // no event @@ -463,7 +465,7 @@ - (void)testWillRaiseInitialEventWhenGoingOfflineAndThereAreNoDocs { accumulatingSnapshots:events]; FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; - FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[], nil); + FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[], absl::nullopt); [listener applyChangedOnlineState:OnlineState::Online]; // no event [listener queryDidChangeViewSnapshot:snap1]; // no event @@ -490,7 +492,7 @@ - (void)testWillRaiseInitialEventWhenStartingOfflineAndThereAreNoDocs { accumulatingSnapshots:events]; FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; - FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[], nil); + FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[], absl::nullopt); [listener applyChangedOnlineState:OnlineState::Offline]; // no event [listener queryDidChangeViewSnapshot:snap1]; // event diff --git a/Firestore/Example/Tests/Core/FSTViewTests.mm b/Firestore/Example/Tests/Core/FSTViewTests.mm index 9173b3ce579..5e6c432d40b 100644 --- a/Firestore/Example/Tests/Core/FSTViewTests.mm +++ b/Firestore/Example/Tests/Core/FSTViewTests.mm @@ -30,6 +30,7 @@ #include "Firestore/core/src/firebase/firestore/model/resource_path.h" #include "Firestore/core/test/firebase/firestore/testutil/testutil.h" +#include "absl/types/optional.h" namespace testutil = firebase::firestore::testutil; using firebase::firestore::core::DocumentViewChangeType; @@ -89,7 +90,7 @@ - (void)testRemovesDocuments { FSTTestDoc("rooms/eros/messages/3", 0, @{@"text" : @"msg3"}, FSTDocumentStateSynced); // initial state - FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); + FSTTestApplyChanges(view, @[ doc1, doc2 ], absl::nullopt); // delete doc2, add doc3 FSTViewSnapshot *snapshot = @@ -120,10 +121,10 @@ - (void)testReturnsNilIfThereAreNoChanges { FSTTestDoc("rooms/eros/messages/2", 0, @{@"text" : @"msg2"}, FSTDocumentStateSynced); // initial state - FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); + FSTTestApplyChanges(view, @[ doc1, doc2 ], absl::nullopt); // reapply same docs, no changes - FSTViewSnapshot *snapshot = FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); + FSTViewSnapshot *snapshot = FSTTestApplyChanges(view, @[ doc1, doc2 ], absl::nullopt); XCTAssertNil(snapshot); } @@ -131,7 +132,7 @@ - (void)testDoesNotReturnNilForFirstChanges { FSTQuery *query = [self queryForMessages]; FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; - FSTViewSnapshot *snapshot = FSTTestApplyChanges(view, @[], nil); + FSTViewSnapshot *snapshot = FSTTestApplyChanges(view, @[], absl::nullopt); XCTAssertNotNil(snapshot); } @@ -155,7 +156,8 @@ - (void)testFiltersDocumentsBasedOnQueryWithFilter { FSTDocument *doc5 = FSTTestDoc("rooms/eros/messages/5", 0, @{@"sort" : @1}, FSTDocumentStateSynced); - FSTViewSnapshot *snapshot = FSTTestApplyChanges(view, @[ doc1, doc2, doc3, doc4, doc5 ], nil); + FSTViewSnapshot *snapshot = + FSTTestApplyChanges(view, @[ doc1, doc2, doc3, doc4, doc5 ], absl::nullopt); XCTAssertEqual(snapshot.query, query); @@ -189,7 +191,7 @@ - (void)testUpdatesDocumentsBasedOnQueryWithFilter { FSTTestDoc("rooms/eros/messages/3", 0, @{@"sort" : @2}, FSTDocumentStateSynced); FSTDocument *doc4 = FSTTestDoc("rooms/eros/messages/4", 0, @{}, FSTDocumentStateSynced); - FSTViewSnapshot *snapshot = FSTTestApplyChanges(view, @[ doc1, doc2, doc3, doc4 ], nil); + FSTViewSnapshot *snapshot = FSTTestApplyChanges(view, @[ doc1, doc2, doc3, doc4 ], absl::nullopt); XCTAssertEqual(snapshot.query, query); @@ -202,7 +204,7 @@ - (void)testUpdatesDocumentsBasedOnQueryWithFilter { FSTDocument *newDoc4 = FSTTestDoc("rooms/eros/messages/4", 1, @{@"sort" : @0}, FSTDocumentStateSynced); - snapshot = FSTTestApplyChanges(view, @[ newDoc2, newDoc3, newDoc4 ], nil); + snapshot = FSTTestApplyChanges(view, @[ newDoc2, newDoc3, newDoc4 ], absl::nullopt); XCTAssertEqual(snapshot.query, query); @@ -232,7 +234,7 @@ - (void)testRemovesDocumentsForQueryWithLimit { FSTTestDoc("rooms/eros/messages/3", 0, @{@"text" : @"msg3"}, FSTDocumentStateSynced); // initial state - FSTTestApplyChanges(view, @[ doc1, doc3 ], nil); + FSTTestApplyChanges(view, @[ doc1, doc3 ], absl::nullopt); // add doc2, which should push out doc3 FSTViewSnapshot *snapshot = FSTTestApplyChanges( @@ -269,7 +271,7 @@ - (void)testDoesntReportChangesForDocumentBeyondLimitOfQuery { FSTTestDoc("rooms/eros/messages/4", 0, @{@"num" : @4}, FSTDocumentStateSynced); // initial state - FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); + FSTTestApplyChanges(view, @[ doc1, doc2 ], absl::nullopt); // change doc2 to 5, and add doc3 and doc4. // doc2 will be modified + removed = removed diff --git a/Firestore/Example/Tests/Remote/FSTRemoteEventTests.mm b/Firestore/Example/Tests/Remote/FSTRemoteEventTests.mm index b728ee94777..22b750a7a54 100644 --- a/Firestore/Example/Tests/Remote/FSTRemoteEventTests.mm +++ b/Firestore/Example/Tests/Remote/FSTRemoteEventTests.mm @@ -46,6 +46,7 @@ using firebase::firestore::remote::DocumentWatchChange; using firebase::firestore::remote::ExistenceFilter; using firebase::firestore::remote::ExistenceFilterWatchChange; +using firebase::firestore::remote::TargetChange; using firebase::firestore::remote::WatchChange; using firebase::firestore::remote::WatchChangeAggregator; using firebase::firestore::remote::WatchTargetChange; @@ -252,31 +253,29 @@ - (void)testWillAccumulateDocumentAddedAndRemovedEvents { // 'change1' and 'change2' affect six different targets XCTAssertEqual(event.targetChanges.size(), 6); - FSTTargetChange *targetChange1 = - FSTTestTargetChange(DocumentKeySet{newDoc.key}, DocumentKeySet{existingDoc.key}, - DocumentKeySet{}, _resumeToken1, NO); - XCTAssertEqualObjects(event.targetChanges.at(1), targetChange1); + TargetChange targetChange1{_resumeToken1, false, DocumentKeySet{newDoc.key}, + DocumentKeySet{existingDoc.key}, DocumentKeySet{}}; + XCTAssertTrue(event.targetChanges.at(1) == targetChange1); - FSTTargetChange *targetChange2 = FSTTestTargetChange( - DocumentKeySet{}, DocumentKeySet{existingDoc.key}, DocumentKeySet{}, _resumeToken1, NO); - XCTAssertEqualObjects(event.targetChanges.at(2), targetChange2); + TargetChange targetChange2{_resumeToken1, false, DocumentKeySet{}, + DocumentKeySet{existingDoc.key}, DocumentKeySet{}}; + XCTAssertTrue(event.targetChanges.at(2) == targetChange2); - FSTTargetChange *targetChange3 = FSTTestTargetChange( - DocumentKeySet{}, DocumentKeySet{existingDoc.key}, DocumentKeySet{}, _resumeToken1, NO); - XCTAssertEqualObjects(event.targetChanges.at(3), targetChange3); + TargetChange targetChange3{_resumeToken1, false, DocumentKeySet{}, + DocumentKeySet{existingDoc.key}, DocumentKeySet{}}; + XCTAssertTrue(event.targetChanges.at(3) == targetChange3); - FSTTargetChange *targetChange4 = - FSTTestTargetChange(DocumentKeySet{newDoc.key}, DocumentKeySet{}, - DocumentKeySet{existingDoc.key}, _resumeToken1, NO); - XCTAssertEqualObjects(event.targetChanges.at(4), targetChange4); + TargetChange targetChange4{_resumeToken1, false, DocumentKeySet{newDoc.key}, DocumentKeySet{}, + DocumentKeySet{existingDoc.key}}; + XCTAssertTrue(event.targetChanges.at(4) == targetChange4); - FSTTargetChange *targetChange5 = FSTTestTargetChange( - DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{existingDoc.key}, _resumeToken1, NO); - XCTAssertEqualObjects(event.targetChanges.at(5), targetChange5); + TargetChange targetChange5{_resumeToken1, false, DocumentKeySet{}, DocumentKeySet{}, + DocumentKeySet{existingDoc.key}}; + XCTAssertTrue(event.targetChanges.at(5) == targetChange5); - FSTTargetChange *targetChange6 = FSTTestTargetChange( - DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{existingDoc.key}, _resumeToken1, NO); - XCTAssertEqualObjects(event.targetChanges.at(6), targetChange6); + TargetChange targetChange6{_resumeToken1, false, DocumentKeySet{}, DocumentKeySet{}, + DocumentKeySet{existingDoc.key}}; + XCTAssertTrue(event.targetChanges.at(6) == targetChange6); } - (void)testWillIgnoreEventsForPendingTargets { @@ -368,9 +367,9 @@ - (void)testWillKeepResetMappingEvenWithUpdates { XCTAssertEqual(event.targetChanges.size(), 1); // Only doc3 is part of the new mapping - FSTTargetChange *expectedChange = FSTTestTargetChange( - DocumentKeySet{doc3.key}, DocumentKeySet{}, DocumentKeySet{doc1.key}, _resumeToken1, NO); - XCTAssertEqualObjects(event.targetChanges.at(1), expectedChange); + TargetChange expectedChange{_resumeToken1, false, DocumentKeySet{doc3.key}, DocumentKeySet{}, + DocumentKeySet{doc1.key}}; + XCTAssertTrue(event.targetChanges.at(1) == expectedChange); } - (void)testWillHandleSingleReset { @@ -392,9 +391,9 @@ - (void)testWillHandleSingleReset { XCTAssertEqual(event.targetChanges.size(), 1); // Reset mapping is empty - FSTTargetChange *expectedChange = - FSTTestTargetChange(DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}, [NSData data], NO); - XCTAssertEqualObjects(event.targetChanges.at(1), expectedChange); + TargetChange expectedChange{ + [NSData data], false, DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}}; + XCTAssertTrue(event.targetChanges.at(1) == expectedChange); } - (void)testWillHandleTargetAddAndRemovalInSameBatch { @@ -418,13 +417,13 @@ - (void)testWillHandleTargetAddAndRemovalInSameBatch { XCTAssertEqual(event.targetChanges.size(), 2); - FSTTargetChange *targetChange1 = FSTTestTargetChange( - DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{doc1b.key}, _resumeToken1, NO); - XCTAssertEqualObjects(event.targetChanges.at(1), targetChange1); + TargetChange targetChange1{_resumeToken1, false, DocumentKeySet{}, DocumentKeySet{}, + DocumentKeySet{doc1b.key}}; + XCTAssertTrue(event.targetChanges.at(1) == targetChange1); - FSTTargetChange *targetChange2 = FSTTestTargetChange(DocumentKeySet{}, DocumentKeySet{doc1b.key}, - DocumentKeySet{}, _resumeToken1, NO); - XCTAssertEqualObjects(event.targetChanges.at(2), targetChange2); + TargetChange targetChange2{_resumeToken1, false, DocumentKeySet{}, DocumentKeySet{doc1b.key}, + DocumentKeySet{}}; + XCTAssertTrue(event.targetChanges.at(2) == targetChange2); } - (void)testTargetCurrentChangeWillMarkTheTargetCurrent { @@ -442,9 +441,9 @@ - (void)testTargetCurrentChangeWillMarkTheTargetCurrent { XCTAssertEqual(event.documentUpdates.size(), 0); XCTAssertEqual(event.targetChanges.size(), 1); - FSTTargetChange *targetChange = - FSTTestTargetChange(DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}, _resumeToken1, YES); - XCTAssertEqualObjects(event.targetChanges.at(1), targetChange); + TargetChange targetChange1{_resumeToken1, true, DocumentKeySet{}, DocumentKeySet{}, + DocumentKeySet{}}; + XCTAssertTrue(event.targetChanges.at(1) == targetChange1); } - (void)testTargetAddedChangeWillResetPreviousState { @@ -480,15 +479,15 @@ - (void)testTargetAddedChangeWillResetPreviousState { // doc1 was before the remove, so it does not show up in the mapping. // Current was before the remove. - FSTTargetChange *targetChange1 = FSTTestTargetChange(DocumentKeySet{}, DocumentKeySet{doc2.key}, - DocumentKeySet{}, _resumeToken1, NO); - XCTAssertEqualObjects(event.targetChanges.at(1), targetChange1); + TargetChange targetChange1{_resumeToken1, false, DocumentKeySet{}, DocumentKeySet{doc2.key}, + DocumentKeySet{}}; + XCTAssertTrue(event.targetChanges.at(1) == targetChange1); // Doc1 was before the remove // Current was before the remove - FSTTargetChange *targetChange3 = FSTTestTargetChange( - DocumentKeySet{doc1.key}, DocumentKeySet{}, DocumentKeySet{doc2.key}, _resumeToken1, YES); - XCTAssertEqualObjects(event.targetChanges.at(3), targetChange3); + TargetChange targetChange3{_resumeToken1, true, DocumentKeySet{doc1.key}, DocumentKeySet{}, + DocumentKeySet{doc2.key}}; + XCTAssertTrue(event.targetChanges.at(3) == targetChange3); } - (void)testNoChangeWillStillMarkTheAffectedTargets { @@ -508,9 +507,9 @@ - (void)testNoChangeWillStillMarkTheAffectedTargets { XCTAssertEqual(event.documentUpdates.size(), 0); XCTAssertEqual(event.targetChanges.size(), 1); - FSTTargetChange *targetChange = - FSTTestTargetChange(DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}, _resumeToken1, NO); - XCTAssertEqualObjects(event.targetChanges.at(1), targetChange); + TargetChange targetChange{_resumeToken1, false, DocumentKeySet{}, DocumentKeySet{}, + DocumentKeySet{}}; + XCTAssertTrue(event.targetChanges.at(1) == targetChange); } - (void)testExistenceFilterMismatchClearsTarget { @@ -537,13 +536,13 @@ - (void)testExistenceFilterMismatchClearsTarget { XCTAssertEqual(event.targetChanges.size(), 2); - FSTTargetChange *targetChange1 = FSTTestTargetChange( - DocumentKeySet{}, DocumentKeySet{doc1.key, doc2.key}, DocumentKeySet{}, _resumeToken1, YES); - XCTAssertEqualObjects(event.targetChanges.at(1), targetChange1); + TargetChange targetChange1{_resumeToken1, true, DocumentKeySet{}, + DocumentKeySet{doc1.key, doc2.key}, DocumentKeySet{}}; + XCTAssertTrue(event.targetChanges.at(1) == targetChange1); - FSTTargetChange *targetChange2 = - FSTTestTargetChange(DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}, _resumeToken1, NO); - XCTAssertEqualObjects(event.targetChanges.at(2), targetChange2); + TargetChange targetChange2{_resumeToken1, false, DocumentKeySet{}, DocumentKeySet{}, + DocumentKeySet{}}; + XCTAssertTrue(event.targetChanges.at(2) == targetChange2); // The existence filter mismatch will remove the document from target 1, // but not synthesize a document delete. @@ -552,9 +551,9 @@ - (void)testExistenceFilterMismatchClearsTarget { event = aggregator.CreateRemoteEvent(testutil::Version(4)); - FSTTargetChange *targetChange3 = FSTTestTargetChange( - DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{doc1.key, doc2.key}, [NSData data], NO); - XCTAssertEqualObjects(event.targetChanges.at(1), targetChange3); + TargetChange targetChange3{ + [NSData data], false, DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{doc1.key, doc2.key}}; + XCTAssertTrue(event.targetChanges.at(1) == targetChange3); XCTAssertEqual(event.targetChanges.size(), 1); XCTAssertEqual(event.targetMismatches.size(), 1); @@ -590,9 +589,9 @@ - (void)testExistenceFilterMismatchRemovesCurrentChanges { XCTAssertEqual(event.targetChanges.size(), 1); - FSTTargetChange *targetChange1 = - FSTTestTargetChange(DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}, [NSData data], NO); - XCTAssertEqualObjects(event.targetChanges.at(1), targetChange1); + TargetChange targetChange1{ + [NSData data], false, DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}}; + XCTAssertTrue(event.targetChanges.at(1) == targetChange1); } - (void)testDocumentUpdate { @@ -647,10 +646,9 @@ - (void)testDocumentUpdate { // Target is unchanged XCTAssertEqual(event.targetChanges.size(), 1); - FSTTargetChange *targetChange = - FSTTestTargetChange(DocumentKeySet{doc3.key}, DocumentKeySet{updatedDoc2.key}, - DocumentKeySet{deletedDoc1.key}, _resumeToken1, NO); - XCTAssertEqualObjects(event.targetChanges.at(1), targetChange); + TargetChange targetChange1{_resumeToken1, false, DocumentKeySet{doc3.key}, + DocumentKeySet{updatedDoc2.key}, DocumentKeySet{deletedDoc1.key}}; + XCTAssertTrue(event.targetChanges.at(1) == targetChange1); } - (void)testResumeTokensHandledPerTarget { @@ -671,13 +669,13 @@ - (void)testResumeTokensHandledPerTarget { FSTRemoteEvent *event = aggregator.CreateRemoteEvent(testutil::Version(3)); XCTAssertEqual(event.targetChanges.size(), 2); - FSTTargetChange *targetChange1 = - FSTTestTargetChange(DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}, _resumeToken1, YES); - XCTAssertEqualObjects(event.targetChanges.at(1), targetChange1); + TargetChange targetChange1{_resumeToken1, true, DocumentKeySet{}, DocumentKeySet{}, + DocumentKeySet{}}; + XCTAssertTrue(event.targetChanges.at(1) == targetChange1); - FSTTargetChange *targetChange2 = - FSTTestTargetChange(DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}, resumeToken2, YES); - XCTAssertEqualObjects(event.targetChanges.at(2), targetChange2); + TargetChange targetChange2{resumeToken2, true, DocumentKeySet{}, DocumentKeySet{}, + DocumentKeySet{}}; + XCTAssertTrue(event.targetChanges.at(2) == targetChange2); } - (void)testLastResumeTokenWins { @@ -702,13 +700,13 @@ - (void)testLastResumeTokenWins { FSTRemoteEvent *event = aggregator.CreateRemoteEvent(testutil::Version(3)); XCTAssertEqual(event.targetChanges.size(), 2); - FSTTargetChange *targetChange1 = - FSTTestTargetChange(DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}, resumeToken2, YES); - XCTAssertEqualObjects(event.targetChanges.at(1), targetChange1); + TargetChange targetChange1{resumeToken2, true, DocumentKeySet{}, DocumentKeySet{}, + DocumentKeySet{}}; + XCTAssertTrue(event.targetChanges.at(1) == targetChange1); - FSTTargetChange *targetChange2 = - FSTTestTargetChange(DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}, resumeToken3, NO); - XCTAssertEqualObjects(event.targetChanges.at(2), targetChange2); + TargetChange targetChange2{resumeToken3, false, DocumentKeySet{}, DocumentKeySet{}, + DocumentKeySet{}}; + XCTAssertTrue(event.targetChanges.at(2) == targetChange2); } - (void)testSynthesizeDeletes { @@ -786,11 +784,10 @@ - (void)testSeparatesDocumentUpdates { std::move(deletedDocChange), std::move(missingDocChange))]; - FSTTargetChange *targetChange = - FSTTestTargetChange(DocumentKeySet{newDoc.key}, DocumentKeySet{existingDoc.key}, - DocumentKeySet{deletedDoc.key}, _resumeToken1, NO); + TargetChange targetChange2{_resumeToken1, false, DocumentKeySet{newDoc.key}, + DocumentKeySet{existingDoc.key}, DocumentKeySet{deletedDoc.key}}; - XCTAssertEqualObjects(event.targetChanges.at(1), targetChange); + XCTAssertTrue(event.targetChanges.at(1) == targetChange2); } - (void)testTracksLimboDocuments { diff --git a/Firestore/Example/Tests/Util/FSTHelpers.h b/Firestore/Example/Tests/Util/FSTHelpers.h index a276ef4900b..446c1fdc47c 100644 --- a/Firestore/Example/Tests/Util/FSTHelpers.h +++ b/Firestore/Example/Tests/Util/FSTHelpers.h @@ -27,7 +27,9 @@ #include "Firestore/core/src/firebase/firestore/model/field_value.h" #include "Firestore/core/src/firebase/firestore/model/resource_path.h" #include "Firestore/core/src/firebase/firestore/model/types.h" +#include "Firestore/core/src/firebase/firestore/remote/remote_event.h" #include "absl/strings/string_view.h" +#include "absl/types/optional.h" @class FIRGeoPoint; @class FSTDeleteMutation; @@ -43,7 +45,6 @@ @class FSTRemoteEvent; @class FSTSetMutation; @class FSTSortOrder; -@class FSTTargetChange; @class FIRTimestamp; @class FSTTransformMutation; @class FSTView; @@ -256,9 +257,10 @@ NSComparator FSTTestDocComparator(const absl::string_view fieldPath); FSTDocumentSet *FSTTestDocSet(NSComparator comp, NSArray *docs); /** Computes changes to the view with the docs and then applies them and returns the snapshot. */ -FSTViewSnapshot *_Nullable FSTTestApplyChanges(FSTView *view, - NSArray *docs, - FSTTargetChange *_Nullable targetChange); +FSTViewSnapshot *_Nullable FSTTestApplyChanges( + FSTView *view, + NSArray *docs, + const absl::optional &targetChange); /** Creates a set mutation for the document key at the given path. */ FSTSetMutation *FSTTestSetMutation(NSString *path, NSDictionary *values); @@ -305,17 +307,11 @@ FSTLocalViewChanges *FSTTestViewChanges(firebase::firestore::model::TargetId tar NSArray *removedKeys); /** Creates a test target change that acks all 'docs' and marks the target as CURRENT */ -FSTTargetChange *FSTTestTargetChangeAckDocuments(firebase::firestore::model::DocumentKeySet docs); +firebase::firestore::remote::TargetChange FSTTestTargetChangeAckDocuments( + firebase::firestore::model::DocumentKeySet docs); /** Creates a test target change that marks the target as CURRENT */ -FSTTargetChange *FSTTestTargetChangeMarkCurrent(); - -/** Creates a test target change. */ -FSTTargetChange *FSTTestTargetChange(firebase::firestore::model::DocumentKeySet added, - firebase::firestore::model::DocumentKeySet modified, - firebase::firestore::model::DocumentKeySet removed, - NSData *resumeToken, - BOOL current); +firebase::firestore::remote::TargetChange FSTTestTargetChangeMarkCurrent(); /** Creates a resume token to match the given snapshot version. */ NSData *_Nullable FSTTestResumeTokenFromSnapshotVersion(FSTTestSnapshotVersion watchSnapshot); diff --git a/Firestore/Example/Tests/Util/FSTHelpers.mm b/Firestore/Example/Tests/Util/FSTHelpers.mm index ffd04dd8c1b..04022769093 100644 --- a/Firestore/Example/Tests/Util/FSTHelpers.mm +++ b/Firestore/Example/Tests/Util/FSTHelpers.mm @@ -73,6 +73,7 @@ using firebase::firestore::model::TargetId; using firebase::firestore::model::TransformOperation; using firebase::firestore::remote::DocumentWatchChange; +using firebase::firestore::remote::TargetChange; using firebase::firestore::remote::WatchChangeAggregator; NS_ASSUME_NONNULL_BEGIN @@ -299,7 +300,7 @@ MaybeDocumentMap FSTTestDocUpdates(NSArray *docs) { FSTViewSnapshot *_Nullable FSTTestApplyChanges(FSTView *view, NSArray *docs, - FSTTargetChange *_Nullable targetChange) { + const absl::optional &targetChange) { return [view applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(docs)] targetChange:targetChange] .snapshot; @@ -385,32 +386,20 @@ - (nullable FSTQueryData *)queryDataForTarget:(TargetId)targetID { return aggregator.CreateRemoteEvent(doc.version); } -FSTTargetChange *FSTTestTargetChangeMarkCurrent() { - return [[FSTTargetChange alloc] initWithResumeToken:[NSData data] - current:YES - addedDocuments:DocumentKeySet {} - modifiedDocuments:DocumentKeySet {} - removedDocuments:DocumentKeySet{}]; -} - -FSTTargetChange *FSTTestTargetChangeAckDocuments(DocumentKeySet docs) { - return [[FSTTargetChange alloc] initWithResumeToken:[NSData data] - current:YES - addedDocuments:docs - modifiedDocuments:DocumentKeySet {} - removedDocuments:DocumentKeySet{}]; -} - -FSTTargetChange *FSTTestTargetChange(DocumentKeySet added, - DocumentKeySet modified, - DocumentKeySet removed, - NSData *resumeToken, - BOOL current) { - return [[FSTTargetChange alloc] initWithResumeToken:resumeToken - current:current - addedDocuments:added - modifiedDocuments:modified - removedDocuments:removed]; +TargetChange FSTTestTargetChangeMarkCurrent() { + return {[NSData data], + /*current=*/true, + /*added_documents=*/DocumentKeySet{}, + /*modified_documents=*/DocumentKeySet{}, + /*removed_documents=*/DocumentKeySet{}}; +} + +TargetChange FSTTestTargetChangeAckDocuments(DocumentKeySet docs) { + return {[NSData data], + /*current=*/true, + /*added_documents*/ std::move(docs), + /*modified_documents*/ DocumentKeySet{}, + /*removed_documents*/ DocumentKeySet{}}; } FSTRemoteEvent *FSTTestUpdateRemoteEventWithLimboTargets( diff --git a/Firestore/Source/Core/FSTSyncEngine.mm b/Firestore/Source/Core/FSTSyncEngine.mm index 57ec5434316..8e7ff023052 100644 --- a/Firestore/Source/Core/FSTSyncEngine.mm +++ b/Firestore/Source/Core/FSTSyncEngine.mm @@ -43,6 +43,7 @@ #include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" #include "Firestore/core/src/firebase/firestore/util/log.h" +#include "absl/types/optional.h" using firebase::firestore::auth::HashUser; using firebase::firestore::auth::User; @@ -57,6 +58,7 @@ using firebase::firestore::model::OnlineState; using firebase::firestore::model::SnapshotVersion; using firebase::firestore::model::TargetId; +using firebase::firestore::remote::TargetChange; using firebase::firestore::util::AsyncQueue; NS_ASSUME_NONNULL_BEGIN @@ -324,23 +326,23 @@ - (void)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent { // Update `receivedDocument` as appropriate for any limbo targets. for (const auto &entry : remoteEvent.targetChanges) { TargetId targetID = entry.first; - FSTTargetChange *change = entry.second; + const TargetChange &change = entry.second; const auto iter = _limboResolutionsByTarget.find(targetID); if (iter != _limboResolutionsByTarget.end()) { LimboResolution &limboResolution = iter->second; // Since this is a limbo resolution lookup, it's for a single document and it could be // added, modified, or removed, but not a combination. - HARD_ASSERT(change.addedDocuments.size() + change.modifiedDocuments.size() + - change.removedDocuments.size() <= + HARD_ASSERT(change.added_documents().size() + change.modified_documents().size() + + change.removed_documents().size() <= 1, "Limbo resolution for single document contains multiple changes."); - if (change.addedDocuments.size() > 0) { + if (change.added_documents().size() > 0) { limboResolution.document_received = true; - } else if (change.modifiedDocuments.size() > 0) { + } else if (change.modified_documents().size() > 0) { HARD_ASSERT(limboResolution.document_received, "Received change for limbo target document without add."); - } else if (change.removedDocuments.size() > 0) { + } else if (change.removed_documents().size() > 0) { HARD_ASSERT(limboResolution.document_received, "Received remove for limbo target document without add."); limboResolution.document_received = false; @@ -498,7 +500,7 @@ - (void)emitNewSnapshotsAndNotifyLocalStoreWithChanges:(const MaybeDocumentMap & previousChanges:viewDocChanges]; } - FSTTargetChange *_Nullable targetChange = nil; + absl::optional targetChange; if (remoteEvent) { auto it = remoteEvent.targetChanges.find(queryView.targetID); if (it != remoteEvent.targetChanges.end()) { diff --git a/Firestore/Source/Core/FSTView.h b/Firestore/Source/Core/FSTView.h index 1a25cd4ea7d..6c0620194df 100644 --- a/Firestore/Source/Core/FSTView.h +++ b/Firestore/Source/Core/FSTView.h @@ -21,10 +21,21 @@ #include "Firestore/core/src/firebase/firestore/model/document_map.h" #include "Firestore/core/src/firebase/firestore/model/types.h" +#include "absl/types/optional.h" + +namespace firebase { +namespace firestore { +namespace remote { + +class TargetChange; + +} // namespace remote +} // namespace firestore +} // namespace firebase + @class FSTDocumentSet; @class FSTDocumentViewChangeSet; @class FSTQuery; -@class FSTTargetChange; @class FSTViewSnapshot; NS_ASSUME_NONNULL_BEGIN @@ -139,8 +150,10 @@ typedef NS_ENUM(NSInteger, FSTLimboDocumentChangeType) { * @param targetChange A target change to apply for computing limbo docs and sync state. * @return A new FSTViewChange with the given docs, changes, and sync state. */ -- (FSTViewChange *)applyChangesToDocuments:(FSTViewDocumentChanges *)docChanges - targetChange:(nullable FSTTargetChange *)targetChange; +- (FSTViewChange *) + applyChangesToDocuments:(FSTViewDocumentChanges *)docChanges + targetChange: + (const absl::optional &)targetChange; /** * Applies an OnlineState change to the view, potentially generating an FSTViewChange if the diff --git a/Firestore/Source/Core/FSTView.mm b/Firestore/Source/Core/FSTView.mm index d400544bd58..0efdc79b1b3 100644 --- a/Firestore/Source/Core/FSTView.mm +++ b/Firestore/Source/Core/FSTView.mm @@ -26,6 +26,7 @@ #import "Firestore/Source/Remote/FSTRemoteEvent.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/remote/remote_event.h" #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" using firebase::firestore::core::DocumentViewChangeType; @@ -33,6 +34,7 @@ using firebase::firestore::model::DocumentKeySet; using firebase::firestore::model::MaybeDocumentMap; using firebase::firestore::model::OnlineState; +using firebase::firestore::remote::TargetChange; NS_ASSUME_NONNULL_BEGIN @@ -343,11 +345,11 @@ - (BOOL)shouldWaitForSyncedDocument:(FSTDocument *)newDoc oldDocument:(FSTDocume } - (FSTViewChange *)applyChangesToDocuments:(FSTViewDocumentChanges *)docChanges { - return [self applyChangesToDocuments:docChanges targetChange:nil]; + return [self applyChangesToDocuments:docChanges targetChange:{}]; } - (FSTViewChange *)applyChangesToDocuments:(FSTViewDocumentChanges *)docChanges - targetChange:(nullable FSTTargetChange *)targetChange { + targetChange:(const absl::optional &)targetChange { HARD_ASSERT(!docChanges.needsRefill, "Cannot apply changes that need a refill"); FSTDocumentSet *oldDocuments = self.documentSet; @@ -392,7 +394,7 @@ - (FSTViewChange *)applyChangesToDocuments:(FSTViewDocumentChanges *)docChanges - (FSTViewChange *)applyChangedOnlineState:(OnlineState)onlineState { if (self.isCurrent && onlineState == OnlineState::Offline) { // If we're offline, set `current` to NO and then call applyChanges to refresh our syncState - // and generate an FSTViewChange as appropriate. We are guaranteed to get a new FSTTargetChange + // and generate an FSTViewChange as appropriate. We are guaranteed to get a new `TargetChange` // that sets `current` back to YES once the client is back online. self.current = NO; return @@ -433,20 +435,22 @@ - (BOOL)shouldBeLimboDocumentKey:(const DocumentKey &)key { /** * Updates syncedDocuments and current based on the given change. */ -- (void)applyTargetChange:(nullable FSTTargetChange *)targetChange { - if (targetChange) { - for (const DocumentKey &key : targetChange.addedDocuments) { +- (void)applyTargetChange:(const absl::optional &)maybeTargetChange { + if (maybeTargetChange.has_value()) { + const TargetChange &target_change = maybeTargetChange.value(); + + for (const DocumentKey &key : target_change.added_documents()) { _syncedDocuments = _syncedDocuments.insert(key); } - for (const DocumentKey &key : targetChange.modifiedDocuments) { + for (const DocumentKey &key : target_change.modified_documents()) { HARD_ASSERT(_syncedDocuments.find(key) != _syncedDocuments.end(), "Modified document %s not found in view.", key.ToString()); } - for (const DocumentKey &key : targetChange.removedDocuments) { + for (const DocumentKey &key : target_change.removed_documents()) { _syncedDocuments = _syncedDocuments.erase(key); } - self.current = targetChange.current; + self.current = target_change.current(); } } diff --git a/Firestore/Source/Local/FSTLocalStore.mm b/Firestore/Source/Local/FSTLocalStore.mm index 64e452b825c..9665462f566 100644 --- a/Firestore/Source/Local/FSTLocalStore.mm +++ b/Firestore/Source/Local/FSTLocalStore.mm @@ -41,6 +41,7 @@ #include "Firestore/core/src/firebase/firestore/local/reference_set.h" #include "Firestore/core/src/firebase/firestore/local/remote_document_cache.h" #include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" +#include "Firestore/core/src/firebase/firestore/remote/remote_event.h" #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" #include "Firestore/core/src/firebase/firestore/util/log.h" @@ -59,6 +60,7 @@ using firebase::firestore::model::ListenSequenceNumber; using firebase::firestore::model::SnapshotVersion; using firebase::firestore::model::TargetId; +using firebase::firestore::remote::TargetChange; NS_ASSUME_NONNULL_BEGIN @@ -215,7 +217,7 @@ - (MaybeDocumentMap)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent { DocumentKeySet authoritativeUpdates; for (const auto &entry : remoteEvent.targetChanges) { TargetId targetID = entry.first; - FSTTargetChange *change = entry.second; + const TargetChange &change = entry.second; // Do not ref/unref unassigned targetIDs - it may lead to leaks. auto found = _targetIDs.find(targetID); @@ -232,20 +234,20 @@ - (MaybeDocumentMap)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent { // If the document is only updated while removing it from a target then watch isn't obligated // to send the absolute latest version: it can send the first version that caused the document // not to match. - for (const DocumentKey &key : change.addedDocuments) { + for (const DocumentKey &key : change.added_documents()) { authoritativeUpdates = authoritativeUpdates.insert(key); } - for (const DocumentKey &key : change.modifiedDocuments) { + for (const DocumentKey &key : change.modified_documents()) { authoritativeUpdates = authoritativeUpdates.insert(key); } - _queryCache->RemoveMatchingKeys(change.removedDocuments, targetID); - _queryCache->AddMatchingKeys(change.addedDocuments, targetID); + _queryCache->RemoveMatchingKeys(change.removed_documents(), targetID); + _queryCache->AddMatchingKeys(change.added_documents(), targetID); // Update the resume token if the change includes one. Don't clear any preexisting value. // Bump the sequence number as well, so that documents being removed now are ordered later // than documents that were previously removed from this target. - NSData *resumeToken = change.resumeToken; + NSData *resumeToken = change.resume_token(); if (resumeToken.length > 0) { FSTQueryData *oldQueryData = queryData; queryData = [queryData queryDataByReplacingSnapshotVersion:remoteEvent.snapshotVersion @@ -327,7 +329,7 @@ - (MaybeDocumentMap)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent { */ - (BOOL)shouldPersistQueryData:(FSTQueryData *)newQueryData oldQueryData:(FSTQueryData *)oldQueryData - change:(FSTTargetChange *)change { + change:(const TargetChange &)change { // Avoid clearing any existing value if (newQueryData.resumeToken.length == 0) return NO; @@ -347,8 +349,8 @@ - (BOOL)shouldPersistQueryData:(FSTQueryData *)newQueryData // worth persisting. Note that the RemoteStore keeps an in-memory view of the currently active // targets which includes the current resume token, so stream failure or user changes will still // use an up-to-date resume token regardless of what we do here. - size_t changes = change.addedDocuments.size() + change.modifiedDocuments.size() + - change.removedDocuments.size(); + size_t changes = change.added_documents().size() + change.modified_documents().size() + + change.removed_documents().size(); return changes > 0; } diff --git a/Firestore/Source/Remote/FSTRemoteEvent.h b/Firestore/Source/Remote/FSTRemoteEvent.h index 3a97a4be138..38667aa136d 100644 --- a/Firestore/Source/Remote/FSTRemoteEvent.h +++ b/Firestore/Source/Remote/FSTRemoteEvent.h @@ -33,62 +33,6 @@ NS_ASSUME_NONNULL_BEGIN -#pragma mark - FSTTargetChange - -/** - * An FSTTargetChange specifies the set of changes for a specific target as part of an - * FSTRemoteEvent. These changes track which documents are added, modified or emoved, as well as the - * target's resume token and whether the target is marked CURRENT. - * - * The actual changes *to* documents are not part of the FSTTargetChange since documents may be part - * of multiple targets. - */ -@interface FSTTargetChange : NSObject - -/** - * Creates a new target change with the given SnapshotVersion. - */ -- (instancetype)initWithResumeToken:(NSData *)resumeToken - current:(BOOL)current - addedDocuments:(firebase::firestore::model::DocumentKeySet)addedDocuments - modifiedDocuments:(firebase::firestore::model::DocumentKeySet)modifiedDocuments - removedDocuments:(firebase::firestore::model::DocumentKeySet)removedDocuments - NS_DESIGNATED_INITIALIZER; - -- (instancetype)init NS_UNAVAILABLE; - -/** - * An opaque, server-assigned token that allows watching a query to be resumed after - * disconnecting without retransmitting all the data that matches the query. The resume token - * essentially identifies a point in time from which the server should resume sending results. - */ -@property(nonatomic, strong, readonly) NSData *resumeToken; - -/** - * The "current" (synced) status of this target. Note that "current" has special meaning in the RPC - * protocol that implies that a target is both up-to-date and consistent with the rest of the watch - * stream. - */ -@property(nonatomic, assign, readonly) BOOL current; - -/** - * The set of documents that were newly assigned to this target as part of this remote event. - */ -- (const firebase::firestore::model::DocumentKeySet &)addedDocuments; - -/** - * The set of documents that were already assigned to this target but received an update during this - * remote event. - */ -- (const firebase::firestore::model::DocumentKeySet &)modifiedDocuments; - -/** - * The set of documents that were removed from this target as part of this remote event. - */ -- (const firebase::firestore::model::DocumentKeySet &)removedDocuments; - -@end - #pragma mark - FSTRemoteEvent /** @@ -100,8 +44,8 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype) initWithSnapshotVersion:(firebase::firestore::model::SnapshotVersion)snapshotVersion targetChanges: - (std::unordered_map) - targetChanges + (std::unordered_map)targetChanges targetMismatches: (std::unordered_set)targetMismatches documentUpdates: @@ -119,8 +63,8 @@ NS_ASSUME_NONNULL_BEGIN - (const firebase::firestore::model::DocumentKeySet &)limboDocumentChanges; /** A map from target to changes to the target. See TargetChange. */ -- (const std::unordered_map &) - targetChanges; +- (const std::unordered_map &)targetChanges; /** * A set of targets that is known to be inconsistent. Listens for these targets should be diff --git a/Firestore/Source/Remote/FSTRemoteEvent.mm b/Firestore/Source/Remote/FSTRemoteEvent.mm index 408c2501ce8..89ab3ebb541 100644 --- a/Firestore/Source/Remote/FSTRemoteEvent.mm +++ b/Firestore/Source/Remote/FSTRemoteEvent.mm @@ -40,81 +40,28 @@ using firebase::firestore::model::TargetId; using firebase::firestore::remote::DocumentWatchChange; using firebase::firestore::remote::ExistenceFilterWatchChange; +using firebase::firestore::remote::TargetChange; using firebase::firestore::remote::WatchTargetChange; using firebase::firestore::remote::WatchTargetChangeState; using firebase::firestore::util::Hash; NS_ASSUME_NONNULL_BEGIN -#pragma mark - FSTTargetChange - -@implementation FSTTargetChange { - DocumentKeySet _addedDocuments; - DocumentKeySet _modifiedDocuments; - DocumentKeySet _removedDocuments; -} - -- (instancetype)initWithResumeToken:(NSData *)resumeToken - current:(BOOL)current - addedDocuments:(DocumentKeySet)addedDocuments - modifiedDocuments:(DocumentKeySet)modifiedDocuments - removedDocuments:(DocumentKeySet)removedDocuments { - if (self = [super init]) { - _resumeToken = [resumeToken copy]; - _current = current; - _addedDocuments = std::move(addedDocuments); - _modifiedDocuments = std::move(modifiedDocuments); - _removedDocuments = std::move(removedDocuments); - } - return self; -} - -- (const DocumentKeySet &)addedDocuments { - return _addedDocuments; -} - -- (const DocumentKeySet &)modifiedDocuments { - return _modifiedDocuments; -} - -- (const DocumentKeySet &)removedDocuments { - return _removedDocuments; -} - -- (BOOL)isEqual:(id)other { - if (other == self) { - return YES; - } - if (![other isMemberOfClass:[FSTTargetChange class]]) { - return NO; - } - - return [self current] == [other current] && - [[self resumeToken] isEqualToData:[other resumeToken]] && - [self addedDocuments] == [other addedDocuments] && - [self modifiedDocuments] == [other modifiedDocuments] && - [self removedDocuments] == [other removedDocuments]; -} - -@end - -#pragma mark - FSTRemoteEvent - @implementation FSTRemoteEvent { SnapshotVersion _snapshotVersion; - std::unordered_map _targetChanges; + std::unordered_map _targetChanges; std::unordered_set _targetMismatches; std::unordered_map _documentUpdates; DocumentKeySet _limboDocumentChanges; } -- (instancetype) - initWithSnapshotVersion:(SnapshotVersion)snapshotVersion - targetChanges:(std::unordered_map)targetChanges - targetMismatches:(std::unordered_set)targetMismatches - documentUpdates:(std::unordered_map) +- (instancetype)initWithSnapshotVersion:(SnapshotVersion)snapshotVersion + targetChanges:(std::unordered_map)targetChanges + targetMismatches:(std::unordered_set)targetMismatches + documentUpdates: + (std::unordered_map) documentUpdates - limboDocuments:(DocumentKeySet)limboDocuments { + limboDocuments:(DocumentKeySet)limboDocuments { self = [super init]; if (self) { _snapshotVersion = std::move(snapshotVersion); @@ -134,7 +81,7 @@ @implementation FSTRemoteEvent { return _limboDocumentChanges; } -- (const std::unordered_map &)targetChanges { +- (const std::unordered_map &)targetChanges { return _targetChanges; } diff --git a/Firestore/Source/Remote/FSTRemoteStore.mm b/Firestore/Source/Remote/FSTRemoteStore.mm index 507c4c6ec6f..4e309b95724 100644 --- a/Firestore/Source/Remote/FSTRemoteStore.mm +++ b/Firestore/Source/Remote/FSTRemoteStore.mm @@ -60,6 +60,7 @@ using firebase::firestore::remote::WriteStream; using firebase::firestore::remote::DocumentWatchChange; using firebase::firestore::remote::ExistenceFilterWatchChange; +using firebase::firestore::remote::TargetChange; using firebase::firestore::remote::WatchChange; using firebase::firestore::remote::WatchChangeAggregator; using firebase::firestore::remote::WatchTargetChange; @@ -377,7 +378,8 @@ - (void)raiseWatchSnapshotWithSnapshotVersion:(const SnapshotVersion &)snapshotV // Update in-memory resume tokens. FSTLocalStore will update the persistent view of these when // applying the completed FSTRemoteEvent. for (const auto &entry : remoteEvent.targetChanges) { - NSData *resumeToken = entry.second.resumeToken; + const TargetChange &target_change = entry.second; + NSData *resumeToken = target_change.resume_token(); if (resumeToken.length > 0) { TargetId targetID = entry.first; auto found = _listenTargets.find(targetID); diff --git a/Firestore/core/src/firebase/firestore/remote/remote_event.h b/Firestore/core/src/firebase/firestore/remote/remote_event.h index e19a9badc06..e8b6f01ba9e 100644 --- a/Firestore/core/src/firebase/firestore/remote/remote_event.h +++ b/Firestore/core/src/firebase/firestore/remote/remote_event.h @@ -28,6 +28,7 @@ #include #include #include +#include #include #include "Firestore/core/src/firebase/firestore/core/view_snapshot.h" @@ -40,7 +41,6 @@ @class FSTMaybeDocument; @class FSTQueryData; @class FSTRemoteEvent; -@class FSTTargetChange; NS_ASSUME_NONNULL_BEGIN @@ -70,6 +70,84 @@ namespace firebase { namespace firestore { namespace remote { +/** + * A `TargetChange` specifies the set of changes for a specific target as part + * of an `FSTRemoteEvent`. These changes track which documents are added, + * modified or emoved, as well as the target's resume token and whether the + * target is marked CURRENT. + * + * The actual changes *to* documents are not part of the `TargetChange` since + * documents may be part of multiple targets. + */ +class TargetChange { + public: + TargetChange() = default; + + TargetChange(NSData* resume_token, + bool current, + model::DocumentKeySet added_documents, + model::DocumentKeySet modified_documents, + model::DocumentKeySet removed_documents) + : resume_token_{resume_token}, + current_{current}, + added_documents_{std::move(added_documents)}, + modified_documents_{std::move(modified_documents)}, + removed_documents_{std::move(removed_documents)} { + } + + /** + * An opaque, server-assigned token that allows watching a query to be resumed + * after disconnecting without retransmitting all the data that matches the + * query. The resume token essentially identifies a point in time from which + * the server should resume sending results. + */ + NSData* resume_token() const { + return resume_token_; + } + + /** + * The "current" (synced) status of this target. Note that "current" has + * special meaning in the RPC protocol that implies that a target is both + * up-to-date and consistent with the rest of the watch stream. + */ + bool current() const { + return current_; + } + + /** + * The set of documents that were newly assigned to this target as part of + * this remote event. + */ + const model::DocumentKeySet& added_documents() const { + return added_documents_; + } + + /** + * The set of documents that were already assigned to this target but received + * an update during this remote event. + */ + const model::DocumentKeySet& modified_documents() const { + return modified_documents_; + } + + /** + * The set of documents that were removed from this target as part of this + * remote event. + */ + const model::DocumentKeySet& removed_documents() const { + return removed_documents_; + } + + private: + NSData* resume_token_ = nil; + bool current_ = false; + model::DocumentKeySet added_documents_; + model::DocumentKeySet modified_documents_; + model::DocumentKeySet removed_documents_; +}; + +bool operator==(const TargetChange& lhs, const TargetChange& rhs); + /** Tracks the internal state of a Watch target. */ class TargetState { public: @@ -78,12 +156,12 @@ class TargetState { /** * Whether this target has been marked 'current'. * - * 'Current' has special meaning in the RPC protocol: It implies that the + * 'current' has special meaning in the RPC protocol: It implies that the * Watch backend has sent us all changes up to the point at which the target * was added and that the target is consistent with the rest of the watch * stream. */ - bool Current() const { + bool current() const { return current_; } @@ -114,7 +192,7 @@ class TargetState { * To reset the document changes after raising this snapshot, call * `ClearPendingChanges()`. */ - FSTTargetChange* ToTargetChange() const; + TargetChange ToTargetChange() const; /** Resets the document changes and sets `HasPendingChanges` to false. */ void ClearPendingChanges(); diff --git a/Firestore/core/src/firebase/firestore/remote/remote_event.mm b/Firestore/core/src/firebase/firestore/remote/remote_event.mm index d9203f532c8..787e7b596af 100644 --- a/Firestore/core/src/firebase/firestore/remote/remote_event.mm +++ b/Firestore/core/src/firebase/firestore/remote/remote_event.mm @@ -33,6 +33,16 @@ namespace firestore { namespace remote { +// TargetChange + +bool operator==(const TargetChange& lhs, const TargetChange& rhs) { + return [lhs.resume_token() isEqualToData:rhs.resume_token()] && + lhs.current() == rhs.current() && + lhs.added_documents() == rhs.added_documents() && + lhs.modified_documents() == rhs.modified_documents() && + lhs.removed_documents() == rhs.removed_documents(); +} + // TargetState TargetState::TargetState() : resume_token_{[NSData data]} { @@ -45,7 +55,7 @@ } } -FSTTargetChange* TargetState::ToTargetChange() const { +TargetChange TargetState::ToTargetChange() const { DocumentKeySet added_documents; DocumentKeySet modified_documents; DocumentKeySet removed_documents; @@ -69,12 +79,9 @@ } } - return [[FSTTargetChange alloc] - initWithResumeToken:resume_token() - current:Current() - addedDocuments:std::move(added_documents) - modifiedDocuments:std::move(modified_documents) - removedDocuments:std::move(removed_documents)]; + return TargetChange{resume_token(), current(), std::move(added_documents), + std::move(modified_documents), + std::move(removed_documents)}; } void TargetState::ClearPendingChanges() { @@ -237,7 +244,7 @@ FSTRemoteEvent* WatchChangeAggregator::CreateRemoteEvent( const SnapshotVersion& snapshot_version) { - std::unordered_map target_changes; + std::unordered_map target_changes; for (auto& entry : target_states_) { TargetId target_id = entry.first; @@ -245,7 +252,7 @@ FSTQueryData* query_data = QueryDataForActiveTarget(target_id); if (query_data) { - if (target_state.Current() && [query_data.query isDocumentQuery]) { + if (target_state.current() && [query_data.query isDocumentQuery]) { // Document queries for document that don't exist can produce an empty // result set. To update our local cache, we synthesize a document // delete if we have not previously received the document. This resolves @@ -291,12 +298,12 @@ } } - FSTRemoteEvent* remote_event = - [[FSTRemoteEvent alloc] initWithSnapshotVersion:snapshot_version - targetChanges:target_changes - targetMismatches:pending_target_resets_ - documentUpdates:pending_document_updates_ - limboDocuments:resolved_limbo_documents]; + FSTRemoteEvent* remote_event = [[FSTRemoteEvent alloc] + initWithSnapshotVersion:snapshot_version + targetChanges:target_changes + targetMismatches:std::move(pending_target_resets_) + documentUpdates:std::move(pending_document_updates_) + limboDocuments:std::move(resolved_limbo_documents)]; // Re-initialize the current state to ensure that we do not modify the // generated `RemoteEvent`. @@ -355,10 +362,10 @@ int WatchChangeAggregator::GetCurrentDocumentCountForTarget( TargetId target_id) { TargetState& target_state = EnsureTargetState(target_id); - FSTTargetChange* target_change = target_state.ToTargetChange(); + TargetChange target_change = target_state.ToTargetChange(); return ([target_metadata_provider_ remoteKeysForTarget:target_id].size() + - target_change.addedDocuments.size() - - target_change.removedDocuments.size()); + target_change.added_documents().size() - + target_change.removed_documents().size()); } void WatchChangeAggregator::RecordPendingTargetRequest(TargetId target_id) { From 8626e31febf4e12aa7cddd280c562af2e4c9203f Mon Sep 17 00:00:00 2001 From: Konstantin Varlamov Date: Tue, 29 Jan 2019 18:55:25 -0500 Subject: [PATCH 09/27] C++ migration: port `FSTRemoteEvent` (#2320) --- .../Tests/Core/FSTQueryListenerTests.mm | 1 - Firestore/Example/Tests/Core/FSTViewTests.mm | 1 - .../Tests/Integration/FSTDatastoreTests.mm | 14 +- .../Example/Tests/Local/FSTLocalStoreTests.mm | 6 +- .../Tests/Remote/FSTRemoteEventTests.mm | 254 +++++++++--------- Firestore/Example/Tests/Util/FSTHelpers.h | 18 +- Firestore/Example/Tests/Util/FSTHelpers.mm | 14 +- Firestore/Source/Core/FSTSyncEngine.h | 1 - Firestore/Source/Core/FSTSyncEngine.mm | 34 ++- Firestore/Source/Core/FSTView.mm | 1 - Firestore/Source/Local/FSTLocalStore.h | 14 +- Firestore/Source/Local/FSTLocalStore.mm | 16 +- Firestore/Source/Local/FSTLocalViewChanges.h | 1 - Firestore/Source/Remote/FSTRemoteEvent.h | 85 ------ Firestore/Source/Remote/FSTRemoteEvent.mm | 98 ------- Firestore/Source/Remote/FSTRemoteStore.h | 6 +- Firestore/Source/Remote/FSTRemoteStore.mm | 14 +- .../firebase/firestore/remote/remote_event.h | 77 +++++- .../firebase/firestore/remote/remote_event.mm | 13 +- 19 files changed, 279 insertions(+), 389 deletions(-) delete mode 100644 Firestore/Source/Remote/FSTRemoteEvent.h delete mode 100644 Firestore/Source/Remote/FSTRemoteEvent.mm diff --git a/Firestore/Example/Tests/Core/FSTQueryListenerTests.mm b/Firestore/Example/Tests/Core/FSTQueryListenerTests.mm index d0d8f45703d..83852d06701 100644 --- a/Firestore/Example/Tests/Core/FSTQueryListenerTests.mm +++ b/Firestore/Example/Tests/Core/FSTQueryListenerTests.mm @@ -22,7 +22,6 @@ #import "Firestore/Source/Core/FSTView.h" #import "Firestore/Source/Model/FSTDocument.h" #import "Firestore/Source/Model/FSTDocumentSet.h" -#import "Firestore/Source/Remote/FSTRemoteEvent.h" #import "Firestore/Source/Util/FSTAsyncQueryListener.h" #import "Firestore/Example/Tests/Util/FSTHelpers.h" diff --git a/Firestore/Example/Tests/Core/FSTViewTests.mm b/Firestore/Example/Tests/Core/FSTViewTests.mm index 5e6c432d40b..b429d55ff54 100644 --- a/Firestore/Example/Tests/Core/FSTViewTests.mm +++ b/Firestore/Example/Tests/Core/FSTViewTests.mm @@ -24,7 +24,6 @@ #import "Firestore/Source/Model/FSTDocument.h" #import "Firestore/Source/Model/FSTDocumentSet.h" #import "Firestore/Source/Model/FSTFieldValue.h" -#import "Firestore/Source/Remote/FSTRemoteEvent.h" #import "Firestore/Example/Tests/Util/FSTHelpers.h" diff --git a/Firestore/Example/Tests/Integration/FSTDatastoreTests.mm b/Firestore/Example/Tests/Integration/FSTDatastoreTests.mm index 4943d04ab34..8abb74d2fc2 100644 --- a/Firestore/Example/Tests/Integration/FSTDatastoreTests.mm +++ b/Firestore/Example/Tests/Integration/FSTDatastoreTests.mm @@ -20,6 +20,7 @@ #import #include +#include #import "Firestore/Source/API/FIRDocumentReference+Internal.h" #import "Firestore/Source/API/FSTUserDataConverter.h" @@ -29,7 +30,6 @@ #import "Firestore/Source/Model/FSTFieldValue.h" #import "Firestore/Source/Model/FSTMutation.h" #import "Firestore/Source/Model/FSTMutationBatch.h" -#import "Firestore/Source/Remote/FSTRemoteEvent.h" #import "Firestore/Source/Remote/FSTRemoteStore.h" #import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h" @@ -40,6 +40,7 @@ #include "Firestore/core/src/firebase/firestore/model/document_key.h" #include "Firestore/core/src/firebase/firestore/model/precondition.h" #include "Firestore/core/src/firebase/firestore/remote/datastore.h" +#include "Firestore/core/src/firebase/firestore/remote/remote_event.h" #include "Firestore/core/src/firebase/firestore/util/async_queue.h" #include "Firestore/core/src/firebase/firestore/util/executor_libdispatch.h" #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" @@ -57,6 +58,7 @@ using firebase::firestore::model::TargetId; using firebase::firestore::remote::Datastore; using firebase::firestore::remote::GrpcConnection; +using firebase::firestore::remote::RemoteEvent; using firebase::firestore::util::AsyncQueue; using firebase::firestore::util::ExecutorLibdispatch; @@ -79,17 +81,17 @@ - (void)expectListenEventWithDescription:(NSString *)description; @property(nonatomic, weak, nullable) XCTestCase *testCase; @property(nonatomic, strong) NSMutableArray *writeEvents; -@property(nonatomic, strong) NSMutableArray *listenEvents; @property(nonatomic, strong) NSMutableArray *writeEventExpectations; @property(nonatomic, strong) NSMutableArray *listenEventExpectations; @end -@implementation FSTRemoteStoreEventCapture +@implementation FSTRemoteStoreEventCapture { + std::vector _listenEvents; +} - (instancetype)initWithTestCase:(XCTestCase *_Nullable)testCase { if (self = [super init]) { _writeEvents = [NSMutableArray array]; - _listenEvents = [NSMutableArray array]; _testCase = testCase; _writeEventExpectations = [NSMutableArray array]; _listenEventExpectations = [NSMutableArray array]; @@ -134,8 +136,8 @@ - (DocumentKeySet)remoteKeysForTarget:(TargetId)targetId { return DocumentKeySet{}; } -- (void)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent { - [self.listenEvents addObject:remoteEvent]; +- (void)applyRemoteEvent:(const RemoteEvent &)remoteEvent { + _listenEvents.push_back(remoteEvent); XCTestExpectation *expectation = [self.listenEventExpectations objectAtIndex:0]; [self.listenEventExpectations removeObjectAtIndex:0]; [expectation fulfill]; diff --git a/Firestore/Example/Tests/Local/FSTLocalStoreTests.mm b/Firestore/Example/Tests/Local/FSTLocalStoreTests.mm index 98835fad06f..6363c652be8 100644 --- a/Firestore/Example/Tests/Local/FSTLocalStoreTests.mm +++ b/Firestore/Example/Tests/Local/FSTLocalStoreTests.mm @@ -27,7 +27,6 @@ #import "Firestore/Source/Model/FSTDocumentSet.h" #import "Firestore/Source/Model/FSTMutation.h" #import "Firestore/Source/Model/FSTMutationBatch.h" -#import "Firestore/Source/Remote/FSTRemoteEvent.h" #import "Firestore/Source/Util/FSTClasses.h" #import "Firestore/Example/Tests/Local/FSTLocalStoreTests.h" @@ -51,6 +50,7 @@ using firebase::firestore::model::MaybeDocumentMap; using firebase::firestore::model::SnapshotVersion; using firebase::firestore::model::TargetId; +using firebase::firestore::remote::RemoteEvent; using firebase::firestore::remote::WatchChangeAggregator; using firebase::firestore::remote::WatchTargetChange; using firebase::firestore::remote::WatchTargetChangeState; @@ -133,7 +133,7 @@ - (void)writeMutations:(NSArray *)mutations { _lastChanges = result.changes; } -- (void)applyRemoteEvent:(FSTRemoteEvent *)event { +- (void)applyRemoteEvent:(const RemoteEvent &)event { _lastChanges = [self.localStore applyRemoteEvent:event]; } @@ -910,7 +910,7 @@ - (void)testPersistsResumeTokens { providerWithSingleResultForKey:testutil::Key("foo/bar") targets:{targetID}]}; aggregator.HandleTargetChange(watchChange); - FSTRemoteEvent *remoteEvent = aggregator.CreateRemoteEvent(testutil::Version(1000)); + RemoteEvent remoteEvent = aggregator.CreateRemoteEvent(testutil::Version(1000)); [self applyRemoteEvent:remoteEvent]; // Stop listening so that the query should become inactive (but persistent) diff --git a/Firestore/Example/Tests/Remote/FSTRemoteEventTests.mm b/Firestore/Example/Tests/Remote/FSTRemoteEventTests.mm index 22b750a7a54..baaa627b63d 100644 --- a/Firestore/Example/Tests/Remote/FSTRemoteEventTests.mm +++ b/Firestore/Example/Tests/Remote/FSTRemoteEventTests.mm @@ -14,8 +14,6 @@ * limitations under the License. */ -#import "Firestore/Source/Remote/FSTRemoteEvent.h" - #import #include @@ -46,6 +44,7 @@ using firebase::firestore::remote::DocumentWatchChange; using firebase::firestore::remote::ExistenceFilter; using firebase::firestore::remote::ExistenceFilterWatchChange; +using firebase::firestore::remote::RemoteEvent; using firebase::firestore::remote::TargetChange; using firebase::firestore::remote::WatchChange; using firebase::firestore::remote::WatchChangeAggregator; @@ -209,7 +208,7 @@ - (void)setUp { * @param watchChanges The watch changes to apply before creating the remote event. Supported * changes are `DocumentWatchChange` and `WatchTargetChange`. */ -- (FSTRemoteEvent *) +- (RemoteEvent) remoteEventAtSnapshotVersion:(FSTTestSnapshotVersion)snapshotVersion targetMap:(std::unordered_map)targetMap outstandingResponses:(const std::unordered_map &)outstandingResponses @@ -239,43 +238,43 @@ - (void)testWillAccumulateDocumentAddedAndRemovedEvents { // with the default resume token (`_resumeToken1`). // As `existingDoc` is provided as an existing key, any updates to this document will be treated // as modifications rather than adds. - FSTRemoteEvent *event = + RemoteEvent event = [self remoteEventAtSnapshotVersion:3 targetMap:targetMap outstandingResponses:_noOutstandingResponses existingKeys:DocumentKeySet{existingDoc.key} changes:Changes(std::move(change1), std::move(change2))]; - XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); - XCTAssertEqual(event.documentUpdates.size(), 2); - XCTAssertEqualObjects(event.documentUpdates.at(existingDoc.key), existingDoc); - XCTAssertEqualObjects(event.documentUpdates.at(newDoc.key), newDoc); + XCTAssertEqual(event.snapshot_version(), testutil::Version(3)); + XCTAssertEqual(event.document_updates().size(), 2); + XCTAssertEqualObjects(event.document_updates().at(existingDoc.key), existingDoc); + XCTAssertEqualObjects(event.document_updates().at(newDoc.key), newDoc); // 'change1' and 'change2' affect six different targets - XCTAssertEqual(event.targetChanges.size(), 6); + XCTAssertEqual(event.target_changes().size(), 6); TargetChange targetChange1{_resumeToken1, false, DocumentKeySet{newDoc.key}, DocumentKeySet{existingDoc.key}, DocumentKeySet{}}; - XCTAssertTrue(event.targetChanges.at(1) == targetChange1); + XCTAssertTrue(event.target_changes().at(1) == targetChange1); TargetChange targetChange2{_resumeToken1, false, DocumentKeySet{}, DocumentKeySet{existingDoc.key}, DocumentKeySet{}}; - XCTAssertTrue(event.targetChanges.at(2) == targetChange2); + XCTAssertTrue(event.target_changes().at(2) == targetChange2); TargetChange targetChange3{_resumeToken1, false, DocumentKeySet{}, DocumentKeySet{existingDoc.key}, DocumentKeySet{}}; - XCTAssertTrue(event.targetChanges.at(3) == targetChange3); + XCTAssertTrue(event.target_changes().at(3) == targetChange3); TargetChange targetChange4{_resumeToken1, false, DocumentKeySet{newDoc.key}, DocumentKeySet{}, DocumentKeySet{existingDoc.key}}; - XCTAssertTrue(event.targetChanges.at(4) == targetChange4); + XCTAssertTrue(event.target_changes().at(4) == targetChange4); TargetChange targetChange5{_resumeToken1, false, DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{existingDoc.key}}; - XCTAssertTrue(event.targetChanges.at(5) == targetChange5); + XCTAssertTrue(event.target_changes().at(5) == targetChange5); TargetChange targetChange6{_resumeToken1, false, DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{existingDoc.key}}; - XCTAssertTrue(event.targetChanges.at(6) == targetChange6); + XCTAssertTrue(event.target_changes().at(6) == targetChange6); } - (void)testWillIgnoreEventsForPendingTargets { @@ -291,20 +290,20 @@ - (void)testWillIgnoreEventsForPendingTargets { // We're waiting for the unwatch and watch ack std::unordered_map outstandingResponses{{1, 2}}; - FSTRemoteEvent *event = + RemoteEvent event = [self remoteEventAtSnapshotVersion:3 targetMap:targetMap outstandingResponses:outstandingResponses existingKeys:DocumentKeySet {} changes:Changes(std::move(change1), std::move(change2), std::move(change3), std::move(change4))]; - XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); + XCTAssertEqual(event.snapshot_version(), testutil::Version(3)); // doc1 is ignored because it was part of an inactive target, but doc2 is in the changes // because it become active. - XCTAssertEqual(event.documentUpdates.size(), 1); - XCTAssertEqualObjects(event.documentUpdates.at(doc2.key), doc2); + XCTAssertEqual(event.document_updates().size(), 1); + XCTAssertEqualObjects(event.document_updates().at(doc2.key), doc2); - XCTAssertEqual(event.targetChanges.size(), 1); + XCTAssertEqual(event.target_changes().size(), 1); } - (void)testWillIgnoreEventsForRemovedTargets { @@ -317,18 +316,18 @@ - (void)testWillIgnoreEventsForRemovedTargets { // We're waiting for the unwatch ack std::unordered_map outstandingResponses{{1, 1}}; - FSTRemoteEvent *event = + RemoteEvent event = [self remoteEventAtSnapshotVersion:3 targetMap:targetMap outstandingResponses:outstandingResponses existingKeys:DocumentKeySet {} changes:Changes(std::move(change1), std::move(change2))]; - XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); + XCTAssertEqual(event.snapshot_version(), testutil::Version(3)); // doc1 is ignored because it was part of an inactive target - XCTAssertEqual(event.documentUpdates.size(), 0); + XCTAssertEqual(event.document_updates().size(), 0); // Target 1 is ignored because it was removed - XCTAssertEqual(event.targetChanges.size(), 0); + XCTAssertEqual(event.target_changes().size(), 0); } - (void)testWillKeepResetMappingEvenWithUpdates { @@ -350,7 +349,7 @@ - (void)testWillKeepResetMappingEvenWithUpdates { // Remove doc2 again, should not show up in reset mapping auto change5 = MakeDocChange({}, {1}, doc2.key, doc2); - FSTRemoteEvent *event = + RemoteEvent event = [self remoteEventAtSnapshotVersion:3 targetMap:targetMap outstandingResponses:_noOutstandingResponses @@ -358,18 +357,18 @@ - (void)testWillKeepResetMappingEvenWithUpdates { changes:Changes(std::move(change1), std::move(change2), std::move(change3), std::move(change4), std::move(change5))]; - XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); - XCTAssertEqual(event.documentUpdates.size(), 3); - XCTAssertEqualObjects(event.documentUpdates.at(doc1.key), doc1); - XCTAssertEqualObjects(event.documentUpdates.at(doc2.key), doc2); - XCTAssertEqualObjects(event.documentUpdates.at(doc3.key), doc3); + XCTAssertEqual(event.snapshot_version(), testutil::Version(3)); + XCTAssertEqual(event.document_updates().size(), 3); + XCTAssertEqualObjects(event.document_updates().at(doc1.key), doc1); + XCTAssertEqualObjects(event.document_updates().at(doc2.key), doc2); + XCTAssertEqualObjects(event.document_updates().at(doc3.key), doc3); - XCTAssertEqual(event.targetChanges.size(), 1); + XCTAssertEqual(event.target_changes().size(), 1); // Only doc3 is part of the new mapping TargetChange expectedChange{_resumeToken1, false, DocumentKeySet{doc3.key}, DocumentKeySet{}, DocumentKeySet{doc1.key}}; - XCTAssertTrue(event.targetChanges.at(1) == expectedChange); + XCTAssertTrue(event.target_changes().at(1) == expectedChange); } - (void)testWillHandleSingleReset { @@ -384,16 +383,16 @@ - (void)testWillHandleSingleReset { changes:{}]; aggregator.HandleTargetChange(change); - FSTRemoteEvent *event = aggregator.CreateRemoteEvent(testutil::Version(3)); + RemoteEvent event = aggregator.CreateRemoteEvent(testutil::Version(3)); - XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); - XCTAssertEqual(event.documentUpdates.size(), 0); - XCTAssertEqual(event.targetChanges.size(), 1); + XCTAssertEqual(event.snapshot_version(), testutil::Version(3)); + XCTAssertEqual(event.document_updates().size(), 0); + XCTAssertEqual(event.target_changes().size(), 1); // Reset mapping is empty TargetChange expectedChange{ [NSData data], false, DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}}; - XCTAssertTrue(event.targetChanges.at(1) == expectedChange); + XCTAssertTrue(event.target_changes().at(1) == expectedChange); } - (void)testWillHandleTargetAddAndRemovalInSameBatch { @@ -405,25 +404,25 @@ - (void)testWillHandleTargetAddAndRemovalInSameBatch { FSTDocument *doc1b = FSTTestDoc("docs/1", 1, @{@"value" : @2}, FSTDocumentStateSynced); auto change2 = MakeDocChange({2}, {1}, doc1b.key, doc1b); - FSTRemoteEvent *event = + RemoteEvent event = [self remoteEventAtSnapshotVersion:3 targetMap:targetMap outstandingResponses:_noOutstandingResponses existingKeys:DocumentKeySet{doc1a.key} changes:Changes(std::move(change1), std::move(change2))]; - XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); - XCTAssertEqual(event.documentUpdates.size(), 1); - XCTAssertEqualObjects(event.documentUpdates.at(doc1b.key), doc1b); + XCTAssertEqual(event.snapshot_version(), testutil::Version(3)); + XCTAssertEqual(event.document_updates().size(), 1); + XCTAssertEqualObjects(event.document_updates().at(doc1b.key), doc1b); - XCTAssertEqual(event.targetChanges.size(), 2); + XCTAssertEqual(event.target_changes().size(), 2); TargetChange targetChange1{_resumeToken1, false, DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{doc1b.key}}; - XCTAssertTrue(event.targetChanges.at(1) == targetChange1); + XCTAssertTrue(event.target_changes().at(1) == targetChange1); TargetChange targetChange2{_resumeToken1, false, DocumentKeySet{}, DocumentKeySet{doc1b.key}, DocumentKeySet{}}; - XCTAssertTrue(event.targetChanges.at(2) == targetChange2); + XCTAssertTrue(event.target_changes().at(2) == targetChange2); } - (void)testTargetCurrentChangeWillMarkTheTargetCurrent { @@ -431,19 +430,19 @@ - (void)testTargetCurrentChangeWillMarkTheTargetCurrent { auto change = MakeTargetChange(WatchTargetChangeState::Current, {1}, _resumeToken1); - FSTRemoteEvent *event = [self remoteEventAtSnapshotVersion:3 - targetMap:targetMap - outstandingResponses:_noOutstandingResponses - existingKeys:DocumentKeySet {} - changes:Changes(std::move(change))]; + RemoteEvent event = [self remoteEventAtSnapshotVersion:3 + targetMap:targetMap + outstandingResponses:_noOutstandingResponses + existingKeys:DocumentKeySet {} + changes:Changes(std::move(change))]; - XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); - XCTAssertEqual(event.documentUpdates.size(), 0); - XCTAssertEqual(event.targetChanges.size(), 1); + XCTAssertEqual(event.snapshot_version(), testutil::Version(3)); + XCTAssertEqual(event.document_updates().size(), 0); + XCTAssertEqual(event.target_changes().size(), 1); TargetChange targetChange1{_resumeToken1, true, DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}}; - XCTAssertTrue(event.targetChanges.at(1) == targetChange1); + XCTAssertTrue(event.target_changes().at(1) == targetChange1); } - (void)testTargetAddedChangeWillResetPreviousState { @@ -460,7 +459,7 @@ - (void)testTargetAddedChangeWillResetPreviousState { std::unordered_map outstandingResponses{{1, 2}, {2, 1}}; - FSTRemoteEvent *event = + RemoteEvent event = [self remoteEventAtSnapshotVersion:3 targetMap:targetMap outstandingResponses:outstandingResponses @@ -469,25 +468,25 @@ - (void)testTargetAddedChangeWillResetPreviousState { std::move(change3), std::move(change4), std::move(change5), std::move(change6))]; - XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); - XCTAssertEqual(event.documentUpdates.size(), 2); - XCTAssertEqualObjects(event.documentUpdates.at(doc1.key), doc1); - XCTAssertEqualObjects(event.documentUpdates.at(doc2.key), doc2); + XCTAssertEqual(event.snapshot_version(), testutil::Version(3)); + XCTAssertEqual(event.document_updates().size(), 2); + XCTAssertEqualObjects(event.document_updates().at(doc1.key), doc1); + XCTAssertEqualObjects(event.document_updates().at(doc2.key), doc2); // target 1 and 3 are affected (1 because of re-add), target 2 is not because of remove - XCTAssertEqual(event.targetChanges.size(), 2); + XCTAssertEqual(event.target_changes().size(), 2); // doc1 was before the remove, so it does not show up in the mapping. // Current was before the remove. TargetChange targetChange1{_resumeToken1, false, DocumentKeySet{}, DocumentKeySet{doc2.key}, DocumentKeySet{}}; - XCTAssertTrue(event.targetChanges.at(1) == targetChange1); + XCTAssertTrue(event.target_changes().at(1) == targetChange1); // Doc1 was before the remove // Current was before the remove TargetChange targetChange3{_resumeToken1, true, DocumentKeySet{doc1.key}, DocumentKeySet{}, DocumentKeySet{doc2.key}}; - XCTAssertTrue(event.targetChanges.at(3) == targetChange3); + XCTAssertTrue(event.target_changes().at(3) == targetChange3); } - (void)testNoChangeWillStillMarkTheAffectedTargets { @@ -501,15 +500,15 @@ - (void)testNoChangeWillStillMarkTheAffectedTargets { WatchTargetChange change{WatchTargetChangeState::NoChange, {1}, _resumeToken1}; aggregator.HandleTargetChange(change); - FSTRemoteEvent *event = aggregator.CreateRemoteEvent(testutil::Version(3)); + RemoteEvent event = aggregator.CreateRemoteEvent(testutil::Version(3)); - XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); - XCTAssertEqual(event.documentUpdates.size(), 0); - XCTAssertEqual(event.targetChanges.size(), 1); + XCTAssertEqual(event.snapshot_version(), testutil::Version(3)); + XCTAssertEqual(event.document_updates().size(), 0); + XCTAssertEqual(event.target_changes().size(), 1); TargetChange targetChange{_resumeToken1, false, DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}}; - XCTAssertTrue(event.targetChanges.at(1) == targetChange); + XCTAssertTrue(event.target_changes().at(1) == targetChange); } - (void)testExistenceFilterMismatchClearsTarget { @@ -527,22 +526,22 @@ - (void)testExistenceFilterMismatchClearsTarget { existingKeys:DocumentKeySet{doc1.key, doc2.key} changes:Changes(std::move(change1), std::move(change2), std::move(change3))]; - FSTRemoteEvent *event = aggregator.CreateRemoteEvent(testutil::Version(3)); + RemoteEvent event = aggregator.CreateRemoteEvent(testutil::Version(3)); - XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); - XCTAssertEqual(event.documentUpdates.size(), 2); - XCTAssertEqualObjects(event.documentUpdates.at(doc1.key), doc1); - XCTAssertEqualObjects(event.documentUpdates.at(doc2.key), doc2); + XCTAssertEqual(event.snapshot_version(), testutil::Version(3)); + XCTAssertEqual(event.document_updates().size(), 2); + XCTAssertEqualObjects(event.document_updates().at(doc1.key), doc1); + XCTAssertEqualObjects(event.document_updates().at(doc2.key), doc2); - XCTAssertEqual(event.targetChanges.size(), 2); + XCTAssertEqual(event.target_changes().size(), 2); TargetChange targetChange1{_resumeToken1, true, DocumentKeySet{}, DocumentKeySet{doc1.key, doc2.key}, DocumentKeySet{}}; - XCTAssertTrue(event.targetChanges.at(1) == targetChange1); + XCTAssertTrue(event.target_changes().at(1) == targetChange1); TargetChange targetChange2{_resumeToken1, false, DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}}; - XCTAssertTrue(event.targetChanges.at(2) == targetChange2); + XCTAssertTrue(event.target_changes().at(2) == targetChange2); // The existence filter mismatch will remove the document from target 1, // but not synthesize a document delete. @@ -553,11 +552,11 @@ - (void)testExistenceFilterMismatchClearsTarget { TargetChange targetChange3{ [NSData data], false, DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{doc1.key, doc2.key}}; - XCTAssertTrue(event.targetChanges.at(1) == targetChange3); + XCTAssertTrue(event.target_changes().at(1) == targetChange3); - XCTAssertEqual(event.targetChanges.size(), 1); - XCTAssertEqual(event.targetMismatches.size(), 1); - XCTAssertEqual(event.documentUpdates.size(), 0); + XCTAssertEqual(event.target_changes().size(), 1); + XCTAssertEqual(event.target_mismatches().size(), 1); + XCTAssertEqual(event.document_updates().size(), 0); } - (void)testExistenceFilterMismatchRemovesCurrentChanges { @@ -580,18 +579,18 @@ - (void)testExistenceFilterMismatchRemovesCurrentChanges { ExistenceFilterWatchChange existenceFilter{ExistenceFilter{0}, 1}; aggregator.HandleExistenceFilter(existenceFilter); - FSTRemoteEvent *event = aggregator.CreateRemoteEvent(testutil::Version(3)); + RemoteEvent event = aggregator.CreateRemoteEvent(testutil::Version(3)); - XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); - XCTAssertEqual(event.documentUpdates.size(), 1); - XCTAssertEqual(event.targetMismatches.size(), 1); - XCTAssertEqualObjects(event.documentUpdates.at(doc1.key), doc1); + XCTAssertEqual(event.snapshot_version(), testutil::Version(3)); + XCTAssertEqual(event.document_updates().size(), 1); + XCTAssertEqual(event.target_mismatches().size(), 1); + XCTAssertEqualObjects(event.document_updates().at(doc1.key), doc1); - XCTAssertEqual(event.targetChanges.size(), 1); + XCTAssertEqual(event.target_changes().size(), 1); TargetChange targetChange1{ [NSData data], false, DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}}; - XCTAssertTrue(event.targetChanges.at(1) == targetChange1); + XCTAssertTrue(event.target_changes().at(1) == targetChange1); } - (void)testDocumentUpdate { @@ -608,12 +607,12 @@ - (void)testDocumentUpdate { existingKeys:DocumentKeySet {} changes:Changes(std::move(change1), std::move(change2))]; - FSTRemoteEvent *event = aggregator.CreateRemoteEvent(testutil::Version(3)); + RemoteEvent event = aggregator.CreateRemoteEvent(testutil::Version(3)); - XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); - XCTAssertEqual(event.documentUpdates.size(), 2); - XCTAssertEqualObjects(event.documentUpdates.at(doc1.key), doc1); - XCTAssertEqualObjects(event.documentUpdates.at(doc2.key), doc2); + XCTAssertEqual(event.snapshot_version(), testutil::Version(3)); + XCTAssertEqual(event.document_updates().size(), 2); + XCTAssertEqualObjects(event.document_updates().at(doc1.key), doc1); + XCTAssertEqualObjects(event.document_updates().at(doc2.key), doc2); [_targetMetadataProvider setSyncedKeys:DocumentKeySet{doc1.key, doc2.key} forQueryData:targetMap[1]]; @@ -634,21 +633,21 @@ - (void)testDocumentUpdate { event = aggregator.CreateRemoteEvent(testutil::Version(3)); - XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); - XCTAssertEqual(event.documentUpdates.size(), 3); + XCTAssertEqual(event.snapshot_version(), testutil::Version(3)); + XCTAssertEqual(event.document_updates().size(), 3); // doc1 is replaced - XCTAssertEqualObjects(event.documentUpdates.at(doc1.key), deletedDoc1); + XCTAssertEqualObjects(event.document_updates().at(doc1.key), deletedDoc1); // doc2 is updated - XCTAssertEqualObjects(event.documentUpdates.at(doc2.key), updatedDoc2); + XCTAssertEqualObjects(event.document_updates().at(doc2.key), updatedDoc2); // doc3 is new - XCTAssertEqualObjects(event.documentUpdates.at(doc3.key), doc3); + XCTAssertEqualObjects(event.document_updates().at(doc3.key), doc3); // Target is unchanged - XCTAssertEqual(event.targetChanges.size(), 1); + XCTAssertEqual(event.target_changes().size(), 1); TargetChange targetChange1{_resumeToken1, false, DocumentKeySet{doc3.key}, DocumentKeySet{updatedDoc2.key}, DocumentKeySet{deletedDoc1.key}}; - XCTAssertTrue(event.targetChanges.at(1) == targetChange1); + XCTAssertTrue(event.target_changes().at(1) == targetChange1); } - (void)testResumeTokensHandledPerTarget { @@ -666,16 +665,16 @@ - (void)testResumeTokensHandledPerTarget { WatchTargetChange change2{WatchTargetChangeState::Current, {2}, resumeToken2}; aggregator.HandleTargetChange(change2); - FSTRemoteEvent *event = aggregator.CreateRemoteEvent(testutil::Version(3)); - XCTAssertEqual(event.targetChanges.size(), 2); + RemoteEvent event = aggregator.CreateRemoteEvent(testutil::Version(3)); + XCTAssertEqual(event.target_changes().size(), 2); TargetChange targetChange1{_resumeToken1, true, DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}}; - XCTAssertTrue(event.targetChanges.at(1) == targetChange1); + XCTAssertTrue(event.target_changes().at(1) == targetChange1); TargetChange targetChange2{resumeToken2, true, DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}}; - XCTAssertTrue(event.targetChanges.at(2) == targetChange2); + XCTAssertTrue(event.target_changes().at(2) == targetChange2); } - (void)testLastResumeTokenWins { @@ -697,16 +696,16 @@ - (void)testLastResumeTokenWins { WatchTargetChange change3{WatchTargetChangeState::NoChange, {2}, resumeToken3}; aggregator.HandleTargetChange(change3); - FSTRemoteEvent *event = aggregator.CreateRemoteEvent(testutil::Version(3)); - XCTAssertEqual(event.targetChanges.size(), 2); + RemoteEvent event = aggregator.CreateRemoteEvent(testutil::Version(3)); + XCTAssertEqual(event.target_changes().size(), 2); TargetChange targetChange1{resumeToken2, true, DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}}; - XCTAssertTrue(event.targetChanges.at(1) == targetChange1); + XCTAssertTrue(event.target_changes().at(1) == targetChange1); TargetChange targetChange2{resumeToken3, false, DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}}; - XCTAssertTrue(event.targetChanges.at(2) == targetChange2); + XCTAssertTrue(event.target_changes().at(2) == targetChange2); } - (void)testSynthesizeDeletes { @@ -714,18 +713,17 @@ - (void)testSynthesizeDeletes { DocumentKey limboKey = testutil::Key("coll/limbo"); auto resolveLimboTarget = MakeTargetChange(WatchTargetChangeState::Current, {1}); - FSTRemoteEvent *event = - [self remoteEventAtSnapshotVersion:3 - targetMap:targetMap - outstandingResponses:_noOutstandingResponses - existingKeys:DocumentKeySet {} - changes:Changes(std::move(resolveLimboTarget))]; + RemoteEvent event = [self remoteEventAtSnapshotVersion:3 + targetMap:targetMap + outstandingResponses:_noOutstandingResponses + existingKeys:DocumentKeySet {} + changes:Changes(std::move(resolveLimboTarget))]; FSTDeletedDocument *expected = [FSTDeletedDocument documentWithKey:limboKey - version:event.snapshotVersion + version:event.snapshot_version() hasCommittedMutations:NO]; - XCTAssertEqualObjects(event.documentUpdates.at(limboKey), expected); - XCTAssertTrue(event.limboDocumentChanges.contains(limboKey)); + XCTAssertEqualObjects(event.document_updates().at(limboKey), expected); + XCTAssertTrue(event.limbo_document_changes().contains(limboKey)); } - (void)testDoesntSynthesizeDeletesForWrongState { @@ -733,14 +731,14 @@ - (void)testDoesntSynthesizeDeletesForWrongState { auto wrongState = MakeTargetChange(WatchTargetChangeState::NoChange, {1}); - FSTRemoteEvent *event = [self remoteEventAtSnapshotVersion:3 - targetMap:targetMap - outstandingResponses:_noOutstandingResponses - existingKeys:DocumentKeySet {} - changes:Changes(std::move(wrongState))]; + RemoteEvent event = [self remoteEventAtSnapshotVersion:3 + targetMap:targetMap + outstandingResponses:_noOutstandingResponses + existingKeys:DocumentKeySet {} + changes:Changes(std::move(wrongState))]; - XCTAssertEqual(event.documentUpdates.size(), 0); - XCTAssertEqual(event.limboDocumentChanges.size(), 0); + XCTAssertEqual(event.document_updates().size(), 0); + XCTAssertEqual(event.limbo_document_changes().size(), 0); } - (void)testDoesntSynthesizeDeletesForExistingDoc { @@ -748,15 +746,15 @@ - (void)testDoesntSynthesizeDeletesForExistingDoc { auto hasDocument = MakeTargetChange(WatchTargetChangeState::Current, {3}); - FSTRemoteEvent *event = + RemoteEvent event = [self remoteEventAtSnapshotVersion:3 targetMap:targetMap outstandingResponses:_noOutstandingResponses existingKeys:DocumentKeySet{FSTTestDocKey(@"coll/limbo")} changes:Changes(std::move(hasDocument))]; - XCTAssertEqual(event.documentUpdates.size(), 0); - XCTAssertEqual(event.limboDocumentChanges.size(), 0); + XCTAssertEqual(event.document_updates().size(), 0); + XCTAssertEqual(event.limbo_document_changes().size(), 0); } - (void)testSeparatesDocumentUpdates { @@ -775,7 +773,7 @@ - (void)testSeparatesDocumentUpdates { FSTDeletedDocument *missingDoc = FSTTestDeletedDoc("docs/missing", 1, NO); auto missingDocChange = MakeDocChange({}, {1}, missingDoc.key, missingDoc); - FSTRemoteEvent *event = [self + RemoteEvent event = [self remoteEventAtSnapshotVersion:3 targetMap:targetMap outstandingResponses:_noOutstandingResponses @@ -787,7 +785,7 @@ - (void)testSeparatesDocumentUpdates { TargetChange targetChange2{_resumeToken1, false, DocumentKeySet{newDoc.key}, DocumentKeySet{existingDoc.key}, DocumentKeySet{deletedDoc.key}}; - XCTAssertTrue(event.targetChanges.at(1) == targetChange2); + XCTAssertTrue(event.target_changes().at(1) == targetChange2); } - (void)testTracksLimboDocuments { @@ -806,7 +804,7 @@ - (void)testTracksLimboDocuments { auto docChange3 = MakeDocChange({1}, {}, doc3.key, doc3); auto targetsChange = MakeTargetChange(WatchTargetChangeState::Current, {1, 2}); - FSTRemoteEvent *event = + RemoteEvent event = [self remoteEventAtSnapshotVersion:3 targetMap:targetMap outstandingResponses:_noOutstandingResponses @@ -814,7 +812,7 @@ - (void)testTracksLimboDocuments { changes:Changes(std::move(docChange1), std::move(docChange2), std::move(docChange3), std::move(targetsChange))]; - DocumentKeySet limboDocChanges = event.limboDocumentChanges; + DocumentKeySet limboDocChanges = event.limbo_document_changes(); // Doc1 is in both limbo and non-limbo targets, therefore not tracked as limbo XCTAssertFalse(limboDocChanges.contains(doc1.key)); // Doc2 is only in the limbo target, so is tracked as a limbo document diff --git a/Firestore/Example/Tests/Util/FSTHelpers.h b/Firestore/Example/Tests/Util/FSTHelpers.h index 446c1fdc47c..c876f2071f4 100644 --- a/Firestore/Example/Tests/Util/FSTHelpers.h +++ b/Firestore/Example/Tests/Util/FSTHelpers.h @@ -20,7 +20,6 @@ #include #import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Remote/FSTRemoteEvent.h" #include "Firestore/core/src/firebase/firestore/model/document_map.h" #include "Firestore/core/src/firebase/firestore/model/field_path.h" @@ -42,7 +41,6 @@ @class FSTLocalViewChanges; @class FSTPatchMutation; @class FSTQuery; -@class FSTRemoteEvent; @class FSTSetMutation; @class FSTSortOrder; @class FIRTimestamp; @@ -51,6 +49,16 @@ @class FSTViewSnapshot; @class FSTObjectValue; +namespace firebase { +namespace firestore { +namespace remote { + +class RemoteEvent; + +} // namespace remote +} // namespace firestore +} // namespace firebase + NS_ASSUME_NONNULL_BEGIN #define FSTAssertIsKindOfClass(value, classType) \ @@ -285,17 +293,17 @@ FSTDeleteMutation *FSTTestDeleteMutation(NSString *path); firebase::firestore::model::MaybeDocumentMap FSTTestDocUpdates(NSArray *docs); /** Creates a remote event that inserts a new document. */ -FSTRemoteEvent *FSTTestAddedRemoteEvent( +firebase::firestore::remote::RemoteEvent FSTTestAddedRemoteEvent( FSTMaybeDocument *doc, const std::vector &addedToTargets); /** Creates a remote event with changes to a document. */ -FSTRemoteEvent *FSTTestUpdateRemoteEvent( +firebase::firestore::remote::RemoteEvent FSTTestUpdateRemoteEvent( FSTMaybeDocument *doc, const std::vector &updatedInTargets, const std::vector &removedFromTargets); /** Creates a remote event with changes to a document. Allows for identifying limbo targets */ -FSTRemoteEvent *FSTTestUpdateRemoteEventWithLimboTargets( +firebase::firestore::remote::RemoteEvent FSTTestUpdateRemoteEventWithLimboTargets( FSTMaybeDocument *doc, const std::vector &updatedInTargets, const std::vector &removedFromTargets, diff --git a/Firestore/Example/Tests/Util/FSTHelpers.mm b/Firestore/Example/Tests/Util/FSTHelpers.mm index 04022769093..e5182240d19 100644 --- a/Firestore/Example/Tests/Util/FSTHelpers.mm +++ b/Firestore/Example/Tests/Util/FSTHelpers.mm @@ -38,7 +38,6 @@ #import "Firestore/Source/Model/FSTDocumentSet.h" #import "Firestore/Source/Model/FSTFieldValue.h" #import "Firestore/Source/Model/FSTMutation.h" -#import "Firestore/Source/Remote/FSTRemoteEvent.h" #include "Firestore/core/src/firebase/firestore/model/database_id.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" @@ -73,6 +72,7 @@ using firebase::firestore::model::TargetId; using firebase::firestore::model::TransformOperation; using firebase::firestore::remote::DocumentWatchChange; +using firebase::firestore::remote::RemoteEvent; using firebase::firestore::remote::TargetChange; using firebase::firestore::remote::WatchChangeAggregator; @@ -375,8 +375,8 @@ - (nullable FSTQueryData *)queryDataForTarget:(TargetId)targetID { @end -FSTRemoteEvent *FSTTestAddedRemoteEvent(FSTMaybeDocument *doc, - const std::vector &addedToTargets) { +RemoteEvent FSTTestAddedRemoteEvent(FSTMaybeDocument *doc, + const std::vector &addedToTargets) { HARD_ASSERT(![doc isKindOfClass:[FSTDocument class]] || ![(FSTDocument *)doc hasLocalMutations], "Docs from remote updates shouldn't have local changes."); DocumentWatchChange change{addedToTargets, {}, doc.key, doc}; @@ -402,7 +402,7 @@ TargetChange FSTTestTargetChangeAckDocuments(DocumentKeySet docs) { /*removed_documents*/ DocumentKeySet{}}; } -FSTRemoteEvent *FSTTestUpdateRemoteEventWithLimboTargets( +RemoteEvent FSTTestUpdateRemoteEventWithLimboTargets( FSTMaybeDocument *doc, const std::vector &updatedInTargets, const std::vector &removedFromTargets, @@ -422,9 +422,9 @@ TargetChange FSTTestTargetChangeAckDocuments(DocumentKeySet docs) { return aggregator.CreateRemoteEvent(doc.version); } -FSTRemoteEvent *FSTTestUpdateRemoteEvent(FSTMaybeDocument *doc, - const std::vector &updatedInTargets, - const std::vector &removedFromTargets) { +RemoteEvent FSTTestUpdateRemoteEvent(FSTMaybeDocument *doc, + const std::vector &updatedInTargets, + const std::vector &removedFromTargets) { return FSTTestUpdateRemoteEventWithLimboTargets(doc, updatedInTargets, removedFromTargets, {}); } diff --git a/Firestore/Source/Core/FSTSyncEngine.h b/Firestore/Source/Core/FSTSyncEngine.h index b4c4af93c22..c99b06616f4 100644 --- a/Firestore/Source/Core/FSTSyncEngine.h +++ b/Firestore/Source/Core/FSTSyncEngine.h @@ -26,7 +26,6 @@ @class FSTLocalStore; @class FSTMutation; @class FSTQuery; -@class FSTRemoteEvent; @class FSTRemoteStore; @class FSTViewSnapshot; diff --git a/Firestore/Source/Core/FSTSyncEngine.mm b/Firestore/Source/Core/FSTSyncEngine.mm index 8e7ff023052..86794d0fec5 100644 --- a/Firestore/Source/Core/FSTSyncEngine.mm +++ b/Firestore/Source/Core/FSTSyncEngine.mm @@ -33,7 +33,6 @@ #import "Firestore/Source/Model/FSTDocument.h" #import "Firestore/Source/Model/FSTDocumentSet.h" #import "Firestore/Source/Model/FSTMutationBatch.h" -#import "Firestore/Source/Remote/FSTRemoteEvent.h" #include "Firestore/core/src/firebase/firestore/auth/user.h" #include "Firestore/core/src/firebase/firestore/core/target_id_generator.h" @@ -41,6 +40,7 @@ #include "Firestore/core/src/firebase/firestore/model/document_key.h" #include "Firestore/core/src/firebase/firestore/model/document_map.h" #include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" +#include "Firestore/core/src/firebase/firestore/remote/remote_event.h" #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" #include "Firestore/core/src/firebase/firestore/util/log.h" #include "absl/types/optional.h" @@ -58,6 +58,7 @@ using firebase::firestore::model::OnlineState; using firebase::firestore::model::SnapshotVersion; using firebase::firestore::model::TargetId; +using firebase::firestore::remote::RemoteEvent; using firebase::firestore::remote::TargetChange; using firebase::firestore::util::AsyncQueue; @@ -253,7 +254,7 @@ - (void)writeMutations:(NSArray *)mutations FSTLocalWriteResult *result = [self.localStore locallyWriteMutations:mutations]; [self addMutationCompletionBlock:completion batchID:result.batchID]; - [self emitNewSnapshotsAndNotifyLocalStoreWithChanges:result.changes remoteEvent:nil]; + [self emitNewSnapshotsAndNotifyLocalStoreWithChanges:result.changes remoteEvent:absl::nullopt]; [self.remoteStore fillWritePipeline]; } @@ -320,11 +321,11 @@ - (void)transactionWithRetries:(int)retries }); } -- (void)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent { +- (void)applyRemoteEvent:(const RemoteEvent &)remoteEvent { [self assertDelegateExistsForSelector:_cmd]; // Update `receivedDocument` as appropriate for any limbo targets. - for (const auto &entry : remoteEvent.targetChanges) { + for (const auto &entry : remoteEvent.target_changes()) { TargetId targetID = entry.first; const TargetChange &change = entry.second; const auto iter = _limboResolutionsByTarget.find(targetID); @@ -392,13 +393,8 @@ - (void)rejectListenWithTargetID:(const TargetId)targetID error:(NSError *)error version:SnapshotVersion::None() hasCommittedMutations:NO]; DocumentKeySet limboDocuments = DocumentKeySet{doc.key}; - FSTRemoteEvent *event = [[FSTRemoteEvent alloc] initWithSnapshotVersion:SnapshotVersion::None() - targetChanges:{} - targetMismatches:{} - documentUpdates:{ - { limboKey, doc } - } - limboDocuments:std::move(limboDocuments)]; + RemoteEvent event{SnapshotVersion::None(), /*target_changes=*/{}, /*target_mismatches=*/{}, + /*document_updates=*/{{limboKey, doc}}, std::move(limboDocuments)}; [self applyRemoteEvent:event]; } else { auto found = _queryViewsByTarget.find(targetID); @@ -424,7 +420,7 @@ - (void)applySuccessfulWriteWithResult:(FSTMutationBatchResult *)batchResult { [self processUserCallbacksForBatchID:batchResult.batch.batchID error:nil]; MaybeDocumentMap changes = [self.localStore acknowledgeBatchWithResult:batchResult]; - [self emitNewSnapshotsAndNotifyLocalStoreWithChanges:changes remoteEvent:nil]; + [self emitNewSnapshotsAndNotifyLocalStoreWithChanges:changes remoteEvent:absl::nullopt]; } - (void)rejectFailedWriteWithBatchID:(BatchId)batchID error:(NSError *)error { @@ -441,7 +437,7 @@ - (void)rejectFailedWriteWithBatchID:(BatchId)batchID error:(NSError *)error { // consistently happen before listen events. [self processUserCallbacksForBatchID:batchID error:error]; - [self emitNewSnapshotsAndNotifyLocalStoreWithChanges:changes remoteEvent:nil]; + [self emitNewSnapshotsAndNotifyLocalStoreWithChanges:changes remoteEvent:absl::nullopt]; } - (void)processUserCallbacksForBatchID:(BatchId)batchID error:(NSError *_Nullable)error { @@ -483,7 +479,8 @@ - (void)removeAndCleanupQuery:(FSTQueryView *)queryView { * Computes a new snapshot from the changes and calls the registered callback with the new snapshot. */ - (void)emitNewSnapshotsAndNotifyLocalStoreWithChanges:(const MaybeDocumentMap &)changes - remoteEvent:(FSTRemoteEvent *_Nullable)remoteEvent { + remoteEvent:(const absl::optional &) + maybeRemoteEvent { NSMutableArray *newSnapshots = [NSMutableArray array]; NSMutableArray *documentChangesInAllViews = [NSMutableArray array]; @@ -501,9 +498,10 @@ - (void)emitNewSnapshotsAndNotifyLocalStoreWithChanges:(const MaybeDocumentMap & } absl::optional targetChange; - if (remoteEvent) { - auto it = remoteEvent.targetChanges.find(queryView.targetID); - if (it != remoteEvent.targetChanges.end()) { + if (maybeRemoteEvent.has_value()) { + const RemoteEvent &remoteEvent = maybeRemoteEvent.value(); + auto it = remoteEvent.target_changes().find(queryView.targetID); + if (it != remoteEvent.target_changes().end()) { targetChange = it->second; } } @@ -593,7 +591,7 @@ - (void)credentialDidChangeWithUser:(const firebase::firestore::auth::User &)use if (userChanged) { // Notify local store and emit any resulting events from swapping out the mutation queue. MaybeDocumentMap changes = [self.localStore userDidChange:user]; - [self emitNewSnapshotsAndNotifyLocalStoreWithChanges:changes remoteEvent:nil]; + [self emitNewSnapshotsAndNotifyLocalStoreWithChanges:changes remoteEvent:absl::nullopt]; } // Notify remote store so it can restart its streams. diff --git a/Firestore/Source/Core/FSTView.mm b/Firestore/Source/Core/FSTView.mm index 0efdc79b1b3..86b9cde8303 100644 --- a/Firestore/Source/Core/FSTView.mm +++ b/Firestore/Source/Core/FSTView.mm @@ -23,7 +23,6 @@ #import "Firestore/Source/Model/FSTDocument.h" #import "Firestore/Source/Model/FSTDocumentSet.h" #import "Firestore/Source/Model/FSTFieldValue.h" -#import "Firestore/Source/Remote/FSTRemoteEvent.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" #include "Firestore/core/src/firebase/firestore/remote/remote_event.h" diff --git a/Firestore/Source/Local/FSTLocalStore.h b/Firestore/Source/Local/FSTLocalStore.h index 60f8d2ee4a7..21f05b68e4c 100644 --- a/Firestore/Source/Local/FSTLocalStore.h +++ b/Firestore/Source/Local/FSTLocalStore.h @@ -25,6 +25,16 @@ #include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" #include "Firestore/core/src/firebase/firestore/model/types.h" +namespace firebase { +namespace firestore { +namespace remote { + +class RemoteEvent; + +} // namespace remote +} // namespace firestore +} // namespace firebase + @class FSTLocalViewChanges; @class FSTLocalWriteResult; @class FSTMutation; @@ -32,7 +42,6 @@ @class FSTMutationBatchResult; @class FSTQuery; @class FSTQueryData; -@class FSTRemoteEvent; @protocol FSTPersistence; NS_ASSUME_NONNULL_BEGIN @@ -147,7 +156,8 @@ NS_ASSUME_NONNULL_BEGIN * * LocalDocuments are re-calculated if there are remaining mutations in the queue. */ -- (firebase::firestore::model::MaybeDocumentMap)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent; +- (firebase::firestore::model::MaybeDocumentMap)applyRemoteEvent: + (const firebase::firestore::remote::RemoteEvent &)remoteEvent; /** * Returns the keys of the documents that are associated with the given targetID in the remote diff --git a/Firestore/Source/Local/FSTLocalStore.mm b/Firestore/Source/Local/FSTLocalStore.mm index 9665462f566..e157702a720 100644 --- a/Firestore/Source/Local/FSTLocalStore.mm +++ b/Firestore/Source/Local/FSTLocalStore.mm @@ -32,7 +32,6 @@ #import "Firestore/Source/Model/FSTDocument.h" #import "Firestore/Source/Model/FSTMutation.h" #import "Firestore/Source/Model/FSTMutationBatch.h" -#import "Firestore/Source/Remote/FSTRemoteEvent.h" #include "Firestore/core/src/firebase/firestore/auth/user.h" #include "Firestore/core/src/firebase/firestore/core/target_id_generator.h" @@ -60,6 +59,7 @@ using firebase::firestore::model::ListenSequenceNumber; using firebase::firestore::model::SnapshotVersion; using firebase::firestore::model::TargetId; +using firebase::firestore::remote::RemoteEvent; using firebase::firestore::remote::TargetChange; NS_ASSUME_NONNULL_BEGIN @@ -209,13 +209,13 @@ - (void)setLastStreamToken:(nullable NSData *)streamToken { return self.queryCache->GetLastRemoteSnapshotVersion(); } -- (MaybeDocumentMap)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent { +- (MaybeDocumentMap)applyRemoteEvent:(const RemoteEvent &)remoteEvent { return self.persistence.run("Apply remote event", [&]() -> MaybeDocumentMap { // TODO(gsoltis): move the sequence number into the reference delegate. ListenSequenceNumber sequenceNumber = self.persistence.currentSequenceNumber; DocumentKeySet authoritativeUpdates; - for (const auto &entry : remoteEvent.targetChanges) { + for (const auto &entry : remoteEvent.target_changes()) { TargetId targetID = entry.first; const TargetChange &change = entry.second; @@ -250,7 +250,7 @@ - (MaybeDocumentMap)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent { NSData *resumeToken = change.resume_token(); if (resumeToken.length > 0) { FSTQueryData *oldQueryData = queryData; - queryData = [queryData queryDataByReplacingSnapshotVersion:remoteEvent.snapshotVersion + queryData = [queryData queryDataByReplacingSnapshotVersion:remoteEvent.snapshot_version() resumeToken:resumeToken sequenceNumber:sequenceNumber]; _targetIDs[targetID] = queryData; @@ -262,16 +262,16 @@ - (MaybeDocumentMap)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent { } MaybeDocumentMap changedDocs; - const DocumentKeySet &limboDocuments = remoteEvent.limboDocumentChanges; + const DocumentKeySet &limboDocuments = remoteEvent.limbo_document_changes(); DocumentKeySet updatedKeys; - for (const auto &kv : remoteEvent.documentUpdates) { + for (const auto &kv : remoteEvent.document_updates()) { updatedKeys = updatedKeys.insert(kv.first); } // Each loop iteration only affects its "own" doc, so it's safe to get all the remote // documents in advance in a single call. MaybeDocumentMap existingDocs = _remoteDocumentCache->GetAll(updatedKeys); - for (const auto &kv : remoteEvent.documentUpdates) { + for (const auto &kv : remoteEvent.document_updates()) { const DocumentKey &key = kv.first; FSTMaybeDocument *doc = kv.second; FSTMaybeDocument *existingDoc = nil; @@ -305,7 +305,7 @@ - (MaybeDocumentMap)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent { // events when we get permission denied errors while trying to resolve the state of a locally // cached document that is in limbo. const SnapshotVersion &lastRemoteVersion = _queryCache->GetLastRemoteSnapshotVersion(); - const SnapshotVersion &remoteVersion = remoteEvent.snapshotVersion; + const SnapshotVersion &remoteVersion = remoteEvent.snapshot_version(); if (remoteVersion != SnapshotVersion::None()) { HARD_ASSERT(remoteVersion >= lastRemoteVersion, "Watch stream reverted to previous snapshot?? (%s < %s)", diff --git a/Firestore/Source/Local/FSTLocalViewChanges.h b/Firestore/Source/Local/FSTLocalViewChanges.h index dcc20055adf..fdcc3b1c960 100644 --- a/Firestore/Source/Local/FSTLocalViewChanges.h +++ b/Firestore/Source/Local/FSTLocalViewChanges.h @@ -22,7 +22,6 @@ @class FSTDocumentSet; @class FSTMutation; @class FSTQuery; -@class FSTRemoteEvent; @class FSTViewSnapshot; NS_ASSUME_NONNULL_BEGIN diff --git a/Firestore/Source/Remote/FSTRemoteEvent.h b/Firestore/Source/Remote/FSTRemoteEvent.h deleted file mode 100644 index 38667aa136d..00000000000 --- a/Firestore/Source/Remote/FSTRemoteEvent.h +++ /dev/null @@ -1,85 +0,0 @@ -/* - * 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 - -#include -#include -#include -#include - -#include "Firestore/core/src/firebase/firestore/model/document_key.h" -#include "Firestore/core/src/firebase/firestore/model/document_key_set.h" -#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" -#include "Firestore/core/src/firebase/firestore/model/types.h" -#include "Firestore/core/src/firebase/firestore/remote/remote_event.h" -#include "Firestore/core/src/firebase/firestore/remote/watch_change.h" - -@class FSTMaybeDocument; -@class FSTQueryData; - -NS_ASSUME_NONNULL_BEGIN - -#pragma mark - FSTRemoteEvent - -/** - * An event from the RemoteStore. It is split into targetChanges (changes to the state or the set - * of documents in our watched targets) and documentUpdates (changes to the actual documents). - */ -@interface FSTRemoteEvent : NSObject - -- (instancetype) - initWithSnapshotVersion:(firebase::firestore::model::SnapshotVersion)snapshotVersion - targetChanges: - (std::unordered_map)targetChanges - targetMismatches: - (std::unordered_set)targetMismatches - documentUpdates: - (std::unordered_map)documentUpdates - limboDocuments:(firebase::firestore::model::DocumentKeySet)limboDocuments; - -/** The snapshot version this event brings us up to. */ -- (const firebase::firestore::model::SnapshotVersion &)snapshotVersion; - -/** - * A set of which document updates are due only to limbo resolution targets. - */ -- (const firebase::firestore::model::DocumentKeySet &)limboDocumentChanges; - -/** A map from target to changes to the target. See TargetChange. */ -- (const std::unordered_map &)targetChanges; - -/** - * A set of targets that is known to be inconsistent. Listens for these targets should be - * re-established without resume tokens. - */ -- (const std::unordered_set &)targetMismatches; - -/** - * A set of which documents have changed or been deleted, along with the doc's new values (if not - * deleted). - */ -- (const std::unordered_map &)documentUpdates; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Remote/FSTRemoteEvent.mm b/Firestore/Source/Remote/FSTRemoteEvent.mm deleted file mode 100644 index 89ab3ebb541..00000000000 --- a/Firestore/Source/Remote/FSTRemoteEvent.mm +++ /dev/null @@ -1,98 +0,0 @@ -/* - * 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 "Firestore/Source/Remote/FSTRemoteEvent.h" - -#include -#include -#include -#include - -#import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTViewSnapshot.h" -#import "Firestore/Source/Local/FSTQueryData.h" -#import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Util/FSTClasses.h" - -#include "Firestore/core/src/firebase/firestore/remote/watch_change.h" -#include "Firestore/core/src/firebase/firestore/util/hard_assert.h" -#include "Firestore/core/src/firebase/firestore/util/hashing.h" -#include "Firestore/core/src/firebase/firestore/util/log.h" - -using firebase::firestore::core::DocumentViewChangeType; -using firebase::firestore::model::DocumentKey; -using firebase::firestore::model::DocumentKeyHash; -using firebase::firestore::model::DocumentKeySet; -using firebase::firestore::model::SnapshotVersion; -using firebase::firestore::model::TargetId; -using firebase::firestore::remote::DocumentWatchChange; -using firebase::firestore::remote::ExistenceFilterWatchChange; -using firebase::firestore::remote::TargetChange; -using firebase::firestore::remote::WatchTargetChange; -using firebase::firestore::remote::WatchTargetChangeState; -using firebase::firestore::util::Hash; - -NS_ASSUME_NONNULL_BEGIN - -@implementation FSTRemoteEvent { - SnapshotVersion _snapshotVersion; - std::unordered_map _targetChanges; - std::unordered_set _targetMismatches; - std::unordered_map _documentUpdates; - DocumentKeySet _limboDocumentChanges; -} - -- (instancetype)initWithSnapshotVersion:(SnapshotVersion)snapshotVersion - targetChanges:(std::unordered_map)targetChanges - targetMismatches:(std::unordered_set)targetMismatches - documentUpdates: - (std::unordered_map) - documentUpdates - limboDocuments:(DocumentKeySet)limboDocuments { - self = [super init]; - if (self) { - _snapshotVersion = std::move(snapshotVersion); - _targetChanges = std::move(targetChanges); - _targetMismatches = std::move(targetMismatches); - _documentUpdates = std::move(documentUpdates); - _limboDocumentChanges = std::move(limboDocuments); - } - return self; -} - -- (const SnapshotVersion &)snapshotVersion { - return _snapshotVersion; -} - -- (const DocumentKeySet &)limboDocumentChanges { - return _limboDocumentChanges; -} - -- (const std::unordered_map &)targetChanges { - return _targetChanges; -} - -- (const std::unordered_map &)documentUpdates { - return _documentUpdates; -} - -- (const std::unordered_set &)targetMismatches { - return _targetMismatches; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Remote/FSTRemoteStore.h b/Firestore/Source/Remote/FSTRemoteStore.h index 267c81e5ef9..5ccb9956647 100644 --- a/Firestore/Source/Remote/FSTRemoteStore.h +++ b/Firestore/Source/Remote/FSTRemoteStore.h @@ -18,18 +18,16 @@ #include -#import "Firestore/Source/Remote/FSTRemoteEvent.h" - #include "Firestore/core/src/firebase/firestore/auth/user.h" #include "Firestore/core/src/firebase/firestore/model/types.h" #include "Firestore/core/src/firebase/firestore/remote/datastore.h" +#include "Firestore/core/src/firebase/firestore/remote/remote_event.h" #include "Firestore/core/src/firebase/firestore/util/async_queue.h" @class FSTLocalStore; @class FSTMutationBatch; @class FSTMutationBatchResult; @class FSTQueryData; -@class FSTRemoteEvent; @class FSTTransaction; NS_ASSUME_NONNULL_BEGIN @@ -47,7 +45,7 @@ NS_ASSUME_NONNULL_BEGIN * any pending mutation batches that would become visible because of the snapshot version the * remote event contains. */ -- (void)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent; +- (void)applyRemoteEvent:(const firebase::firestore::remote::RemoteEvent &)remoteEvent; /** * Rejects the listen for the given targetID. This can be triggered by the backend for any active diff --git a/Firestore/Source/Remote/FSTRemoteStore.mm b/Firestore/Source/Remote/FSTRemoteStore.mm index 4e309b95724..7c4bcd4e3e4 100644 --- a/Firestore/Source/Remote/FSTRemoteStore.mm +++ b/Firestore/Source/Remote/FSTRemoteStore.mm @@ -29,7 +29,6 @@ #import "Firestore/Source/Model/FSTMutation.h" #import "Firestore/Source/Model/FSTMutationBatch.h" #import "Firestore/Source/Remote/FSTOnlineStateTracker.h" -#import "Firestore/Source/Remote/FSTRemoteEvent.h" #import "Firestore/Source/Remote/FSTStream.h" #include "Firestore/core/src/firebase/firestore/auth/user.h" @@ -60,6 +59,7 @@ using firebase::firestore::remote::WriteStream; using firebase::firestore::remote::DocumentWatchChange; using firebase::firestore::remote::ExistenceFilterWatchChange; +using firebase::firestore::remote::RemoteEvent; using firebase::firestore::remote::TargetChange; using firebase::firestore::remote::WatchChange; using firebase::firestore::remote::WatchChangeAggregator; @@ -366,18 +366,18 @@ - (void)watchStreamWasInterruptedWithError:(const Status &)error { } /** - * Takes a batch of changes from the Datastore, repackages them as a RemoteEvent, and passes that + * Takes a batch of changes from the Datastore, repackages them as a `RemoteEvent`, and passes that * on to the SyncEngine. */ - (void)raiseWatchSnapshotWithSnapshotVersion:(const SnapshotVersion &)snapshotVersion { HARD_ASSERT(snapshotVersion != SnapshotVersion::None(), "Can't raise event for unknown SnapshotVersion"); - FSTRemoteEvent *remoteEvent = _watchChangeAggregator->CreateRemoteEvent(snapshotVersion); + RemoteEvent remoteEvent = _watchChangeAggregator->CreateRemoteEvent(snapshotVersion); - // Update in-memory resume tokens. FSTLocalStore will update the persistent view of these when - // applying the completed FSTRemoteEvent. - for (const auto &entry : remoteEvent.targetChanges) { + // Update in-memory resume tokens. `FSTLocalStore` will update the persistent view of these when + // applying the completed `RemoteEvent`. + for (const auto &entry : remoteEvent.target_changes()) { const TargetChange &target_change = entry.second; NSData *resumeToken = target_change.resume_token(); if (resumeToken.length > 0) { @@ -396,7 +396,7 @@ - (void)raiseWatchSnapshotWithSnapshotVersion:(const SnapshotVersion &)snapshotV // Re-establish listens for the targets that have been invalidated by existence filter // mismatches. - for (TargetId targetID : remoteEvent.targetMismatches) { + for (TargetId targetID : remoteEvent.target_mismatches()) { auto found = _listenTargets.find(targetID); if (found == _listenTargets.end()) { // A watched target might have been removed already. diff --git a/Firestore/core/src/firebase/firestore/remote/remote_event.h b/Firestore/core/src/firebase/firestore/remote/remote_event.h index e8b6f01ba9e..e154b9ee623 100644 --- a/Firestore/core/src/firebase/firestore/remote/remote_event.h +++ b/Firestore/core/src/firebase/firestore/remote/remote_event.h @@ -40,7 +40,6 @@ @class FSTMaybeDocument; @class FSTQueryData; -@class FSTRemoteEvent; NS_ASSUME_NONNULL_BEGIN @@ -72,8 +71,8 @@ namespace remote { /** * A `TargetChange` specifies the set of changes for a specific target as part - * of an `FSTRemoteEvent`. These changes track which documents are added, - * modified or emoved, as well as the target's resume token and whether the + * of an `RemoteEvent`. These changes track which documents are added, + * modified or removed, as well as the target's resume token and whether the * target is marked CURRENT. * * The actual changes *to* documents are not part of the `TargetChange` since @@ -234,6 +233,75 @@ class TargetState { bool has_pending_changes_ = true; }; +/** + * An event from the RemoteStore. It is split into `TargetChanges` (changes to + * the state or the set of documents in our watched targets) and + * `DocumentUpdates` (changes to the actual documents). + */ +class RemoteEvent { + public: + RemoteEvent(model::SnapshotVersion snapshot_version, + std::unordered_map target_changes, + std::unordered_set target_mismatches, + std::unordered_map document_updates, + model::DocumentKeySet limbo_document_changes) + : snapshot_version_{snapshot_version}, + target_changes_{std::move(target_changes)}, + target_mismatches_{std::move(target_mismatches)}, + document_updates_{std::move(document_updates)}, + limbo_document_changes_{std::move(limbo_document_changes)} { + } + + /** The snapshot version this event brings us up to. */ + const model::SnapshotVersion& snapshot_version() const { + return snapshot_version_; + } + + /** A map from target to changes to the target. See `TargetChange`. */ + const std::unordered_map& target_changes() + const { + return target_changes_; + } + + /** + * A set of targets that is known to be inconsistent. Listens for these + * targets should be re-established without resume tokens. + */ + const std::unordered_set& target_mismatches() const { + return target_mismatches_; + } + + /** + * A set of which documents have changed or been deleted, along with the doc's + * new values (if not deleted). + */ + const std::unordered_map& + document_updates() const { + return document_updates_; + } + + /** + * A set of which document updates are due only to limbo resolution targets. + */ + const model::DocumentKeySet& limbo_document_changes() const { + return limbo_document_changes_; + } + + private: + model::SnapshotVersion snapshot_version_; + std::unordered_map target_changes_; + std::unordered_set target_mismatches_; + std::unordered_map + document_updates_; + model::DocumentKeySet limbo_document_changes_; +}; + /** * A helper class to accumulate watch changes into a `RemoteEvent` and other * target information. @@ -268,8 +336,7 @@ class WatchChangeAggregator { * taken from the initializer. Resets the accumulated changes before * returning. */ - FSTRemoteEvent* CreateRemoteEvent( - const model::SnapshotVersion& snapshot_version); + RemoteEvent CreateRemoteEvent(const model::SnapshotVersion& snapshot_version); /** Removes the in-memory state for the provided target. */ void RemoveTarget(model::TargetId target_id); diff --git a/Firestore/core/src/firebase/firestore/remote/remote_event.mm b/Firestore/core/src/firebase/firestore/remote/remote_event.mm index 787e7b596af..fd4355ddab2 100644 --- a/Firestore/core/src/firebase/firestore/remote/remote_event.mm +++ b/Firestore/core/src/firebase/firestore/remote/remote_event.mm @@ -21,7 +21,6 @@ #import "Firestore/Source/Core/FSTQuery.h" #import "Firestore/Source/Local/FSTQueryData.h" #import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Remote/FSTRemoteEvent.h" using firebase::firestore::core::DocumentViewChangeType; using firebase::firestore::model::DocumentKey; @@ -242,7 +241,7 @@ } } -FSTRemoteEvent* WatchChangeAggregator::CreateRemoteEvent( +RemoteEvent WatchChangeAggregator::CreateRemoteEvent( const SnapshotVersion& snapshot_version) { std::unordered_map target_changes; @@ -298,12 +297,10 @@ } } - FSTRemoteEvent* remote_event = [[FSTRemoteEvent alloc] - initWithSnapshotVersion:snapshot_version - targetChanges:target_changes - targetMismatches:std::move(pending_target_resets_) - documentUpdates:std::move(pending_document_updates_) - limboDocuments:std::move(resolved_limbo_documents)]; + RemoteEvent remote_event{snapshot_version, std::move(target_changes), + std::move(pending_target_resets_), + std::move(pending_document_updates_), + std::move(resolved_limbo_documents)}; // Re-initialize the current state to ensure that we do not modify the // generated `RemoteEvent`. From 388f267d6ea035bd5766e27396cf36dbfd590c8e Mon Sep 17 00:00:00 2001 From: Greg Soltis Date: Tue, 29 Jan 2019 16:01:59 -0800 Subject: [PATCH 10/27] Remove FSTMutationQueue (#2319) * Remove FSTMutationQueue --- .../Local/FSTLRUGarbageCollectorTests.mm | 9 +- .../Tests/Local/FSTLevelDBMigrationsTests.mm | 1 - .../Local/FSTLevelDBMutationQueueTests.mm | 4 +- .../Local/FSTMemoryMutationQueueTests.mm | 4 +- .../Source/Local/FSTLRUGarbageCollector.mm | 1 - Firestore/Source/Local/FSTLevelDB.mm | 13 +- .../Source/Local/FSTLevelDBMutationQueue.h | 52 ----- .../Source/Local/FSTLevelDBMutationQueue.mm | 181 ------------------ .../Source/Local/FSTLocalDocumentsView.h | 5 +- .../Source/Local/FSTLocalDocumentsView.mm | 36 ++-- Firestore/Source/Local/FSTLocalStore.mm | 52 ++--- .../Source/Local/FSTMemoryMutationQueue.h | 41 ---- .../Source/Local/FSTMemoryMutationQueue.mm | 152 --------------- .../Source/Local/FSTMemoryPersistence.mm | 29 +-- Firestore/Source/Local/FSTMutationQueue.h | 133 ------------- Firestore/Source/Local/FSTPersistence.h | 7 +- 16 files changed, 85 insertions(+), 635 deletions(-) delete mode 100644 Firestore/Source/Local/FSTLevelDBMutationQueue.h delete mode 100644 Firestore/Source/Local/FSTLevelDBMutationQueue.mm delete mode 100644 Firestore/Source/Local/FSTMemoryMutationQueue.h delete mode 100644 Firestore/Source/Local/FSTMemoryMutationQueue.mm delete mode 100644 Firestore/Source/Local/FSTMutationQueue.h diff --git a/Firestore/Example/Tests/Local/FSTLRUGarbageCollectorTests.mm b/Firestore/Example/Tests/Local/FSTLRUGarbageCollectorTests.mm index 6c6a91b6621..3a1f5b78876 100644 --- a/Firestore/Example/Tests/Local/FSTLRUGarbageCollectorTests.mm +++ b/Firestore/Example/Tests/Local/FSTLRUGarbageCollectorTests.mm @@ -24,13 +24,13 @@ #import "FIRTimestamp.h" #import "Firestore/Example/Tests/Util/FSTHelpers.h" #import "Firestore/Source/Local/FSTLRUGarbageCollector.h" -#import "Firestore/Source/Local/FSTMutationQueue.h" #import "Firestore/Source/Local/FSTPersistence.h" #import "Firestore/Source/Model/FSTDocument.h" #import "Firestore/Source/Model/FSTFieldValue.h" #import "Firestore/Source/Model/FSTMutation.h" #import "Firestore/Source/Util/FSTClasses.h" #include "Firestore/core/src/firebase/firestore/auth/user.h" +#include "Firestore/core/src/firebase/firestore/local/mutation_queue.h" #include "Firestore/core/src/firebase/firestore/local/query_cache.h" #include "Firestore/core/src/firebase/firestore/local/reference_set.h" #include "Firestore/core/src/firebase/firestore/local/remote_document_cache.h" @@ -44,6 +44,7 @@ using firebase::firestore::auth::User; using firebase::firestore::local::LruParams; using firebase::firestore::local::LruResults; +using firebase::firestore::local::MutationQueue; using firebase::firestore::local::QueryCache; using firebase::firestore::local::ReferenceSet; using firebase::firestore::local::RemoteDocumentCache; @@ -64,7 +65,7 @@ @implementation FSTLRUGarbageCollectorTests { id _persistence; QueryCache *_queryCache; RemoteDocumentCache *_documentCache; - id _mutationQueue; + MutationQueue *_mutationQueue; id _lruDelegate; FSTLRUGarbageCollector *_gc; ListenSequenceNumber _initialSequenceNumber; @@ -96,7 +97,7 @@ - (void)newTestResourcesWithLruParams:(LruParams)lruParams { _mutationQueue = [_persistence mutationQueueForUser:_user]; _lruDelegate = (id)_persistence.referenceDelegate; _initialSequenceNumber = _persistence.run("start querycache", [&]() -> ListenSequenceNumber { - [_mutationQueue start]; + _mutationQueue->Start(); _gc = _lruDelegate.gc; return _persistence.currentSequenceNumber; }); @@ -447,7 +448,7 @@ - (void)testRemoveOrphanedDocuments { // serve to keep the mutated documents from being GC'd while the mutations are outstanding. _persistence.run("actually register the mutations", [&]() { FIRTimestamp *writeTime = [FIRTimestamp timestamp]; - [_mutationQueue addMutationBatchWithWriteTime:writeTime mutations:mutations]; + _mutationQueue->AddMutationBatch(writeTime, mutations); }); // Mark 5 documents eligible for GC. This simulates documents that were mutated then ack'd. diff --git a/Firestore/Example/Tests/Local/FSTLevelDBMigrationsTests.mm b/Firestore/Example/Tests/Local/FSTLevelDBMigrationsTests.mm index d4d46a496ba..79a9b977e1c 100644 --- a/Firestore/Example/Tests/Local/FSTLevelDBMigrationsTests.mm +++ b/Firestore/Example/Tests/Local/FSTLevelDBMigrationsTests.mm @@ -23,7 +23,6 @@ #import "Firestore/Protos/objc/firestore/local/Mutation.pbobjc.h" #import "Firestore/Protos/objc/firestore/local/Target.pbobjc.h" #import "Firestore/Source/Local/FSTLevelDB.h" -#import "Firestore/Source/Local/FSTLevelDBMutationQueue.h" #include "Firestore/core/src/firebase/firestore/local/leveldb_key.h" #include "Firestore/core/src/firebase/firestore/local/leveldb_migrations.h" diff --git a/Firestore/Example/Tests/Local/FSTLevelDBMutationQueueTests.mm b/Firestore/Example/Tests/Local/FSTLevelDBMutationQueueTests.mm index 917f163c179..d24e24a0818 100644 --- a/Firestore/Example/Tests/Local/FSTLevelDBMutationQueueTests.mm +++ b/Firestore/Example/Tests/Local/FSTLevelDBMutationQueueTests.mm @@ -14,8 +14,6 @@ * limitations under the License. */ -#import "Firestore/Source/Local/FSTLevelDBMutationQueue.h" - #import #include @@ -81,7 +79,7 @@ - (void)setUp { _db = [FSTPersistenceTestHelpers levelDBPersistence]; [_db.referenceDelegate addInMemoryPins:&_additionalReferences]; - self.mutationQueue = [_db mutationQueueForUser:User("user")].mutationQueue; + self.mutationQueue = [_db mutationQueueForUser:User("user")]; self.persistence = _db; self.persistence.run("Setup", [&]() { self.mutationQueue->Start(); }); diff --git a/Firestore/Example/Tests/Local/FSTMemoryMutationQueueTests.mm b/Firestore/Example/Tests/Local/FSTMemoryMutationQueueTests.mm index bb2ff6c4018..0d54f4466ec 100644 --- a/Firestore/Example/Tests/Local/FSTMemoryMutationQueueTests.mm +++ b/Firestore/Example/Tests/Local/FSTMemoryMutationQueueTests.mm @@ -14,8 +14,6 @@ * limitations under the License. */ -#import "Firestore/Source/Local/FSTMemoryMutationQueue.h" - #import "Firestore/Source/Local/FSTMemoryPersistence.h" #import "Firestore/Example/Tests/Local/FSTMutationQueueTests.h" @@ -43,7 +41,7 @@ - (void)setUp { self.persistence = [FSTPersistenceTestHelpers eagerGCMemoryPersistence]; [self.persistence.referenceDelegate addInMemoryPins:&_additionalReferences]; - self.mutationQueue = [self.persistence mutationQueueForUser:User("user")].mutationQueue; + self.mutationQueue = [self.persistence mutationQueueForUser:User("user")]; } @end diff --git a/Firestore/Source/Local/FSTLRUGarbageCollector.mm b/Firestore/Source/Local/FSTLRUGarbageCollector.mm index 2071a35b977..b47a833857a 100644 --- a/Firestore/Source/Local/FSTLRUGarbageCollector.mm +++ b/Firestore/Source/Local/FSTLRUGarbageCollector.mm @@ -20,7 +20,6 @@ #include #include -#import "Firestore/Source/Local/FSTMutationQueue.h" #import "Firestore/Source/Local/FSTPersistence.h" #include "Firestore/core/include/firebase/firestore/timestamp.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" diff --git a/Firestore/Source/Local/FSTLevelDB.mm b/Firestore/Source/Local/FSTLevelDB.mm index c451aa9b262..ae010cfc272 100644 --- a/Firestore/Source/Local/FSTLevelDB.mm +++ b/Firestore/Source/Local/FSTLevelDB.mm @@ -22,7 +22,6 @@ #import "FIRFirestoreErrors.h" #import "Firestore/Source/Local/FSTLRUGarbageCollector.h" -#import "Firestore/Source/Local/FSTLevelDBMutationQueue.h" #import "Firestore/Source/Remote/FSTSerializerBeta.h" #include "Firestore/core/include/firebase/firestore/firestore_errors.h" @@ -30,6 +29,7 @@ #include "Firestore/core/src/firebase/firestore/core/database_info.h" #include "Firestore/core/src/firebase/firestore/local/leveldb_key.h" #include "Firestore/core/src/firebase/firestore/local/leveldb_migrations.h" +#include "Firestore/core/src/firebase/firestore/local/leveldb_mutation_queue.h" #include "Firestore/core/src/firebase/firestore/local/leveldb_query_cache.h" #include "Firestore/core/src/firebase/firestore/local/leveldb_remote_document_cache.h" #include "Firestore/core/src/firebase/firestore/local/leveldb_transaction.h" @@ -63,6 +63,7 @@ using firebase::firestore::local::LevelDbDocumentTargetKey; using firebase::firestore::local::LevelDbMigrations; using firebase::firestore::local::LevelDbMutationKey; +using firebase::firestore::local::LevelDbMutationQueue; using firebase::firestore::local::LevelDbQueryCache; using firebase::firestore::local::LevelDbRemoteDocumentCache; using firebase::firestore::local::LevelDbTransaction; @@ -93,7 +94,9 @@ - (size_t)byteSize; @property(nonatomic, assign, getter=isStarted) BOOL started; -- (firebase::firestore::local::LevelDbQueryCache *)queryCache; +- (LevelDbQueryCache *)queryCache; + +- (LevelDbMutationQueue *)mutationQueueForUser:(const User &)user; @end @@ -280,6 +283,7 @@ @implementation FSTLevelDB { FSTLevelDBLRUDelegate *_referenceDelegate; std::unique_ptr _queryCache; std::set _users; + std::unique_ptr _currentMutationQueue; } /** @@ -463,9 +467,10 @@ - (LevelDbTransaction *)currentTransaction { #pragma mark - Persistence Factory methods -- (id)mutationQueueForUser:(const User &)user { +- (LevelDbMutationQueue *)mutationQueueForUser:(const User &)user { _users.insert(user.uid()); - return [FSTLevelDBMutationQueue mutationQueueWithUser:user db:self serializer:self.serializer]; + _currentMutationQueue.reset(new LevelDbMutationQueue(user, self, self.serializer)); + return _currentMutationQueue.get(); } - (LevelDbQueryCache *)queryCache { diff --git a/Firestore/Source/Local/FSTLevelDBMutationQueue.h b/Firestore/Source/Local/FSTLevelDBMutationQueue.h deleted file mode 100644 index 72fde7d1ba8..00000000000 --- a/Firestore/Source/Local/FSTLevelDBMutationQueue.h +++ /dev/null @@ -1,52 +0,0 @@ -/* - * 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 - -#include - -#import "Firestore/Source/Local/FSTMutationQueue.h" - -#include "Firestore/core/src/firebase/firestore/auth/user.h" -#include "Firestore/core/src/firebase/firestore/local/leveldb_mutation_queue.h" -#include "Firestore/core/src/firebase/firestore/model/types.h" -#include "leveldb/db.h" - -@class FSTLevelDB; -@class FSTLocalSerializer; - -NS_ASSUME_NONNULL_BEGIN - -/** A mutation queue for a specific user, backed by LevelDB. */ -@interface FSTLevelDBMutationQueue : NSObject - -- (instancetype)init __attribute__((unavailable("Use a static constructor"))); - -/** - * Creates a new mutation queue for the given user, in the given LevelDB. - * - * @param user The user for which to create a mutation queue. - * @param db The LevelDB in which to create the queue. - */ -+ (instancetype)mutationQueueWithUser:(const firebase::firestore::auth::User &)user - db:(FSTLevelDB *)db - serializer:(FSTLocalSerializer *)serializer; - -- (firebase::firestore::local::LevelDbMutationQueue *)mutationQueue; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLevelDBMutationQueue.mm b/Firestore/Source/Local/FSTLevelDBMutationQueue.mm deleted file mode 100644 index 7753db22f12..00000000000 --- a/Firestore/Source/Local/FSTLevelDBMutationQueue.mm +++ /dev/null @@ -1,181 +0,0 @@ -/* - * 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 "Firestore/Source/Local/FSTLevelDBMutationQueue.h" - -#include -#include -#include -#include -#include - -#import "Firestore/Protos/objc/firestore/local/Mutation.pbobjc.h" -#import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Local/FSTLevelDB.h" -#import "Firestore/Source/Local/FSTLocalSerializer.h" -#import "Firestore/Source/Model/FSTMutation.h" -#import "Firestore/Source/Model/FSTMutationBatch.h" - -#include "Firestore/core/src/firebase/firestore/auth/user.h" -#include "Firestore/core/src/firebase/firestore/local/leveldb_key.h" -#include "Firestore/core/src/firebase/firestore/local/leveldb_mutation_queue.h" -#include "Firestore/core/src/firebase/firestore/local/leveldb_transaction.h" -#include "Firestore/core/src/firebase/firestore/local/leveldb_util.h" -#include "Firestore/core/src/firebase/firestore/model/mutation_batch.h" -#include "Firestore/core/src/firebase/firestore/model/resource_path.h" -#include "Firestore/core/src/firebase/firestore/util/hard_assert.h" -#include "Firestore/core/src/firebase/firestore/util/string_apple.h" -#include "Firestore/core/src/firebase/firestore/util/string_util.h" -#include "absl/memory/memory.h" -#include "absl/strings/match.h" -#include "leveldb/db.h" -#include "leveldb/write_batch.h" - -NS_ASSUME_NONNULL_BEGIN - -namespace util = firebase::firestore::util; -using firebase::firestore::auth::User; -using firebase::firestore::local::DescribeKey; -using firebase::firestore::local::LevelDbDocumentMutationKey; -using firebase::firestore::local::LevelDbMutationKey; -using firebase::firestore::local::LevelDbMutationQueue; -using firebase::firestore::local::LevelDbMutationQueueKey; -using firebase::firestore::local::LevelDbTransaction; -using firebase::firestore::local::LoadNextBatchIdFromDb; -using firebase::firestore::local::MakeStringView; -using firebase::firestore::model::BatchId; -using firebase::firestore::model::kBatchIdUnknown; -using firebase::firestore::model::DocumentKey; -using firebase::firestore::model::DocumentKeySet; -using firebase::firestore::model::ResourcePath; -using leveldb::DB; -using leveldb::Iterator; -using leveldb::ReadOptions; -using leveldb::Slice; -using leveldb::Status; -using leveldb::WriteBatch; -using leveldb::WriteOptions; - -static NSArray *toNSArray(const std::vector &vec) { - NSMutableArray *copy = [NSMutableArray array]; - for (auto &batch : vec) { - [copy addObject:batch]; - } - return copy; -} - -@interface FSTLevelDBMutationQueue () - -- (instancetype)initWithUserID:(std::string)userID - db:(FSTLevelDB *)db - serializer:(FSTLocalSerializer *)serializer - delegate:(std::unique_ptr)delegate - NS_DESIGNATED_INITIALIZER; - -@end - -@implementation FSTLevelDBMutationQueue { - std::unique_ptr _delegate; -} - -+ (instancetype)mutationQueueWithUser:(const User &)user - db:(FSTLevelDB *)db - serializer:(FSTLocalSerializer *)serializer { - std::string userID = user.is_authenticated() ? user.uid() : ""; - - return [[FSTLevelDBMutationQueue alloc] - initWithUserID:std::move(userID) - db:db - serializer:serializer - delegate:absl::make_unique(user, db, serializer)]; -} - -- (instancetype)initWithUserID:(std::string)userID - db:(FSTLevelDB *)db - serializer:(FSTLocalSerializer *)serializer - delegate:(std::unique_ptr)delegate { - if (self = [super init]) { - _delegate = std::move(delegate); - } - return self; -} - -- (void)start { - _delegate->Start(); -} - -- (BOOL)isEmpty { - return _delegate->IsEmpty(); -} - -- (void)acknowledgeBatch:(FSTMutationBatch *)batch streamToken:(nullable NSData *)streamToken { - _delegate->AcknowledgeBatch(batch, streamToken); -} - -- (nullable NSData *)lastStreamToken { - return _delegate->GetLastStreamToken(); -} - -- (void)setLastStreamToken:(nullable NSData *)streamToken { - _delegate->SetLastStreamToken(streamToken); -} - -- (FSTMutationBatch *)addMutationBatchWithWriteTime:(FIRTimestamp *)localWriteTime - mutations:(NSArray *)mutations { - return _delegate->AddMutationBatch(localWriteTime, mutations); -} - -- (nullable FSTMutationBatch *)lookupMutationBatch:(BatchId)batchID { - return _delegate->LookupMutationBatch(batchID); -} - -- (nullable FSTMutationBatch *)nextMutationBatchAfterBatchID:(BatchId)batchID { - return _delegate->NextMutationBatchAfterBatchId(batchID); -} - -- (NSArray *)allMutationBatchesAffectingDocumentKey: - (const DocumentKey &)documentKey { - return toNSArray(_delegate->AllMutationBatchesAffectingDocumentKey(documentKey)); -} - -- (NSArray *)allMutationBatchesAffectingDocumentKeys: - (const DocumentKeySet &)documentKeys { - return toNSArray(_delegate->AllMutationBatchesAffectingDocumentKeys(documentKeys)); -} - -- (NSArray *)allMutationBatchesAffectingQuery:(FSTQuery *)query { - return toNSArray(_delegate->AllMutationBatchesAffectingQuery(query)); -} - -- (NSArray *)allMutationBatches { - return toNSArray(_delegate->AllMutationBatches()); -} - -- (void)removeMutationBatch:(FSTMutationBatch *)batch { - _delegate->RemoveMutationBatch(batch); -} - -- (void)performConsistencyCheck { - _delegate->PerformConsistencyCheck(); -} - -- (LevelDbMutationQueue *)mutationQueue { - return _delegate.get(); -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLocalDocumentsView.h b/Firestore/Source/Local/FSTLocalDocumentsView.h index a559b7c2136..730e0ea47bc 100644 --- a/Firestore/Source/Local/FSTLocalDocumentsView.h +++ b/Firestore/Source/Local/FSTLocalDocumentsView.h @@ -16,6 +16,7 @@ #import +#include "Firestore/core/src/firebase/firestore/local/mutation_queue.h" #include "Firestore/core/src/firebase/firestore/local/remote_document_cache.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" #include "Firestore/core/src/firebase/firestore/model/document_key_set.h" @@ -23,7 +24,6 @@ @class FSTMaybeDocument; @class FSTQuery; -@protocol FSTMutationQueue; NS_ASSUME_NONNULL_BEGIN @@ -36,7 +36,8 @@ NS_ASSUME_NONNULL_BEGIN + (instancetype)viewWithRemoteDocumentCache: (firebase::firestore::local::RemoteDocumentCache *)remoteDocumentCache - mutationQueue:(id)mutationQueue; + mutationQueue: + (firebase::firestore::local::MutationQueue *)mutationQueue; - (instancetype)init __attribute__((unavailable("Use a static constructor"))); diff --git a/Firestore/Source/Local/FSTLocalDocumentsView.mm b/Firestore/Source/Local/FSTLocalDocumentsView.mm index bdc92a8dc08..eb11d3ca0ab 100644 --- a/Firestore/Source/Local/FSTLocalDocumentsView.mm +++ b/Firestore/Source/Local/FSTLocalDocumentsView.mm @@ -16,12 +16,14 @@ #import "Firestore/Source/Local/FSTLocalDocumentsView.h" +#include + #import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Local/FSTMutationQueue.h" #import "Firestore/Source/Model/FSTDocument.h" #import "Firestore/Source/Model/FSTMutation.h" #import "Firestore/Source/Model/FSTMutationBatch.h" +#include "Firestore/core/src/firebase/firestore/local/mutation_queue.h" #include "Firestore/core/src/firebase/firestore/local/remote_document_cache.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" #include "Firestore/core/src/firebase/firestore/model/document_map.h" @@ -29,6 +31,7 @@ #include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" +using firebase::firestore::local::MutationQueue; using firebase::firestore::local::RemoteDocumentCache; using firebase::firestore::model::DocumentKey; using firebase::firestore::model::DocumentKeySet; @@ -41,24 +44,24 @@ @interface FSTLocalDocumentsView () - (instancetype)initWithRemoteDocumentCache:(RemoteDocumentCache *)remoteDocumentCache - mutationQueue:(id)mutationQueue + mutationQueue:(MutationQueue *)mutationQueue NS_DESIGNATED_INITIALIZER; -@property(nonatomic, strong, readonly) id mutationQueue; @end @implementation FSTLocalDocumentsView { RemoteDocumentCache *_remoteDocumentCache; + MutationQueue *_mutationQueue; } + (instancetype)viewWithRemoteDocumentCache:(RemoteDocumentCache *)remoteDocumentCache - mutationQueue:(id)mutationQueue { + mutationQueue:(MutationQueue *)mutationQueue { return [[FSTLocalDocumentsView alloc] initWithRemoteDocumentCache:remoteDocumentCache mutationQueue:mutationQueue]; } - (instancetype)initWithRemoteDocumentCache:(RemoteDocumentCache *)remoteDocumentCache - mutationQueue:(id)mutationQueue { + mutationQueue:(MutationQueue *)mutationQueue { if (self = [super init]) { _remoteDocumentCache = remoteDocumentCache; _mutationQueue = mutationQueue; @@ -67,16 +70,16 @@ - (instancetype)initWithRemoteDocumentCache:(RemoteDocumentCache *)remoteDocumen } - (nullable FSTMaybeDocument *)documentForKey:(const DocumentKey &)key { - NSArray *batches = - [self.mutationQueue allMutationBatchesAffectingDocumentKey:key]; + std::vector batches = + _mutationQueue->AllMutationBatchesAffectingDocumentKey(key); return [self documentForKey:key inBatches:batches]; } // Internal version of documentForKey: which allows reusing `batches`. - (nullable FSTMaybeDocument *)documentForKey:(const DocumentKey &)key - inBatches:(NSArray *)batches { + inBatches:(const std::vector &)batches { FSTMaybeDocument *_Nullable document = _remoteDocumentCache->Get(key); - for (FSTMutationBatch *batch in batches) { + for (FSTMutationBatch *batch : batches) { document = [batch applyToLocalDocument:document documentKey:key]; } @@ -86,13 +89,14 @@ - (nullable FSTMaybeDocument *)documentForKey:(const DocumentKey &)key // Returns the view of the given `docs` as they would appear after applying all // mutations in the given `batches`. - (MaybeDocumentMap)applyLocalMutationsToDocuments:(const MaybeDocumentMap &)docs - fromBatches:(NSArray *)batches { + fromBatches: + (const std::vector &)batches { MaybeDocumentMap results; for (const auto &kv : docs) { const DocumentKey &key = kv.first; FSTMaybeDocument *localView = kv.second; - for (FSTMutationBatch *batch in batches) { + for (FSTMutationBatch *batch : batches) { localView = [batch applyToLocalDocument:localView documentKey:key]; } results = results.insert(key, localView); @@ -116,8 +120,8 @@ - (MaybeDocumentMap)localViewsForDocuments:(const MaybeDocumentMap &)baseDocs { for (const auto &kv : baseDocs) { allKeys = allKeys.insert(kv.first); } - NSArray *batches = - [self.mutationQueue allMutationBatchesAffectingDocumentKeys:allKeys]; + std::vector batches = + _mutationQueue->AllMutationBatchesAffectingDocumentKeys(allKeys); MaybeDocumentMap docs = [self applyLocalMutationsToDocuments:baseDocs fromBatches:batches]; @@ -158,10 +162,10 @@ - (DocumentMap)documentsMatchingDocumentQuery:(const ResourcePath &)docPath { - (DocumentMap)documentsMatchingCollectionQuery:(FSTQuery *)query { DocumentMap results = _remoteDocumentCache->GetMatching(query); // Get locally persisted mutation batches. - NSArray *matchingBatches = - [self.mutationQueue allMutationBatchesAffectingQuery:query]; + std::vector matchingBatches = + _mutationQueue->AllMutationBatchesAffectingQuery(query); - for (FSTMutationBatch *batch in matchingBatches) { + for (FSTMutationBatch *batch : matchingBatches) { for (FSTMutation *mutation in batch.mutations) { // Only process documents belonging to the collection. if (!query.path.IsImmediateParentOf(mutation.key.path())) { diff --git a/Firestore/Source/Local/FSTLocalStore.mm b/Firestore/Source/Local/FSTLocalStore.mm index e157702a720..22d9aa283e0 100644 --- a/Firestore/Source/Local/FSTLocalStore.mm +++ b/Firestore/Source/Local/FSTLocalStore.mm @@ -19,6 +19,7 @@ #include #include #include +#include #import "FIRTimestamp.h" #import "Firestore/Source/Core/FSTQuery.h" @@ -26,7 +27,6 @@ #import "Firestore/Source/Local/FSTLocalDocumentsView.h" #import "Firestore/Source/Local/FSTLocalViewChanges.h" #import "Firestore/Source/Local/FSTLocalWriteResult.h" -#import "Firestore/Source/Local/FSTMutationQueue.h" #import "Firestore/Source/Local/FSTPersistence.h" #import "Firestore/Source/Local/FSTQueryData.h" #import "Firestore/Source/Model/FSTDocument.h" @@ -36,6 +36,7 @@ #include "Firestore/core/src/firebase/firestore/auth/user.h" #include "Firestore/core/src/firebase/firestore/core/target_id_generator.h" #include "Firestore/core/src/firebase/firestore/immutable/sorted_set.h" +#include "Firestore/core/src/firebase/firestore/local/mutation_queue.h" #include "Firestore/core/src/firebase/firestore/local/query_cache.h" #include "Firestore/core/src/firebase/firestore/local/reference_set.h" #include "Firestore/core/src/firebase/firestore/local/remote_document_cache.h" @@ -47,6 +48,7 @@ using firebase::firestore::auth::User; using firebase::firestore::core::TargetIdGenerator; using firebase::firestore::local::LruResults; +using firebase::firestore::local::MutationQueue; using firebase::firestore::local::QueryCache; using firebase::firestore::local::ReferenceSet; using firebase::firestore::local::RemoteDocumentCache; @@ -77,11 +79,8 @@ @interface FSTLocalStore () /** Manages our in-memory or durable persistence. */ @property(nonatomic, strong, readonly) id persistence; -/** The set of all mutations that have been sent but not yet been applied to the backend. */ -@property(nonatomic, strong) id mutationQueue; - /** The "local" view of all documents (layering mutationQueue on top of remoteDocumentCache). */ -@property(nonatomic, strong) FSTLocalDocumentsView *localDocuments; +@property(nonatomic, nullable, strong) FSTLocalDocumentsView *localDocuments; /** Maps a query to the data about that query. */ @property(nonatomic) QueryCache *queryCache; @@ -94,6 +93,8 @@ @implementation FSTLocalStore { /** The set of all cached remote documents. */ RemoteDocumentCache *_remoteDocumentCache; QueryCache *_queryCache; + /** The set of all mutations that have been sent but not yet been applied to the backend. */ + MutationQueue *_mutationQueue; /** The set of document references maintained by any local views. */ ReferenceSet _localViewReferences; @@ -125,30 +126,32 @@ - (void)start { } - (void)startMutationQueue { - self.persistence.run("Start MutationQueue", [&]() { [self.mutationQueue start]; }); + self.persistence.run("Start MutationQueue", [&]() { _mutationQueue->Start(); }); } - (MaybeDocumentMap)userDidChange:(const User &)user { // Swap out the mutation queue, grabbing the pending mutation batches before and after. - NSArray *oldBatches = self.persistence.run( + std::vector oldBatches = self.persistence.run( "OldBatches", - [&]() -> NSArray * { return [self.mutationQueue allMutationBatches]; }); + [&]() -> std::vector { return _mutationQueue->AllMutationBatches(); }); - self.mutationQueue = [self.persistence mutationQueueForUser:user]; + // The old one has a reference to the mutation queue, so nil it out first. + self.localDocuments = nil; + _mutationQueue = [self.persistence mutationQueueForUser:user]; [self startMutationQueue]; return self.persistence.run("NewBatches", [&]() -> MaybeDocumentMap { - NSArray *newBatches = [self.mutationQueue allMutationBatches]; + std::vector newBatches = _mutationQueue->AllMutationBatches(); // Recreate our LocalDocumentsView using the new MutationQueue. self.localDocuments = [FSTLocalDocumentsView viewWithRemoteDocumentCache:_remoteDocumentCache - mutationQueue:self.mutationQueue]; + mutationQueue:_mutationQueue]; // Union the old/new changed keys. DocumentKeySet changedKeys; - for (NSArray *batches in @[ oldBatches, newBatches ]) { - for (FSTMutationBatch *batch in batches) { + for (const std::vector &batches : {oldBatches, newBatches}) { + for (FSTMutationBatch *batch : batches) { for (FSTMutation *mutation in batch.mutations) { changedKeys = changedKeys.insert(mutation.key); } @@ -163,8 +166,7 @@ - (MaybeDocumentMap)userDidChange:(const User &)user { - (FSTLocalWriteResult *)locallyWriteMutations:(NSArray *)mutations { return self.persistence.run("Locally write mutations", [&]() -> FSTLocalWriteResult * { FIRTimestamp *localWriteTime = [FIRTimestamp timestamp]; - FSTMutationBatch *batch = [self.mutationQueue addMutationBatchWithWriteTime:localWriteTime - mutations:mutations]; + FSTMutationBatch *batch = _mutationQueue->AddMutationBatch(localWriteTime, mutations); DocumentKeySet keys = [batch keys]; MaybeDocumentMap changedDocuments = [self.localDocuments documentsForKeys:keys]; return [FSTLocalWriteResult resultForBatchID:batch.batchID changes:std::move(changedDocuments)]; @@ -173,12 +175,10 @@ - (FSTLocalWriteResult *)locallyWriteMutations:(NSArray *)mutatio - (MaybeDocumentMap)acknowledgeBatchWithResult:(FSTMutationBatchResult *)batchResult { return self.persistence.run("Acknowledge batch", [&]() -> MaybeDocumentMap { - id mutationQueue = self.mutationQueue; - FSTMutationBatch *batch = batchResult.batch; - [mutationQueue acknowledgeBatch:batch streamToken:batchResult.streamToken]; + _mutationQueue->AcknowledgeBatch(batch, batchResult.streamToken); [self applyBatchResult:batchResult]; - [self.mutationQueue performConsistencyCheck]; + _mutationQueue->PerformConsistencyCheck(); return [self.localDocuments documentsForKeys:batch.keys]; }); @@ -186,23 +186,23 @@ - (MaybeDocumentMap)acknowledgeBatchWithResult:(FSTMutationBatchResult *)batchRe - (MaybeDocumentMap)rejectBatchID:(BatchId)batchID { return self.persistence.run("Reject batch", [&]() -> MaybeDocumentMap { - FSTMutationBatch *toReject = [self.mutationQueue lookupMutationBatch:batchID]; + FSTMutationBatch *toReject = _mutationQueue->LookupMutationBatch(batchID); HARD_ASSERT(toReject, "Attempt to reject nonexistent batch!"); - [self.mutationQueue removeMutationBatch:toReject]; - [self.mutationQueue performConsistencyCheck]; + _mutationQueue->RemoveMutationBatch(toReject); + _mutationQueue->PerformConsistencyCheck(); return [self.localDocuments documentsForKeys:toReject.keys]; }); } - (nullable NSData *)lastStreamToken { - return [self.mutationQueue lastStreamToken]; + return _mutationQueue->GetLastStreamToken(); } - (void)setLastStreamToken:(nullable NSData *)streamToken { self.persistence.run("Set stream token", - [&]() { [self.mutationQueue setLastStreamToken:streamToken]; }); + [&]() { _mutationQueue->SetLastStreamToken(streamToken); }); } - (const SnapshotVersion &)lastRemoteSnapshotVersion { @@ -369,7 +369,7 @@ - (void)notifyLocalViewChanges:(NSArray *)viewChanges { - (nullable FSTMutationBatch *)nextMutationBatchAfterBatchID:(BatchId)batchID { FSTMutationBatch *result = self.persistence.run("NextMutationBatchAfterBatchID", [&]() -> FSTMutationBatch * { - return [self.mutationQueue nextMutationBatchAfterBatchID:batchID]; + return _mutationQueue->NextMutationBatchAfterBatchId(batchID); }); return result; } @@ -466,7 +466,7 @@ - (void)applyBatchResult:(FSTMutationBatchResult *)batchResult { } } - [self.mutationQueue removeMutationBatch:batch]; + _mutationQueue->RemoveMutationBatch(batch); } - (LruResults)collectGarbage:(FSTLRUGarbageCollector *)garbageCollector { diff --git a/Firestore/Source/Local/FSTMemoryMutationQueue.h b/Firestore/Source/Local/FSTMemoryMutationQueue.h deleted file mode 100644 index 3a7808d0ebb..00000000000 --- a/Firestore/Source/Local/FSTMemoryMutationQueue.h +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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 - -#import "Firestore/Source/Local/FSTMemoryPersistence.h" -#import "Firestore/Source/Local/FSTMutationQueue.h" - -NS_ASSUME_NONNULL_BEGIN - -@class FSTLocalSerializer; - -@interface FSTMemoryMutationQueue : NSObject - -- (instancetype)initWithPersistence:(FSTMemoryPersistence *)persistence NS_DESIGNATED_INITIALIZER; - -- (instancetype)init NS_UNAVAILABLE; - -/** - * Checks to see if there are any references to a document with the given key. - */ -- (BOOL)containsKey:(const firebase::firestore::model::DocumentKey &)key; - -- (size_t)byteSizeWithSerializer:(FSTLocalSerializer *)serializer; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTMemoryMutationQueue.mm b/Firestore/Source/Local/FSTMemoryMutationQueue.mm deleted file mode 100644 index a8a50732fa3..00000000000 --- a/Firestore/Source/Local/FSTMemoryMutationQueue.mm +++ /dev/null @@ -1,152 +0,0 @@ -/* - * 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 "Firestore/Source/Local/FSTMemoryMutationQueue.h" - -#import - -#include -#include - -#import "Firestore/Protos/objc/firestore/local/Mutation.pbobjc.h" -#import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Local/FSTMemoryPersistence.h" -#import "Firestore/Source/Model/FSTMutation.h" -#import "Firestore/Source/Model/FSTMutationBatch.h" - -#include "Firestore/core/src/firebase/firestore/immutable/sorted_set.h" -#include "Firestore/core/src/firebase/firestore/local/document_reference.h" -#include "Firestore/core/src/firebase/firestore/local/memory_mutation_queue.h" -#include "Firestore/core/src/firebase/firestore/model/document_key.h" -#include "Firestore/core/src/firebase/firestore/model/resource_path.h" -#include "Firestore/core/src/firebase/firestore/util/hard_assert.h" -#include "absl/memory/memory.h" - -using firebase::firestore::immutable::SortedSet; -using firebase::firestore::local::DocumentReference; -using firebase::firestore::local::MemoryMutationQueue; -using firebase::firestore::model::BatchId; -using firebase::firestore::model::DocumentKey; -using firebase::firestore::model::DocumentKeySet; -using firebase::firestore::model::ResourcePath; - -NS_ASSUME_NONNULL_BEGIN - -static NSArray *toNSArray(const std::vector &vec) { - NSMutableArray *copy = [NSMutableArray array]; - for (auto &batch : vec) { - [copy addObject:batch]; - } - return copy; -} - -@interface FSTMemoryMutationQueue () - -- (MemoryMutationQueue *)mutationQueue; - -@end - -@implementation FSTMemoryMutationQueue { - std::unique_ptr _delegate; -} - -- (instancetype)initWithPersistence:(FSTMemoryPersistence *)persistence { - if (self = [super init]) { - _delegate = absl::make_unique(persistence); - } - return self; -} - -- (void)setLastStreamToken:(NSData *_Nullable)streamToken { - _delegate->SetLastStreamToken(streamToken); -} - -- (NSData *_Nullable)lastStreamToken { - return _delegate->GetLastStreamToken(); -} - -#pragma mark - FSTMutationQueue implementation - -- (void)start { - _delegate->Start(); -} - -- (BOOL)isEmpty { - return _delegate->IsEmpty(); -} - -- (void)acknowledgeBatch:(FSTMutationBatch *)batch streamToken:(nullable NSData *)streamToken { - _delegate->AcknowledgeBatch(batch, streamToken); -} - -- (FSTMutationBatch *)addMutationBatchWithWriteTime:(FIRTimestamp *)localWriteTime - mutations:(NSArray *)mutations { - return _delegate->AddMutationBatch(localWriteTime, mutations); -} - -- (nullable FSTMutationBatch *)lookupMutationBatch:(BatchId)batchID { - return _delegate->LookupMutationBatch(batchID); -} - -- (nullable FSTMutationBatch *)nextMutationBatchAfterBatchID:(BatchId)batchID { - return _delegate->NextMutationBatchAfterBatchId(batchID); -} - -- (NSArray *)allMutationBatches { - return toNSArray(_delegate->AllMutationBatches()); -} - -- (NSArray *)allMutationBatchesAffectingDocumentKey: - (const DocumentKey &)documentKey { - return toNSArray(_delegate->AllMutationBatchesAffectingDocumentKey(documentKey)); -} - -- (NSArray *)allMutationBatchesAffectingDocumentKeys: - (const DocumentKeySet &)documentKeys { - return toNSArray(_delegate->AllMutationBatchesAffectingDocumentKeys(documentKeys)); -} - -- (NSArray *)allMutationBatchesAffectingQuery:(FSTQuery *)query { - return toNSArray(_delegate->AllMutationBatchesAffectingQuery(query)); -} - -- (void)removeMutationBatch:(FSTMutationBatch *)batch { - _delegate->RemoveMutationBatch(batch); -} - -- (void)performConsistencyCheck { - _delegate->PerformConsistencyCheck(); -} - -#pragma mark - FSTGarbageSource implementation - -- (BOOL)containsKey:(const DocumentKey &)key { - return _delegate->ContainsKey(key); -} - -#pragma mark - Helpers - -- (size_t)byteSizeWithSerializer:(FSTLocalSerializer *)serializer { - return _delegate->CalculateByteSize(serializer); -} - -- (MemoryMutationQueue *)mutationQueue { - return _delegate.get(); -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTMemoryPersistence.mm b/Firestore/Source/Local/FSTMemoryPersistence.mm index f90d9f3c826..6dc197cab32 100644 --- a/Firestore/Source/Local/FSTMemoryPersistence.mm +++ b/Firestore/Source/Local/FSTMemoryPersistence.mm @@ -21,21 +21,21 @@ #include #include -#import "Firestore/Source/Local/FSTMemoryMutationQueue.h" -#include "absl/memory/memory.h" - #include "Firestore/core/src/firebase/firestore/auth/user.h" #include "Firestore/core/src/firebase/firestore/local/listen_sequence.h" +#include "Firestore/core/src/firebase/firestore/local/memory_mutation_queue.h" #include "Firestore/core/src/firebase/firestore/local/memory_query_cache.h" #include "Firestore/core/src/firebase/firestore/local/memory_remote_document_cache.h" #include "Firestore/core/src/firebase/firestore/local/reference_set.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" +#include "absl/memory/memory.h" using firebase::firestore::auth::HashUser; using firebase::firestore::auth::User; using firebase::firestore::local::ListenSequence; using firebase::firestore::local::LruParams; +using firebase::firestore::local::MemoryMutationQueue; using firebase::firestore::local::MemoryQueryCache; using firebase::firestore::local::MemoryRemoteDocumentCache; using firebase::firestore::local::ReferenceSet; @@ -45,7 +45,7 @@ using firebase::firestore::model::TargetId; using firebase::firestore::util::Status; -using MutationQueues = std::unordered_map; +using MutationQueues = std::unordered_map, HashUser>; NS_ASSUME_NONNULL_BEGIN @@ -55,6 +55,8 @@ - (MemoryQueryCache *)queryCache; - (MemoryRemoteDocumentCache *)remoteDocumentCache; +- (MemoryMutationQueue *)mutationQueueForUser:(const User &)user; + @property(nonatomic, readonly) MutationQueues &mutationQueues; @property(nonatomic, assign, getter=isStarted) BOOL started; @@ -134,13 +136,14 @@ - (ListenSequenceNumber)currentSequenceNumber { return _transactionRunner; } -- (id)mutationQueueForUser:(const User &)user { - id queue = _mutationQueues[user]; - if (!queue) { - queue = [[FSTMemoryMutationQueue alloc] initWithPersistence:self]; - _mutationQueues[user] = queue; +- (MemoryMutationQueue *)mutationQueueForUser:(const User &)user { + const std::unique_ptr &existing = _mutationQueues[user]; + if (!existing) { + _mutationQueues[user] = absl::make_unique(self); + return _mutationQueues[user].get(); + } else { + return existing.get(); } - return queue; } - (MemoryQueryCache *)queryCache { @@ -272,7 +275,7 @@ - (void)removeReference:(const DocumentKey &)key { - (BOOL)mutationQueuesContainKey:(const DocumentKey &)key { const MutationQueues &queues = [_persistence mutationQueues]; for (const auto &entry : queues) { - if ([entry.second containsKey:key]) { + if (entry.second->ContainsKey(key)) { return YES; } } @@ -310,7 +313,7 @@ - (size_t)byteSize { count += _persistence.remoteDocumentCache->CalculateByteSize(_serializer); const MutationQueues &queues = [_persistence mutationQueues]; for (const auto &entry : queues) { - count += [entry.second byteSizeWithSerializer:_serializer]; + count += entry.second->CalculateByteSize(_serializer); } return count; } @@ -389,7 +392,7 @@ - (void)startTransaction:(__unused absl::string_view)label { - (BOOL)mutationQueuesContainKey:(const DocumentKey &)key { const MutationQueues &queues = [_persistence mutationQueues]; for (const auto &entry : queues) { - if ([entry.second containsKey:key]) { + if (entry.second->ContainsKey(key)) { return YES; } } diff --git a/Firestore/Source/Local/FSTMutationQueue.h b/Firestore/Source/Local/FSTMutationQueue.h deleted file mode 100644 index 6540e745b20..00000000000 --- a/Firestore/Source/Local/FSTMutationQueue.h +++ /dev/null @@ -1,133 +0,0 @@ -/* - * 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 - -#include "Firestore/core/src/firebase/firestore/local/mutation_queue.h" -#include "Firestore/core/src/firebase/firestore/model/document_key.h" -#include "Firestore/core/src/firebase/firestore/model/document_key_set.h" -#include "Firestore/core/src/firebase/firestore/model/types.h" - -@class FSTMutation; -@class FSTMutationBatch; -@class FSTQuery; -@class FIRTimestamp; - -NS_ASSUME_NONNULL_BEGIN - -#pragma mark - FSTMutationQueue - -/** A queue of mutations to apply to the remote store. */ -@protocol FSTMutationQueue - -/** - * Starts the mutation queue, performing any initial reads that might be required to establish - * invariants, etc. - */ -- (void)start; - -/** Returns YES if this queue contains no mutation batches. */ -- (BOOL)isEmpty; - -/** Acknowledges the given batch. */ -- (void)acknowledgeBatch:(FSTMutationBatch *)batch streamToken:(nullable NSData *)streamToken; - -/** Returns the current stream token for this mutation queue. */ -- (nullable NSData *)lastStreamToken; - -/** Sets the stream token for this mutation queue. */ -- (void)setLastStreamToken:(nullable NSData *)streamToken; - -/** Creates a new mutation batch and adds it to this mutation queue. */ -- (FSTMutationBatch *)addMutationBatchWithWriteTime:(FIRTimestamp *)localWriteTime - mutations:(NSArray *)mutations; - -/** Loads the mutation batch with the given batchID. */ -- (nullable FSTMutationBatch *)lookupMutationBatch:(firebase::firestore::model::BatchId)batchID; - -/** - * Gets the first unacknowledged mutation batch after the passed in batchId in the mutation queue - * or nil if empty. - * - * @param batchID The batch to search after, or kBatchIdUnknown for the first mutation in the - * queue. - * - * @return the next mutation or nil if there wasn't one. - */ -- (nullable FSTMutationBatch *)nextMutationBatchAfterBatchID: - (firebase::firestore::model::BatchId)batchID; - -/** Gets all mutation batches in the mutation queue. */ -// TODO(mikelehen): PERF: Current consumer only needs mutated keys; if we can provide that -// cheaply, we should replace this. -- (NSArray *)allMutationBatches; - -/** - * Finds all mutation batches that could @em possibly affect the given document key. Not all - * mutations in a batch will necessarily affect the document key, so when looping through the - * batch you'll need to check that the mutation itself matches the key. - * - * Note that because of this requirement implementations are free to return mutation batches that - * don't contain the document key at all if it's convenient. - */ -// TODO(mcg): This should really return an NSEnumerator -- (NSArray *)allMutationBatchesAffectingDocumentKey: - (const firebase::firestore::model::DocumentKey &)documentKey; - -/** - * Finds all mutation batches that could @em possibly affect the given document keys. Not all - * mutations in a batch will necessarily affect each key, so when looping through the batches you'll - * need to check that the mutation itself matches the key. - * - * Note that because of this requirement implementations are free to return mutation batches that - * don't contain any of the given document keys at all if it's convenient. - */ -// TODO(mcg): This should really return an NSEnumerator -- (NSArray *)allMutationBatchesAffectingDocumentKeys: - (const firebase::firestore::model::DocumentKeySet &)documentKeys; - -/** - * Finds all mutation batches that could affect the results for the given query. Not all - * mutations in a batch will necessarily affect the query, so when looping through the batch - * you'll need to check that the mutation itself matches the query. - * - * Note that because of this requirement implementations are free to return mutation batches that - * don't match the query at all if it's convenient. - * - * NOTE: A FSTPatchMutation does not need to include all fields in the query filter criteria in - * order to be a match (but any fields it does contain do need to match). - */ -// TODO(mikelehen): This should perhaps return an NSEnumerator, though I'm not sure we can avoid -// loading them all in memory. -- (NSArray *)allMutationBatchesAffectingQuery:(FSTQuery *)query; - -/** - * Removes the given mutation batch from the queue. This is useful in two circumstances: - * - * + Removing applied mutations from the head of the queue - * + Removing rejected mutations from anywhere in the queue - */ -- (void)removeMutationBatch:(FSTMutationBatch *)batch; - -/** Performs a consistency check, examining the mutation queue for any leaks, if possible. */ -- (void)performConsistencyCheck; - -// Visible for testing -- (firebase::firestore::local::MutationQueue *)mutationQueue; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTPersistence.h b/Firestore/Source/Local/FSTPersistence.h index e44ee6500d6..961ad0dcbf7 100644 --- a/Firestore/Source/Local/FSTPersistence.h +++ b/Firestore/Source/Local/FSTPersistence.h @@ -17,6 +17,7 @@ #import #include "Firestore/core/src/firebase/firestore/auth/user.h" +#include "Firestore/core/src/firebase/firestore/local/mutation_queue.h" #include "Firestore/core/src/firebase/firestore/local/query_cache.h" #include "Firestore/core/src/firebase/firestore/local/reference_set.h" #include "Firestore/core/src/firebase/firestore/local/remote_document_cache.h" @@ -26,7 +27,6 @@ #include "Firestore/core/src/firebase/firestore/util/status.h" @class FSTQueryData; -@protocol FSTMutationQueue; @protocol FSTReferenceDelegate; struct FSTTransactionRunner; @@ -69,14 +69,15 @@ NS_ASSUME_NONNULL_BEGIN - (void)shutdown; /** - * Returns an FSTMutationQueue representing the persisted mutations for the given user. + * Returns a MutationQueue representing the persisted mutations for the given user. * *

Note: The implementation is free to return the same instance every time this is called for a * given user. In particular, the memory-backed implementation does this to emulate the persisted * implementation to the extent possible (e.g. in the case of uid switching from * sally=>jack=>sally, sally's mutation queue will be preserved). */ -- (id)mutationQueueForUser:(const firebase::firestore::auth::User &)user; +- (firebase::firestore::local::MutationQueue *)mutationQueueForUser: + (const firebase::firestore::auth::User &)user; /** Creates an FSTQueryCache representing the persisted cache of queries. */ - (firebase::firestore::local::QueryCache *)queryCache; From 9ec14cc0db401cf44fe9bbd6165e7f9c314a3356 Mon Sep 17 00:00:00 2001 From: Konstantin Varlamov Date: Fri, 1 Feb 2019 14:14:53 -0500 Subject: [PATCH 11/27] Don't build fuzz tests target on Travis (#2330) This XCode target started failing on Travis for no clear reason. This isn't reproducible locally; remove the target to unblock development. --- scripts/build.sh | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/scripts/build.sh b/scripts/build.sh index 881b72cad1f..b99c2e26bff 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -296,16 +296,6 @@ case "$product-$method-$platform" in "${xcb_flags[@]}" \ build - # Firestore_FuzzTests_iOS require a Clang that supports -fsanitize-coverage=trace-pc-guard - # and cannot run with thread sanitizer. - if [[ "$xcode_major" -ge 9 ]] && ! [[ -n "${SANITIZERS:-}" && "$SANITIZERS" = *"tsan"* ]]; then - RunXcodebuild \ - -workspace 'Firestore/Example/Firestore.xcworkspace' \ - -scheme "Firestore_FuzzTests_iOS" \ - "${xcb_flags[@]}" \ - FUZZING_TARGET="NONE" \ - test - fi ;; Firestore-cmake-macOS) From 61f173eb125a64b3dc4a3b3408bacd858c37f563 Mon Sep 17 00:00:00 2001 From: Konstantin Varlamov Date: Fri, 1 Feb 2019 15:44:18 -0500 Subject: [PATCH 12/27] C++ migration: port `FSTOnlineStateTracker` (#2325) --- .../Tests/Integration/FSTDatastoreTests.mm | 4 +- .../Tests/SpecTests/FSTSyncEngineTestDriver.h | 2 +- .../SpecTests/FSTSyncEngineTestDriver.mm | 18 +- Firestore/Source/Core/FSTEventManager.h | 4 +- Firestore/Source/Core/FSTFirestoreClient.h | 2 +- Firestore/Source/Core/FSTFirestoreClient.mm | 11 +- .../Source/Remote/FSTOnlineStateTracker.h | 72 ------- .../Source/Remote/FSTOnlineStateTracker.mm | 175 ------------------ Firestore/Source/Remote/FSTRemoteStore.h | 25 +-- Firestore/Source/Remote/FSTRemoteStore.mm | 42 ++--- .../firestore/remote/online_state_tracker.cc | 150 +++++++++++++++ .../firestore/remote/online_state_tracker.h | 124 +++++++++++++ 12 files changed, 318 insertions(+), 311 deletions(-) delete mode 100644 Firestore/Source/Remote/FSTOnlineStateTracker.h delete mode 100644 Firestore/Source/Remote/FSTOnlineStateTracker.mm create mode 100644 Firestore/core/src/firebase/firestore/remote/online_state_tracker.cc create mode 100644 Firestore/core/src/firebase/firestore/remote/online_state_tracker.h diff --git a/Firestore/Example/Tests/Integration/FSTDatastoreTests.mm b/Firestore/Example/Tests/Integration/FSTDatastoreTests.mm index 8abb74d2fc2..f057cb5deab 100644 --- a/Firestore/Example/Tests/Integration/FSTDatastoreTests.mm +++ b/Firestore/Example/Tests/Integration/FSTDatastoreTests.mm @@ -55,6 +55,7 @@ using firebase::firestore::model::DocumentKey; using firebase::firestore::model::DocumentKeySet; using firebase::firestore::model::Precondition; +using firebase::firestore::model::OnlineState; using firebase::firestore::model::TargetId; using firebase::firestore::remote::Datastore; using firebase::firestore::remote::GrpcConnection; @@ -186,7 +187,8 @@ - (void)setUp { _remoteStore = [[FSTRemoteStore alloc] initWithLocalStore:_localStore datastore:_datastore - workerQueue:_testWorkerQueue.get()]; + workerQueue:_testWorkerQueue.get() + onlineStateHandler:[](OnlineState) {}]; _testWorkerQueue->Enqueue([=] { [_remoteStore start]; }); } diff --git a/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h index 0622aa4290e..b3b2a1d2a5c 100644 --- a/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h +++ b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h @@ -86,7 +86,7 @@ typedef std::unordered_map +@interface FSTSyncEngineTestDriver : NSObject /** * Initializes the underlying FSTSyncEngine with the given local persistence implementation and diff --git a/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.mm b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.mm index e34349e716a..8d7b7a236da 100644 --- a/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.mm +++ b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.mm @@ -153,9 +153,14 @@ - (instancetype)initWithPersistence:(id)persistence _datastore = std::make_shared(_databaseInfo, _workerQueue.get(), &_credentialProvider); - _remoteStore = [[FSTRemoteStore alloc] initWithLocalStore:_localStore - datastore:_datastore - workerQueue:_workerQueue.get()]; + _remoteStore = + [[FSTRemoteStore alloc] initWithLocalStore:_localStore + datastore:_datastore + workerQueue:_workerQueue.get() + onlineStateHandler:[self](OnlineState onlineState) { + [self.syncEngine applyChangedOnlineState:onlineState]; + [self.eventManager applyChangedOnlineState:onlineState]; + }]; _syncEngine = [[FSTSyncEngine alloc] initWithLocalStore:_localStore remoteStore:_remoteStore @@ -163,8 +168,6 @@ - (instancetype)initWithPersistence:(id)persistence _remoteStore.syncEngine = _syncEngine; _eventManager = [FSTEventManager eventManagerWithSyncEngine:_syncEngine]; - _remoteStore.onlineStateDelegate = self; - // Set up internal event tracking for the spec tests. NSMutableArray *events = [NSMutableArray array]; _eventHandler = ^(FSTQueryEvent *e) { @@ -203,11 +206,6 @@ - (void)drainQueue { return _currentUser; } -- (void)applyChangedOnlineState:(OnlineState)onlineState { - [self.syncEngine applyChangedOnlineState:onlineState]; - [self.eventManager applyChangedOnlineState:onlineState]; -} - - (void)start { _workerQueue->EnqueueBlocking([&] { [self.localStore start]; diff --git a/Firestore/Source/Core/FSTEventManager.h b/Firestore/Source/Core/FSTEventManager.h index 67951b91984..814eb725833 100644 --- a/Firestore/Source/Core/FSTEventManager.h +++ b/Firestore/Source/Core/FSTEventManager.h @@ -75,7 +75,7 @@ NS_ASSUME_NONNULL_BEGIN * EventManager is responsible for mapping queries to query event emitters. It handles "fan-out." * (Identical queries will re-use the same watch on the backend.) */ -@interface FSTEventManager : NSObject +@interface FSTEventManager : NSObject + (instancetype)eventManagerWithSyncEngine:(FSTSyncEngine *)syncEngine; @@ -84,6 +84,8 @@ NS_ASSUME_NONNULL_BEGIN - (firebase::firestore::model::TargetId)addListener:(FSTQueryListener *)listener; - (void)removeListener:(FSTQueryListener *)listener; +- (void)applyChangedOnlineState:(firebase::firestore::model::OnlineState)onlineState; + @end NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTFirestoreClient.h b/Firestore/Source/Core/FSTFirestoreClient.h index f4d850dda6d..1d2e3abe66c 100644 --- a/Firestore/Source/Core/FSTFirestoreClient.h +++ b/Firestore/Source/Core/FSTFirestoreClient.h @@ -49,7 +49,7 @@ NS_ASSUME_NONNULL_BEGIN * SDK architecture. It is responsible for creating the worker queue that is shared by all of the * other components in the system. */ -@interface FSTFirestoreClient : NSObject +@interface FSTFirestoreClient : NSObject /** * Creates and returns a FSTFirestoreClient with the given parameters. diff --git a/Firestore/Source/Core/FSTFirestoreClient.mm b/Firestore/Source/Core/FSTFirestoreClient.mm index 9f6b33df461..6f9161f1706 100644 --- a/Firestore/Source/Core/FSTFirestoreClient.mm +++ b/Firestore/Source/Core/FSTFirestoreClient.mm @@ -228,7 +228,10 @@ - (void)initializeWithUser:(const User &)user settings:(FIRFirestoreSettings *)s _remoteStore = [[FSTRemoteStore alloc] initWithLocalStore:_localStore datastore:std::move(datastore) - workerQueue:_workerQueue.get()]; + workerQueue:_workerQueue.get() + onlineStateHandler:[self](OnlineState onlineState) { + [self.syncEngine applyChangedOnlineState:onlineState]; + }]; _syncEngine = [[FSTSyncEngine alloc] initWithLocalStore:_localStore remoteStore:_remoteStore @@ -239,8 +242,6 @@ - (void)initializeWithUser:(const User &)user settings:(FIRFirestoreSettings *)s // Setup wiring for remote store. _remoteStore.syncEngine = _syncEngine; - _remoteStore.onlineStateDelegate = self; - // NOTE: RemoteStore depends on LocalStore (for persisting stream tokens, refilling mutation // queue, etc.) so must be started after LocalStore. [_localStore start]; @@ -267,10 +268,6 @@ - (void)credentialDidChangeWithUser:(const User &)user { [self.syncEngine credentialDidChangeWithUser:user]; } -- (void)applyChangedOnlineState:(OnlineState)onlineState { - [self.syncEngine applyChangedOnlineState:onlineState]; -} - - (void)disableNetworkWithCompletion:(nullable FSTVoidErrorBlock)completion { _workerQueue->Enqueue([self, completion] { [self.remoteStore disableNetwork]; diff --git a/Firestore/Source/Remote/FSTOnlineStateTracker.h b/Firestore/Source/Remote/FSTOnlineStateTracker.h deleted file mode 100644 index 56aad0539a8..00000000000 --- a/Firestore/Source/Remote/FSTOnlineStateTracker.h +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2018 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 - -#include "Firestore/core/src/firebase/firestore/model/types.h" -#include "Firestore/core/src/firebase/firestore/util/async_queue.h" - -@protocol FSTOnlineStateDelegate; - -NS_ASSUME_NONNULL_BEGIN - -/** - * A component used by the FSTRemoteStore to track the OnlineState (that is, whether or not the - * client as a whole should be considered to be online or offline), implementing the appropriate - * heuristics. - * - * In particular, when the client is trying to connect to the backend, we allow up to - * kMaxWatchStreamFailures within kOnlineStateTimeout for a connection to succeed. If we have too - * many failures or the timeout elapses, then we set the OnlineState to Offline, and - * the client will behave as if it is offline (getDocument() calls will return cached data, etc.). - */ -@interface FSTOnlineStateTracker : NSObject - -- (instancetype)initWithWorkerQueue:(firebase::firestore::util::AsyncQueue *)queue; - -- (instancetype)init NS_UNAVAILABLE; - -/** A delegate to be notified on OnlineState changes. */ -@property(nonatomic, weak) id onlineStateDelegate; - -/** - * Called by FSTRemoteStore when a watch stream is started (including on each backoff attempt). - * - * If this is the first attempt, it sets the OnlineState to Unknown and starts the - * onlineStateTimer. - */ -- (void)handleWatchStreamStart; - -/** - * Called by FSTRemoteStore when a watch stream fails. - * - * Updates our OnlineState as appropriate. The first failure moves us to OnlineState::Unknown. - * We then may allow multiple failures (based on kMaxWatchStreamFailures) before we actually - * transition to OnlineState::Offline. - */ -- (void)handleWatchStreamFailure:(NSError *)error; - -/** - * Explicitly sets the OnlineState to the specified state. - * - * Note that this resets the timers / failure counters, etc. used by our Offline heuristics, so - * it must not be used in place of handleWatchStreamStart and handleWatchStreamFailure. - */ -- (void)updateState:(firebase::firestore::model::OnlineState)newState; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Remote/FSTOnlineStateTracker.mm b/Firestore/Source/Remote/FSTOnlineStateTracker.mm deleted file mode 100644 index be4feab4bfa..00000000000 --- a/Firestore/Source/Remote/FSTOnlineStateTracker.mm +++ /dev/null @@ -1,175 +0,0 @@ -/* - * Copyright 2018 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 "Firestore/Source/Remote/FSTOnlineStateTracker.h" - -#include // NOLINT(build/c++11) - -#import "Firestore/Source/Remote/FSTRemoteStore.h" - -#include "Firestore/core/src/firebase/firestore/util/executor.h" -#include "Firestore/core/src/firebase/firestore/util/hard_assert.h" -#include "Firestore/core/src/firebase/firestore/util/log.h" - -namespace chr = std::chrono; -using firebase::firestore::model::OnlineState; -using firebase::firestore::util::AsyncQueue; -using firebase::firestore::util::DelayedOperation; -using firebase::firestore::util::TimerId; - -NS_ASSUME_NONNULL_BEGIN - -namespace { - -// To deal with transient failures, we allow multiple stream attempts before giving up and -// transitioning from OnlineState Unknown to Offline. -// TODO(mikelehen): This used to be set to 2 as a mitigation for b/66228394. @jdimond thinks that -// bug is sufficiently fixed so that we can set this back to 1. If that works okay, we could -// potentially remove this logic entirely. -const int kMaxWatchStreamFailures = 1; - -// To deal with stream attempts that don't succeed or fail in a timely manner, we have a -// timeout for OnlineState to reach Online or Offline. If the timeout is reached, we transition -// to Offline rather than waiting indefinitely. -const AsyncQueue::Milliseconds kOnlineStateTimeout = chr::seconds(10); - -} // namespace - -@interface FSTOnlineStateTracker () - -/** The current OnlineState. */ -@property(nonatomic, assign) OnlineState state; - -/** - * A count of consecutive failures to open the stream. If it reaches the maximum defined by - * kMaxWatchStreamFailures, we'll revert to OnlineState::Offline. - */ -@property(nonatomic, assign) int watchStreamFailures; - -/** - * Whether the client should log a warning message if it fails to connect to the backend - * (initially YES, cleared after a successful stream, or if we've logged the message already). - */ -@property(nonatomic, assign) BOOL shouldWarnClientIsOffline; - -@end - -@implementation FSTOnlineStateTracker { - /** - * A timer that elapses after kOnlineStateTimeout, at which point we transition from OnlineState - * Unknown to Offline without waiting for the stream to actually fail (kMaxWatchStreamFailures - * times). - */ - DelayedOperation _onlineStateTimer; - - /** The worker queue to use for running timers (and to call onlineStateDelegate). */ - AsyncQueue *_workerQueue; -} - -- (instancetype)initWithWorkerQueue:(AsyncQueue *)workerQueue { - if (self = [super init]) { - _workerQueue = workerQueue; - _state = OnlineState::Unknown; - _shouldWarnClientIsOffline = YES; - } - return self; -} - -- (void)handleWatchStreamStart { - if (self.watchStreamFailures == 0) { - [self setAndBroadcastState:OnlineState::Unknown]; - - HARD_ASSERT(!_onlineStateTimer, "_onlineStateTimer shouldn't be started yet"); - _onlineStateTimer = - _workerQueue->EnqueueAfterDelay(kOnlineStateTimeout, TimerId::OnlineStateTimeout, [self] { - _onlineStateTimer = {}; - HARD_ASSERT(self.state == OnlineState::Unknown, - "Timer should be canceled if we transitioned to a different state."); - [self logClientOfflineWarningIfNecessaryWithReason: - [NSString stringWithFormat:@"Backend didn't respond within %lld seconds.", - chr::duration_cast(kOnlineStateTimeout) - .count()]]; - [self setAndBroadcastState:OnlineState::Offline]; - - // NOTE: handleWatchStreamFailure will continue to increment - // watchStreamFailures even though we are already marked Offline but this is - // non-harmful. - }); - } -} - -- (void)handleWatchStreamFailure:(NSError *)error { - if (self.state == OnlineState::Online) { - [self setAndBroadcastState:OnlineState::Unknown]; - - // To get to OnlineState::Online, updateState: must have been called which would have reset - // our heuristics. - HARD_ASSERT(self.watchStreamFailures == 0, "watchStreamFailures must be 0"); - HARD_ASSERT(!_onlineStateTimer, "_onlineStateTimer must not be set yet"); - } else { - self.watchStreamFailures++; - if (self.watchStreamFailures >= kMaxWatchStreamFailures) { - [self clearOnlineStateTimer]; - [self logClientOfflineWarningIfNecessaryWithReason: - [NSString stringWithFormat:@"Connection failed %d times. Most recent error: %@", - kMaxWatchStreamFailures, error]]; - [self setAndBroadcastState:OnlineState::Offline]; - } - } -} - -- (void)updateState:(OnlineState)newState { - [self clearOnlineStateTimer]; - self.watchStreamFailures = 0; - - if (newState == OnlineState::Online) { - // We've connected to watch at least once. Don't warn the developer about being offline going - // forward. - self.shouldWarnClientIsOffline = NO; - } - - [self setAndBroadcastState:newState]; -} - -- (void)setAndBroadcastState:(OnlineState)newState { - if (newState != self.state) { - self.state = newState; - [self.onlineStateDelegate applyChangedOnlineState:newState]; - } -} - -- (void)logClientOfflineWarningIfNecessaryWithReason:(NSString *)reason { - NSString *message = [NSString - stringWithFormat: - @"Could not reach Cloud Firestore backend. %@\n This typically indicates that your " - @"device does not have a healthy Internet connection at the moment. The client will " - @"operate in offline mode until it is able to successfully connect to the backend.", - reason]; - if (self.shouldWarnClientIsOffline) { - LOG_WARN("%s", message); - self.shouldWarnClientIsOffline = NO; - } else { - LOG_DEBUG("%s", message); - } -} - -- (void)clearOnlineStateTimer { - _onlineStateTimer.Cancel(); -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Remote/FSTRemoteStore.h b/Firestore/Source/Remote/FSTRemoteStore.h index 5ccb9956647..c52868bf788 100644 --- a/Firestore/Source/Remote/FSTRemoteStore.h +++ b/Firestore/Source/Remote/FSTRemoteStore.h @@ -16,6 +16,7 @@ #import +#include #include #include "Firestore/core/src/firebase/firestore/auth/user.h" @@ -83,18 +84,6 @@ NS_ASSUME_NONNULL_BEGIN @end -/** - * A protocol for the FSTRemoteStore online state delegate, called whenever the state of the - * online streams of the FSTRemoteStore changes. - * Note that this protocol only supports the watch stream for now. - */ -@protocol FSTOnlineStateDelegate - -/** Called whenever the online state of the watch stream changes */ -- (void)applyChangedOnlineState:(firebase::firestore::model::OnlineState)onlineState; - -@end - #pragma mark - FSTRemoteStore /** @@ -103,17 +92,17 @@ NS_ASSUME_NONNULL_BEGIN */ @interface FSTRemoteStore : NSObject -- (instancetype)initWithLocalStore:(FSTLocalStore *)localStore - datastore: - (std::shared_ptr)datastore - workerQueue:(firebase::firestore::util::AsyncQueue *)queue; +- (instancetype) + initWithLocalStore:(FSTLocalStore *)localStore + datastore:(std::shared_ptr)datastore + workerQueue:(firebase::firestore::util::AsyncQueue *)queue + onlineStateHandler: + (std::function)onlineStateHandler; - (instancetype)init NS_UNAVAILABLE; @property(nonatomic, weak) id syncEngine; -@property(nonatomic, weak) id onlineStateDelegate; - /** Starts up the remote store, creating streams, restoring state from LocalStore, etc. */ - (void)start; diff --git a/Firestore/Source/Remote/FSTRemoteStore.mm b/Firestore/Source/Remote/FSTRemoteStore.mm index 7c4bcd4e3e4..c765e5c46a7 100644 --- a/Firestore/Source/Remote/FSTRemoteStore.mm +++ b/Firestore/Source/Remote/FSTRemoteStore.mm @@ -28,13 +28,13 @@ #import "Firestore/Source/Model/FSTDocument.h" #import "Firestore/Source/Model/FSTMutation.h" #import "Firestore/Source/Model/FSTMutationBatch.h" -#import "Firestore/Source/Remote/FSTOnlineStateTracker.h" #import "Firestore/Source/Remote/FSTStream.h" #include "Firestore/core/src/firebase/firestore/auth/user.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" #include "Firestore/core/src/firebase/firestore/model/mutation_batch.h" #include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" +#include "Firestore/core/src/firebase/firestore/remote/online_state_tracker.h" #include "Firestore/core/src/firebase/firestore/remote/remote_event.h" #include "Firestore/core/src/firebase/firestore/remote/stream.h" #include "Firestore/core/src/firebase/firestore/util/error_apple.h" @@ -59,6 +59,7 @@ using firebase::firestore::remote::WriteStream; using firebase::firestore::remote::DocumentWatchChange; using firebase::firestore::remote::ExistenceFilterWatchChange; +using firebase::firestore::remote::OnlineStateTracker; using firebase::firestore::remote::RemoteEvent; using firebase::firestore::remote::TargetChange; using firebase::firestore::remote::WatchChange; @@ -88,8 +89,6 @@ @interface FSTRemoteStore () #pragma mark Watch Stream -@property(nonatomic, strong, readonly) FSTOnlineStateTracker *onlineStateTracker; - /** * A list of up to kMaxPendingWrites writes that we have fetched from the LocalStore via * fillWritePipeline and have or will send to the write stream. @@ -108,9 +107,11 @@ @interface FSTRemoteStore () @end @implementation FSTRemoteStore { + OnlineStateTracker _onlineStateTracker; + std::unique_ptr _watchChangeAggregator; - /** The client-side proxy for interacting with the backend. */ + /** The client-side proxy for interacting with the backend. */ std::shared_ptr _datastore; /** * A mapping of watched targets that the client cares about tracking and the @@ -133,13 +134,14 @@ @implementation FSTRemoteStore { - (instancetype)initWithLocalStore:(FSTLocalStore *)localStore datastore:(std::shared_ptr)datastore - workerQueue:(AsyncQueue *)queue { + workerQueue:(AsyncQueue *)queue + onlineStateHandler:(std::function)onlineStateHandler { if (self = [super init]) { _localStore = localStore; _datastore = std::move(datastore); _writePipeline = [NSMutableArray array]; - _onlineStateTracker = [[FSTOnlineStateTracker alloc] initWithWorkerQueue:queue]; + _onlineStateTracker = OnlineStateTracker{queue, std::move(onlineStateHandler)}; _datastore->Start(); // Create streams (but note they're not started yet) @@ -156,16 +158,6 @@ - (void)start { [self enableNetwork]; } -@dynamic onlineStateDelegate; - -- (nullable id)onlineStateDelegate { - return self.onlineStateTracker.onlineStateDelegate; -} - -- (void)setOnlineStateDelegate:(nullable id)delegate { - self.onlineStateTracker.onlineStateDelegate = delegate; -} - #pragma mark Online/Offline state - (BOOL)canUseNetwork { @@ -184,7 +176,7 @@ - (void)enableNetwork { if ([self shouldStartWatchStream]) { [self startWatchStream]; } else { - [self.onlineStateTracker updateState:OnlineState::Unknown]; + _onlineStateTracker.UpdateState(OnlineState::Unknown); } // This will start the write stream if necessary. @@ -197,7 +189,7 @@ - (void)disableNetwork { [self disableNetworkInternal]; // Set the OnlineState to Offline so get()s return from cache, etc. - [self.onlineStateTracker updateState:OnlineState::Offline]; + _onlineStateTracker.UpdateState(OnlineState::Offline); } /** Disables the network, setting the OnlineState to the specified targetOnlineState. */ @@ -222,7 +214,7 @@ - (void)shutdown { [self disableNetworkInternal]; // Set the OnlineState to Unknown (rather than Offline) to avoid potentially triggering // spurious listener events with cached data, etc. - [self.onlineStateTracker updateState:OnlineState::Unknown]; + _onlineStateTracker.UpdateState(OnlineState::Unknown); _datastore->Shutdown(); } @@ -234,7 +226,7 @@ - (void)credentialDidChange { LOG_DEBUG("FSTRemoteStore %s restarting streams for new credential", (__bridge void *)self); _isNetworkEnabled = NO; [self disableNetworkInternal]; - [self.onlineStateTracker updateState:OnlineState::Unknown]; + _onlineStateTracker.UpdateState(OnlineState::Unknown); [self enableNetwork]; } } @@ -247,7 +239,7 @@ - (void)startWatchStream { _watchChangeAggregator = absl::make_unique(self); _watchStream->Start(); - [self.onlineStateTracker handleWatchStreamStart]; + _onlineStateTracker.HandleWatchStreamStart(); } - (void)listenToTargetWithQueryData:(FSTQueryData *)queryData { @@ -284,7 +276,7 @@ - (void)stopListeningToTargetID:(TargetId)targetID { // Revert to OnlineState::Unknown if the watch stream is not open and we have no listeners, // since without any listens to send we cannot confirm if the stream is healthy and upgrade // to OnlineState::Online. - [self.onlineStateTracker updateState:OnlineState::Unknown]; + _onlineStateTracker.UpdateState(OnlineState::Unknown); } } } @@ -316,7 +308,7 @@ - (void)watchStreamDidOpen { - (void)watchStreamDidChange:(const WatchChange &)change snapshotVersion:(const SnapshotVersion &)snapshotVersion { // Mark the connection as Online because we got a message from the server. - [self.onlineStateTracker updateState:OnlineState::Online]; + _onlineStateTracker.UpdateState(OnlineState::Online); if (change.type() == WatchChange::Type::TargetChange) { const WatchTargetChange &watchTargetChange = static_cast(change); @@ -355,13 +347,13 @@ - (void)watchStreamWasInterruptedWithError:(const Status &)error { // If we still need the watch stream, retry the connection. if ([self shouldStartWatchStream]) { - [self.onlineStateTracker handleWatchStreamFailure:util::MakeNSError(error)]; + _onlineStateTracker.HandleWatchStreamFailure(error); [self startWatchStream]; } else { // We don't need to restart the watch stream because there are no active targets. The online // state is set to unknown because there is no active attempt at establishing a connection. - [self.onlineStateTracker updateState:OnlineState::Unknown]; + _onlineStateTracker.UpdateState(OnlineState::Unknown); } } diff --git a/Firestore/core/src/firebase/firestore/remote/online_state_tracker.cc b/Firestore/core/src/firebase/firestore/remote/online_state_tracker.cc new file mode 100644 index 00000000000..afdc69d93d4 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/remote/online_state_tracker.cc @@ -0,0 +1,150 @@ +/* + * Copyright 2019 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. + */ + +#include "Firestore/core/src/firebase/firestore/remote/online_state_tracker.h" + +#include // NOLINT(build/c++11) + +#include "Firestore/core/src/firebase/firestore/util/executor.h" +#include "Firestore/core/src/firebase/firestore/util/hard_assert.h" +#include "Firestore/core/src/firebase/firestore/util/log.h" +#include "Firestore/core/src/firebase/firestore/util/string_format.h" + +namespace chr = std::chrono; +using firebase::firestore::model::OnlineState; +using firebase::firestore::util::AsyncQueue; +using firebase::firestore::util::DelayedOperation; +using firebase::firestore::util::Status; +using firebase::firestore::util::StringFormat; +using firebase::firestore::util::TimerId; + +namespace { + +// To deal with transient failures, we allow multiple stream attempts before +// giving up and transitioning from OnlineState Unknown to Offline. +// TODO(mikelehen): This used to be set to 2 as a mitigation for b/66228394. +// @jdimond thinks that bug is sufficiently fixed so that we can set this back +// to 1. If that works okay, we could potentially remove this logic entirely. +const int kMaxWatchStreamFailures = 1; + +// To deal with stream attempts that don't succeed or fail in a timely manner, +// we have a timeout for OnlineState to reach Online or Offline. If the timeout +// is reached, we transition to Offline rather than waiting indefinitely. +const AsyncQueue::Milliseconds kOnlineStateTimeout = chr::seconds(10); + +} // namespace + +namespace firebase { +namespace firestore { +namespace remote { + +void OnlineStateTracker::HandleWatchStreamStart() { + if (watch_stream_failures_ != 0) { + return; + } + + SetAndBroadcast(OnlineState::Unknown); + + HARD_ASSERT(!online_state_timer_, + "online_state_timer_ shouldn't be started yet"); + online_state_timer_ = worker_queue_->EnqueueAfterDelay( + kOnlineStateTimeout, TimerId::OnlineStateTimeout, [this] { + online_state_timer_ = {}; + + HARD_ASSERT(state_ == OnlineState::Unknown, + "Timer should be canceled if we transitioned to a " + "different state."); + LogClientOfflineWarningIfNecessary(StringFormat( + "Backend didn't respond within %s seconds.", + chr::duration_cast(kOnlineStateTimeout).count())); + SetAndBroadcast(OnlineState::Offline); + + // NOTE: `HandleWatchStreamFailure` will continue to increment + // `watch_stream_failures_` even though we are already marked `Offline` + // but this is non-harmful. + }); +} + +void OnlineStateTracker::HandleWatchStreamFailure(const Status& error) { + if (state_ == OnlineState::Online) { + SetAndBroadcast(OnlineState::Unknown); + + // To get to `OnlineState`::Online, `UpdateState` must have been called + // which would have reset our heuristics. + HARD_ASSERT(watch_stream_failures_ == 0, + "watch_stream_failures_ must be 0"); + HARD_ASSERT(!online_state_timer_, + "online_state_timer_ must not be set yet"); + } else { + ++watch_stream_failures_; + + if (watch_stream_failures_ >= kMaxWatchStreamFailures) { + ClearOnlineStateTimer(); + + LogClientOfflineWarningIfNecessary( + StringFormat("Connection failed %s times. Most recent error: %s", + kMaxWatchStreamFailures, error.error_message())); + + SetAndBroadcast(OnlineState::Offline); + } + } +} + +void OnlineStateTracker::UpdateState(OnlineState new_state) { + ClearOnlineStateTimer(); + watch_stream_failures_ = 0; + + if (new_state == OnlineState::Online) { + // We've connected to watch at least once. Don't warn the developer about + // being offline going forward. + should_warn_client_is_offline_ = false; + } + + SetAndBroadcast(new_state); +} + +void OnlineStateTracker::SetAndBroadcast(OnlineState new_state) { + if (new_state != state_) { + state_ = new_state; + online_state_handler_(new_state); + } +} + +void OnlineStateTracker::LogClientOfflineWarningIfNecessary( + const std::string& reason) { + std::string message = StringFormat( + "Could not reach Cloud Firestore backend. %s\n This " + "typically indicates that your device does not have a " + "healthy Internet connection at the moment. The client will " + "operate in offline mode until it is able to successfully " + "connect to the backend.", + reason); + + if (should_warn_client_is_offline_) { + LOG_WARN("%s", message); + should_warn_client_is_offline_ = false; + } else { + LOG_DEBUG("%s", message); + } +} + +void OnlineStateTracker::ClearOnlineStateTimer() { + online_state_timer_.Cancel(); +} + +} // namespace remote +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/src/firebase/firestore/remote/online_state_tracker.h b/Firestore/core/src/firebase/firestore/remote/online_state_tracker.h new file mode 100644 index 00000000000..2344f520a39 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/remote/online_state_tracker.h @@ -0,0 +1,124 @@ +/* + * Copyright 2019 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. + */ + +#ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_REMOTE_ONLINE_STATE_TRACKER_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_REMOTE_ONLINE_STATE_TRACKER_H_ + +#include +#include + +#include "Firestore/core/src/firebase/firestore/model/types.h" +#include "Firestore/core/src/firebase/firestore/util/async_queue.h" +#include "Firestore/core/src/firebase/firestore/util/status.h" + +namespace firebase { +namespace firestore { +namespace remote { + +/** + * A component used by the `FSTRemoteStore` to track the `OnlineState` (that is, + * whether or not the client as a whole should be considered to be online or + * offline), implementing the appropriate heuristics. + * + * In particular, when the client is trying to connect to the backend, we allow + * up to `kMaxWatchStreamFailures` within `kOnlineStateTimeout` for a connection + * to succeed. If we have too many failures or the timeout elapses, then we set + * the `OnlineState` to `Offline`, and the client will behave as if it is + * offline (`getDocument()` calls will return cached data, etc.). + */ +class OnlineStateTracker { + public: + OnlineStateTracker() = default; + + OnlineStateTracker( + util::AsyncQueue* worker_queue, + std::function online_state_handler) + : worker_queue_{worker_queue}, + online_state_handler_{online_state_handler} { + } + + /** + * Called by `FSTRemoteStore` when a watch stream is started (including on + * each backoff attempt). + * + * If this is the first attempt, it sets the `OnlineState` to `Unknown` and + * starts the `onlineStateTimer`. + */ + void HandleWatchStreamStart(); + + /** + * Called by `FSTRemoteStore` when a watch stream fails. + * + * Updates our `OnlineState` as appropriate. The first failure moves us to + * `OnlineState::Unknown`. We then may allow multiple failures (based on + * `kMaxWatchStreamFailures`) before we actually transition to + * `OnlineState::Offline`. + */ + void HandleWatchStreamFailure(const util::Status& error); + + /** + * Explicitly sets the `OnlineState` to the specified state. + * + * Note that this resets the timers / failure counters, etc. used by our + * `Offline` heuristics, so it must not be used in place of + * `HandleWatchStreamStart` and `HandleWatchStreamFailure`. + */ + void UpdateState(model::OnlineState new_state); + + private: + void SetAndBroadcast(model::OnlineState new_state); + void LogClientOfflineWarningIfNecessary(const std::string& reason); + void ClearOnlineStateTimer(); + + /** The current `OnlineState`. */ + model::OnlineState state_ = model::OnlineState::Unknown; + + /** + * A count of consecutive failures to open the stream. If it reaches the + * maximum defined by `kMaxWatchStreamFailures`, we'll revert to + * `OnlineState::Offline`. + */ + int watch_stream_failures_ = 0; + + /** + * A timer that elapses after `kOnlineStateTimeout`, at which point we + * transition from `OnlineState` `Unknown` to `Offline` without waiting for + * the stream to actually fail (`kMaxWatchStreamFailures` times). + */ + util::DelayedOperation online_state_timer_; + + /** + * Whether the client should log a warning message if it fails to connect to + * the backend (initially true, cleared after a successful stream, or if we've + * logged the message already). + */ + bool should_warn_client_is_offline_ = true; + + /** + * The worker queue to use for running timers (and to call + * `online_state_handler_`). + */ + util::AsyncQueue* worker_queue_ = nullptr; + + /** A callback to be notified on `OnlineState` changes. */ + std::function online_state_handler_; +}; + +} // namespace remote +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_REMOTE_ONLINE_STATE_TRACKER_H_ From 11b5ddb13dfa2d25e2ef1de3dbea9e4a065553d4 Mon Sep 17 00:00:00 2001 From: Michael Lehenbauer Date: Fri, 1 Feb 2019 14:21:38 -0800 Subject: [PATCH 13/27] Make scripts/style.sh compatible with newer swiftformat versions (0.38) (#2334) * Log swiftformat version in non-interactive builds. * Use a mildly-terrible regex for version check to ensure at least 0.35.0 (travis has 0.35.8). * Include StorageViewController.swift change swiftformat 0.38 makes (but which seems to be compatible with 0.35) --- .../tvOSSample/StorageViewController.swift | 2 +- scripts/style.sh | 14 ++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/Example/tvOSSample/tvOSSample/StorageViewController.swift b/Example/tvOSSample/tvOSSample/StorageViewController.swift index 2e91d92ae01..ed76d7641b7 100644 --- a/Example/tvOSSample/tvOSSample/StorageViewController.swift +++ b/Example/tvOSSample/tvOSSample/StorageViewController.swift @@ -42,7 +42,7 @@ class StorageViewController: UIViewController { } } - /// MARK: - Properties + // MARK: - Properties /// The current internal state of the view controller. private var state: UIState = .cleared { diff --git a/scripts/style.sh b/scripts/style.sh index 742c6069853..7565452e8d7 100755 --- a/scripts/style.sh +++ b/scripts/style.sh @@ -58,11 +58,17 @@ esac system=$(uname -s) if [[ "$system" == "Darwin" ]]; then version=$(swiftformat --version) + # Log the version in non-interactive use as it can be useful in travis logs. + if [[ ! -t 1 ]]; then + echo "Found: $version" + fi version="${version/*version /}" - # Allow an older swiftformat because travis isn't running High Sierra yet - # and the formula hasn't been updated in a while on Sierra :-/. - if [[ "$version" != "0.32.0" && "$version" != "0.33"* && "$version" != "0.35"* && "$version" != "0.37"* ]]; then - echo "Version $version installed. Please upgrade to at least swiftformat 0.33.8" + # Ensure the swiftformat version is at least 0.35.x since (as of 2019-02-01) + # travis runs 0.35.7. We may need to be more strict about version checks in + # the future if we run into different versions making incompatible format + # changes. + if [[ ! "$version" =~ ^0.3[5-9] && ! "$version" =~ ^0.[4-9] ]]; then + echo "Version $version installed. Please upgrade to at least swiftformat 0.35.0" echo "If it's installed via homebrew you can run: brew upgrade swiftformat" exit 1 fi From a43709f18e8e8e9d635bdb4b42f9412f821076b1 Mon Sep 17 00:00:00 2001 From: Michael Lehenbauer Date: Fri, 1 Feb 2019 15:02:12 -0800 Subject: [PATCH 14/27] Port flaky test fix from web. (#2332) Port flaky test fix from web. Port of https://github.com/firebase/firebase-js-sdk/pull/1511 --- .../Example/Tests/Integration/API/FIRServerTimestampTests.mm | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Firestore/Example/Tests/Integration/API/FIRServerTimestampTests.mm b/Firestore/Example/Tests/Integration/API/FIRServerTimestampTests.mm index 860c8aac9ee..ba5fa55a2db 100644 --- a/Firestore/Example/Tests/Integration/API/FIRServerTimestampTests.mm +++ b/Firestore/Example/Tests/Integration/API/FIRServerTimestampTests.mm @@ -220,7 +220,8 @@ - (void)testServerTimestampsWithConsecutiveUpdates { serverTimestampBehavior:FIRServerTimestampBehaviorPrevious], @42); - [_docRef updateData:@{@"a" : [FIRFieldValue fieldValueForServerTimestamp]}]; + // include b=1 to ensure there's a change resulting in a new snapshot. + [_docRef updateData:@{@"a" : [FIRFieldValue fieldValueForServerTimestamp], @"b" : @1}]; localSnapshot = [_accumulator awaitLocalEvent]; XCTAssertEqualObjects([localSnapshot valueForField:@"a" serverTimestampBehavior:FIRServerTimestampBehaviorPrevious], From 3905bd250a89ba899c68466006285bfdb86c75ca Mon Sep 17 00:00:00 2001 From: christibbs <43829046+christibbs@users.noreply.github.com> Date: Fri, 1 Feb 2019 17:05:36 -0800 Subject: [PATCH 15/27] Open source FIAM headless SDK (#2312) --- .../Analytics/FIRIAMAnalyticsEventLogger.h | 61 ++ .../FIRIAMAnalyticsEventLoggerImpl.h | 45 + .../FIRIAMAnalyticsEventLoggerImpl.m | 170 ++++ .../FIRIAMClearcutHttpRequestSender.h | 51 ++ .../FIRIAMClearcutHttpRequestSender.m | 202 +++++ .../Analytics/FIRIAMClearcutLogStorage.h | 48 + .../Analytics/FIRIAMClearcutLogStorage.m | 171 ++++ .../Analytics/FIRIAMClearcutLogger.h | 46 + .../Analytics/FIRIAMClearcutLogger.m | 203 +++++ .../Analytics/FIRIAMClearcutUploader.h | 75 ++ .../Analytics/FIRIAMClearcutUploader.m | 233 +++++ Firebase/InAppMessaging/CHANGELOG.md | 6 + .../Data/FIRIAMFetchResponseParser.h | 40 + .../Data/FIRIAMFetchResponseParser.m | 313 +++++++ .../Data/FIRIAMMessageContentData.h | 42 + .../FIRIAMMessageContentDataWithImageURL.h | 47 + .../FIRIAMMessageContentDataWithImageURL.m | 138 +++ .../Data/FIRIAMMessageDefinition.h | 60 ++ .../Data/FIRIAMMessageDefinition.m | 85 ++ .../Data/FIRIAMMessageRenderData.h | 36 + .../Data/FIRIAMRenderingEffectSetting.h | 56 ++ .../Data/FIRIAMRenderingEffectSetting.m | 31 + .../FIRIAMDisplayTriggerDefinition.h | 34 + .../FIRIAMDisplayTriggerDefinition.m | 33 + .../InAppMessaging/FIRCore+InAppMessaging.h | 36 + .../InAppMessaging/FIRCore+InAppMessaging.m | 22 + Firebase/InAppMessaging/FIRInAppMessaging.m | 144 +++ .../InAppMessaging/FIRInAppMessagingPrivate.h | 26 + .../Flows/FIRIAMActivityLogger.h | 89 ++ .../Flows/FIRIAMActivityLogger.m | 215 +++++ .../InAppMessaging/Flows/FIRIAMBookKeeper.h | 75 ++ .../InAppMessaging/Flows/FIRIAMBookKeeper.m | 260 ++++++ .../Flows/FIRIAMClientInfoFetcher.h | 39 + .../Flows/FIRIAMClientInfoFetcher.m | 120 +++ .../FIRIAMDisplayCheckOnAnalyticEventsFlow.h | 22 + .../FIRIAMDisplayCheckOnAnalyticEventsFlow.m | 66 ++ .../FIRIAMDisplayCheckOnAppForegroundFlow.h | 23 + .../FIRIAMDisplayCheckOnAppForegroundFlow.m | 55 ++ ...MDisplayCheckOnFetchDoneNotificationFlow.h | 22 + ...MDisplayCheckOnFetchDoneNotificationFlow.m | 62 ++ .../Flows/FIRIAMDisplayCheckTriggerFlow.h | 37 + .../Flows/FIRIAMDisplayCheckTriggerFlow.m | 32 + .../Flows/FIRIAMDisplayExecutor.h | 61 ++ .../Flows/FIRIAMDisplayExecutor.m | 497 +++++++++++ .../InAppMessaging/Flows/FIRIAMFetchFlow.h | 59 ++ .../InAppMessaging/Flows/FIRIAMFetchFlow.m | 253 ++++++ .../Flows/FIRIAMFetchOnAppForegroundFlow.h | 22 + .../Flows/FIRIAMFetchOnAppForegroundFlow.m | 51 ++ .../Flows/FIRIAMMessageClientCache.h | 91 ++ .../Flows/FIRIAMMessageClientCache.m | 223 +++++ .../Flows/FIRIAMMsgFetcherUsingRestful.h | 55 ++ .../Flows/FIRIAMMsgFetcherUsingRestful.m | 272 ++++++ .../Flows/FIRIAMServerMsgFetchStorage.h | 30 + .../Flows/FIRIAMServerMsgFetchStorage.m | 64 ++ .../InAppMessaging/Public/FIRInAppMessaging.h | 80 ++ .../Public/FIRInAppMessagingRendering.h | 251 ++++++ .../FIRInAppMessagingRenderingDataClasses.m | 108 +++ .../Runtime/FIRIAMActionURLFollower.h | 46 + .../Runtime/FIRIAMActionURLFollower.m | 244 ++++++ .../Runtime/FIRIAMRuntimeManager.h | 56 ++ .../Runtime/FIRIAMRuntimeManager.m | 431 +++++++++ .../Runtime/FIRIAMSDKModeManager.h | 73 ++ .../Runtime/FIRIAMSDKModeManager.m | 113 +++ .../Runtime/FIRIAMSDKRuntimeErrorCodes.h | 25 + .../Runtime/FIRIAMSDKSettings.h | 53 ++ .../Runtime/FIRIAMSDKSettings.m | 35 + .../Runtime/FIRInAppMessaging+Bootstrap.h | 33 + .../Runtime/FIRInAppMessaging+Bootstrap.m | 137 +++ .../Util/FIRIAMElapsedTimeTracker.h | 29 + .../Util/FIRIAMElapsedTimeTracker.m | 56 ++ .../InAppMessaging/Util/FIRIAMTimeFetcher.h | 28 + .../InAppMessaging/Util/FIRIAMTimeFetcher.m | 23 + .../Util/NSString+FIRInterlaceStrings.h | 30 + .../Util/NSString+FIRInterlaceStrings.m | 42 + .../Util/UIColor+FIRIAMHexString.h | 31 + .../Util/UIColor+FIRIAMHexString.m | 39 + Firebase/InAppMessaging/firebase_28dp.png | Bin 0 -> 1428 bytes FirebaseInAppMessaging.podspec | 39 + .../InAppMessaging-Example-iOS/AppDelegate.h | 36 + .../InAppMessaging-Example-iOS/AppDelegate.m | 118 +++ .../AppIcon.appiconset/Contents.json | 93 ++ .../AutoDisplayFlowViewController.h | 20 + .../AutoDisplayFlowViewController.m | 170 ++++ .../AutoDisplayMesagesTableVC.h | 23 + .../AutoDisplayMesagesTableVC.m | 137 +++ .../Base.lproj/LaunchScreen.storyboard | 27 + .../Base.lproj/Main.storyboard | 395 +++++++++ .../GoogleService-Info.plist | 28 + .../App/InAppMessaging-Example-iOS/Info.plist | 67 ++ .../LogDumpViewController.h | 21 + .../LogDumpViewController.m | 73 ++ .../App/InAppMessaging-Example-iOS/Podfile | 34 + .../App/InAppMessaging-Example-iOS/main.m | 24 + .../GoogleService-Info.plist | 28 + .../fiam-external-ios-testing-app/Podfile | 18 + .../project.pbxproj | 423 +++++++++ .../AppDelegate.h | 21 + .../AppDelegate.m | 70 ++ .../AppIcon.appiconset/Contents.json | 93 ++ .../Base.lproj/LaunchScreen.storyboard | 31 + .../Base.lproj/Main.storyboard | 51 ++ .../fiam-external-ios-testing-app/Info.plist | 60 ++ .../ViewController.h | 19 + .../ViewController.m | 39 + .../fiam-external-ios-testing-app/main.m | 24 + .../Example/FIRIAMSDKModeManagerTests.m | 137 +++ .../project.pbxproj | 828 ++++++++++++++++++ .../InAppMessaging_Example_iOS.xcscheme | 108 +++ .../InAppMessaging_Tests_iOS.xcscheme | 69 ++ .../InAppMessaging_Example_iOS.entitlements | 10 + ...ppMessaging_Example_iOS_SwiftUITests.swift | 680 ++++++++++++++ .../Info.plist | 22 + InAppMessaging/Example/Podfile | 27 + InAppMessaging/Example/Scanfile | 13 + InAppMessaging/Example/Snapfile | 37 + .../Example/TestJsonDataFromFetch.txt | 169 ++++ .../TestJsonDataWithTestMessageFromFetch.txt | 71 ++ .../Tests/FIRIAMActionUrlFollowerTests.m | 270 ++++++ .../Example/Tests/FIRIAMActivityLoggerTests.m | 185 ++++ .../FIRIAMAnalyticsEventLoggerImplTests.m | 224 +++++ .../FIRIAMBookKeeperViaUserDefaultsTests.m | 187 ++++ .../Example/Tests/FIRIAMClearcutLoggerTests.m | 144 +++ .../FIRIAMClearcutRetryLocalStorageTests.m | 81 ++ .../Tests/FIRIAMClearcutUploaderTests.m | 373 ++++++++ .../Tests/FIRIAMDisplayExecutorTests.m | 761 ++++++++++++++++ .../Tests/FIRIAMElapsedTimeTrackerTests.m | 72 ++ .../Example/Tests/FIRIAMFetchFlowTests.m | 331 +++++++ .../Tests/FIRIAMFetchResponseParserTests.m | 180 ++++ .../Tests/FIRIAMMessageClientCacheTests.m | 378 ++++++++ ...IRIAMMessageContentDataWithImageURLTests.m | 241 +++++ .../Tests/FIRIAMMsgFetcherUsingRestfulTests.m | 233 +++++ InAppMessaging/Example/Tests/Info.plist | 22 + .../JsonDataWithInvalidMessagesFromFetch.txt | 64 ++ .../Tests/NSString+InterlaceStringsTests.m | 60 ++ .../Tests/UIColor+FIRIAMHexStringTests.m | 59 ++ .../Analytics/Public/FIRAnalyticsInterop.h | 8 + .../Public/FIRAnalyticsInteropListener.h | 24 + 137 files changed, 15462 insertions(+) create mode 100644 Firebase/InAppMessaging/Analytics/FIRIAMAnalyticsEventLogger.h create mode 100644 Firebase/InAppMessaging/Analytics/FIRIAMAnalyticsEventLoggerImpl.h create mode 100644 Firebase/InAppMessaging/Analytics/FIRIAMAnalyticsEventLoggerImpl.m create mode 100644 Firebase/InAppMessaging/Analytics/FIRIAMClearcutHttpRequestSender.h create mode 100644 Firebase/InAppMessaging/Analytics/FIRIAMClearcutHttpRequestSender.m create mode 100644 Firebase/InAppMessaging/Analytics/FIRIAMClearcutLogStorage.h create mode 100644 Firebase/InAppMessaging/Analytics/FIRIAMClearcutLogStorage.m create mode 100644 Firebase/InAppMessaging/Analytics/FIRIAMClearcutLogger.h create mode 100644 Firebase/InAppMessaging/Analytics/FIRIAMClearcutLogger.m create mode 100644 Firebase/InAppMessaging/Analytics/FIRIAMClearcutUploader.h create mode 100644 Firebase/InAppMessaging/Analytics/FIRIAMClearcutUploader.m create mode 100644 Firebase/InAppMessaging/CHANGELOG.md create mode 100644 Firebase/InAppMessaging/Data/FIRIAMFetchResponseParser.h create mode 100644 Firebase/InAppMessaging/Data/FIRIAMFetchResponseParser.m create mode 100644 Firebase/InAppMessaging/Data/FIRIAMMessageContentData.h create mode 100644 Firebase/InAppMessaging/Data/FIRIAMMessageContentDataWithImageURL.h create mode 100644 Firebase/InAppMessaging/Data/FIRIAMMessageContentDataWithImageURL.m create mode 100644 Firebase/InAppMessaging/Data/FIRIAMMessageDefinition.h create mode 100644 Firebase/InAppMessaging/Data/FIRIAMMessageDefinition.m create mode 100644 Firebase/InAppMessaging/Data/FIRIAMMessageRenderData.h create mode 100644 Firebase/InAppMessaging/Data/FIRIAMRenderingEffectSetting.h create mode 100644 Firebase/InAppMessaging/Data/FIRIAMRenderingEffectSetting.m create mode 100644 Firebase/InAppMessaging/DisplayTrigger/FIRIAMDisplayTriggerDefinition.h create mode 100644 Firebase/InAppMessaging/DisplayTrigger/FIRIAMDisplayTriggerDefinition.m create mode 100644 Firebase/InAppMessaging/FIRCore+InAppMessaging.h create mode 100644 Firebase/InAppMessaging/FIRCore+InAppMessaging.m create mode 100644 Firebase/InAppMessaging/FIRInAppMessaging.m create mode 100644 Firebase/InAppMessaging/FIRInAppMessagingPrivate.h create mode 100644 Firebase/InAppMessaging/Flows/FIRIAMActivityLogger.h create mode 100644 Firebase/InAppMessaging/Flows/FIRIAMActivityLogger.m create mode 100644 Firebase/InAppMessaging/Flows/FIRIAMBookKeeper.h create mode 100644 Firebase/InAppMessaging/Flows/FIRIAMBookKeeper.m create mode 100644 Firebase/InAppMessaging/Flows/FIRIAMClientInfoFetcher.h create mode 100644 Firebase/InAppMessaging/Flows/FIRIAMClientInfoFetcher.m create mode 100644 Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckOnAnalyticEventsFlow.h create mode 100644 Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckOnAnalyticEventsFlow.m create mode 100644 Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckOnAppForegroundFlow.h create mode 100644 Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckOnAppForegroundFlow.m create mode 100644 Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckOnFetchDoneNotificationFlow.h create mode 100644 Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckOnFetchDoneNotificationFlow.m create mode 100644 Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckTriggerFlow.h create mode 100644 Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckTriggerFlow.m create mode 100644 Firebase/InAppMessaging/Flows/FIRIAMDisplayExecutor.h create mode 100644 Firebase/InAppMessaging/Flows/FIRIAMDisplayExecutor.m create mode 100644 Firebase/InAppMessaging/Flows/FIRIAMFetchFlow.h create mode 100644 Firebase/InAppMessaging/Flows/FIRIAMFetchFlow.m create mode 100644 Firebase/InAppMessaging/Flows/FIRIAMFetchOnAppForegroundFlow.h create mode 100644 Firebase/InAppMessaging/Flows/FIRIAMFetchOnAppForegroundFlow.m create mode 100644 Firebase/InAppMessaging/Flows/FIRIAMMessageClientCache.h create mode 100644 Firebase/InAppMessaging/Flows/FIRIAMMessageClientCache.m create mode 100644 Firebase/InAppMessaging/Flows/FIRIAMMsgFetcherUsingRestful.h create mode 100644 Firebase/InAppMessaging/Flows/FIRIAMMsgFetcherUsingRestful.m create mode 100644 Firebase/InAppMessaging/Flows/FIRIAMServerMsgFetchStorage.h create mode 100644 Firebase/InAppMessaging/Flows/FIRIAMServerMsgFetchStorage.m create mode 100644 Firebase/InAppMessaging/Public/FIRInAppMessaging.h create mode 100644 Firebase/InAppMessaging/Public/FIRInAppMessagingRendering.h create mode 100644 Firebase/InAppMessaging/RenderingObjects/FIRInAppMessagingRenderingDataClasses.m create mode 100644 Firebase/InAppMessaging/Runtime/FIRIAMActionURLFollower.h create mode 100644 Firebase/InAppMessaging/Runtime/FIRIAMActionURLFollower.m create mode 100644 Firebase/InAppMessaging/Runtime/FIRIAMRuntimeManager.h create mode 100644 Firebase/InAppMessaging/Runtime/FIRIAMRuntimeManager.m create mode 100644 Firebase/InAppMessaging/Runtime/FIRIAMSDKModeManager.h create mode 100644 Firebase/InAppMessaging/Runtime/FIRIAMSDKModeManager.m create mode 100644 Firebase/InAppMessaging/Runtime/FIRIAMSDKRuntimeErrorCodes.h create mode 100644 Firebase/InAppMessaging/Runtime/FIRIAMSDKSettings.h create mode 100644 Firebase/InAppMessaging/Runtime/FIRIAMSDKSettings.m create mode 100644 Firebase/InAppMessaging/Runtime/FIRInAppMessaging+Bootstrap.h create mode 100644 Firebase/InAppMessaging/Runtime/FIRInAppMessaging+Bootstrap.m create mode 100644 Firebase/InAppMessaging/Util/FIRIAMElapsedTimeTracker.h create mode 100644 Firebase/InAppMessaging/Util/FIRIAMElapsedTimeTracker.m create mode 100644 Firebase/InAppMessaging/Util/FIRIAMTimeFetcher.h create mode 100644 Firebase/InAppMessaging/Util/FIRIAMTimeFetcher.m create mode 100644 Firebase/InAppMessaging/Util/NSString+FIRInterlaceStrings.h create mode 100644 Firebase/InAppMessaging/Util/NSString+FIRInterlaceStrings.m create mode 100644 Firebase/InAppMessaging/Util/UIColor+FIRIAMHexString.h create mode 100644 Firebase/InAppMessaging/Util/UIColor+FIRIAMHexString.m create mode 100644 Firebase/InAppMessaging/firebase_28dp.png create mode 100644 FirebaseInAppMessaging.podspec create mode 100644 InAppMessaging/Example/App/InAppMessaging-Example-iOS/AppDelegate.h create mode 100644 InAppMessaging/Example/App/InAppMessaging-Example-iOS/AppDelegate.m create mode 100644 InAppMessaging/Example/App/InAppMessaging-Example-iOS/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 InAppMessaging/Example/App/InAppMessaging-Example-iOS/AutoDisplayFlowViewController.h create mode 100644 InAppMessaging/Example/App/InAppMessaging-Example-iOS/AutoDisplayFlowViewController.m create mode 100644 InAppMessaging/Example/App/InAppMessaging-Example-iOS/AutoDisplayMesagesTableVC.h create mode 100644 InAppMessaging/Example/App/InAppMessaging-Example-iOS/AutoDisplayMesagesTableVC.m create mode 100644 InAppMessaging/Example/App/InAppMessaging-Example-iOS/Base.lproj/LaunchScreen.storyboard create mode 100644 InAppMessaging/Example/App/InAppMessaging-Example-iOS/Base.lproj/Main.storyboard create mode 100644 InAppMessaging/Example/App/InAppMessaging-Example-iOS/GoogleService-Info.plist create mode 100644 InAppMessaging/Example/App/InAppMessaging-Example-iOS/Info.plist create mode 100644 InAppMessaging/Example/App/InAppMessaging-Example-iOS/LogDumpViewController.h create mode 100644 InAppMessaging/Example/App/InAppMessaging-Example-iOS/LogDumpViewController.m create mode 100644 InAppMessaging/Example/App/InAppMessaging-Example-iOS/Podfile create mode 100644 InAppMessaging/Example/App/InAppMessaging-Example-iOS/main.m create mode 100644 InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/GoogleService-Info.plist create mode 100644 InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/Podfile create mode 100644 InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app.xcodeproj/project.pbxproj create mode 100644 InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/AppDelegate.h create mode 100644 InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/AppDelegate.m create mode 100644 InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/Base.lproj/LaunchScreen.storyboard create mode 100644 InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/Base.lproj/Main.storyboard create mode 100644 InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/Info.plist create mode 100644 InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/ViewController.h create mode 100644 InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/ViewController.m create mode 100644 InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/main.m create mode 100644 InAppMessaging/Example/FIRIAMSDKModeManagerTests.m create mode 100644 InAppMessaging/Example/InAppMessaging-Example-iOS.xcodeproj/project.pbxproj create mode 100644 InAppMessaging/Example/InAppMessaging-Example-iOS.xcodeproj/xcshareddata/xcschemes/InAppMessaging_Example_iOS.xcscheme create mode 100644 InAppMessaging/Example/InAppMessaging-Example-iOS.xcodeproj/xcshareddata/xcschemes/InAppMessaging_Tests_iOS.xcscheme create mode 100644 InAppMessaging/Example/InAppMessaging_Example_iOS.entitlements create mode 100644 InAppMessaging/Example/InAppMessaging_Example_iOS_SwiftUITests/InAppMessaging_Example_iOS_SwiftUITests.swift create mode 100644 InAppMessaging/Example/InAppMessaging_Example_iOS_SwiftUITests/Info.plist create mode 100644 InAppMessaging/Example/Podfile create mode 100644 InAppMessaging/Example/Scanfile create mode 100644 InAppMessaging/Example/Snapfile create mode 100644 InAppMessaging/Example/TestJsonDataFromFetch.txt create mode 100644 InAppMessaging/Example/TestJsonDataWithTestMessageFromFetch.txt create mode 100644 InAppMessaging/Example/Tests/FIRIAMActionUrlFollowerTests.m create mode 100644 InAppMessaging/Example/Tests/FIRIAMActivityLoggerTests.m create mode 100644 InAppMessaging/Example/Tests/FIRIAMAnalyticsEventLoggerImplTests.m create mode 100644 InAppMessaging/Example/Tests/FIRIAMBookKeeperViaUserDefaultsTests.m create mode 100644 InAppMessaging/Example/Tests/FIRIAMClearcutLoggerTests.m create mode 100644 InAppMessaging/Example/Tests/FIRIAMClearcutRetryLocalStorageTests.m create mode 100644 InAppMessaging/Example/Tests/FIRIAMClearcutUploaderTests.m create mode 100644 InAppMessaging/Example/Tests/FIRIAMDisplayExecutorTests.m create mode 100644 InAppMessaging/Example/Tests/FIRIAMElapsedTimeTrackerTests.m create mode 100644 InAppMessaging/Example/Tests/FIRIAMFetchFlowTests.m create mode 100644 InAppMessaging/Example/Tests/FIRIAMFetchResponseParserTests.m create mode 100644 InAppMessaging/Example/Tests/FIRIAMMessageClientCacheTests.m create mode 100644 InAppMessaging/Example/Tests/FIRIAMMessageContentDataWithImageURLTests.m create mode 100644 InAppMessaging/Example/Tests/FIRIAMMsgFetcherUsingRestfulTests.m create mode 100644 InAppMessaging/Example/Tests/Info.plist create mode 100644 InAppMessaging/Example/Tests/JsonDataWithInvalidMessagesFromFetch.txt create mode 100644 InAppMessaging/Example/Tests/NSString+InterlaceStringsTests.m create mode 100644 InAppMessaging/Example/Tests/UIColor+FIRIAMHexStringTests.m create mode 100644 Interop/Analytics/Public/FIRAnalyticsInteropListener.h diff --git a/Firebase/InAppMessaging/Analytics/FIRIAMAnalyticsEventLogger.h b/Firebase/InAppMessaging/Analytics/FIRIAMAnalyticsEventLogger.h new file mode 100644 index 00000000000..0599159c696 --- /dev/null +++ b/Firebase/InAppMessaging/Analytics/FIRIAMAnalyticsEventLogger.h @@ -0,0 +1,61 @@ +/* + * Copyright 2018 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 +#import "FIRIAMClientInfoFetcher.h" +#import "FIRIAMTimeFetcher.h" + +NS_ASSUME_NONNULL_BEGIN + +/// Values for different fiam activity types. +typedef NS_ENUM(NSInteger, FIRIAMAnalyticsLogEventType) { + + FIRIAMAnalyticsLogEventUnknown = -1, + + FIRIAMAnalyticsEventMessageImpression = 0, + FIRIAMAnalyticsEventActionURLFollow = 1, + FIRIAMAnalyticsEventMessageDismissAuto = 2, + FIRIAMAnalyticsEventMessageDismissClick = 3, + FIRIAMAnalyticsEventMessageDismissSwipe = 4, + + // category: errors happened + FIRIAMAnalyticsEventImageFetchError = 11, + FIRIAMAnalyticsEventImageFormatUnsupported = 12, + + FIRIAMAnalyticsEventFetchAPINetworkError = 13, + FIRIAMAnalyticsEventFetchAPIClientError = 14, // server returns 4xx status code + FIRIAMAnalyticsEventFetchAPIServerError = 15, // server returns 5xx status code + + // Events for test messages + FIRIAMAnalyticsEventTestMessageImpression = 16, + FIRIAMAnalyticsEventTestMessageClick = 17, +}; + +// a protocol for collecting Analytics log records. It's implementation will decide +// what to do with that analytics log record +@protocol FIRIAMAnalyticsEventLogger +/** + * Adds an analytics log record. + * @param eventTimeInMs the timestamp in ms for when the event happened. + * if it's nil, the implementation will use the current system for this info. + */ +- (void)logAnalyticsEventForType:(FIRIAMAnalyticsLogEventType)eventType + forCampaignID:(NSString *)campaignID + withCampaignName:(NSString *)campaignName + eventTimeInMs:(nullable NSNumber *)eventTimeInMs + completion:(void (^)(BOOL success))completion; +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Analytics/FIRIAMAnalyticsEventLoggerImpl.h b/Firebase/InAppMessaging/Analytics/FIRIAMAnalyticsEventLoggerImpl.h new file mode 100644 index 00000000000..41e289aaf75 --- /dev/null +++ b/Firebase/InAppMessaging/Analytics/FIRIAMAnalyticsEventLoggerImpl.h @@ -0,0 +1,45 @@ +/* + * Copyright 2018 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 + +#import "FIRIAMAnalyticsEventLogger.h" + +@class FIRIAMClearcutLogger; +@protocol FIRIAMTimeFetcher; +@protocol FIRAnalyticsInterop; + +NS_ASSUME_NONNULL_BEGIN +/** + * Implementation of protocol FIRIAMAnalyticsEventLogger by doing two things + * 1 Firing Firebase Analytics Events for impressions and clicks and dismisses + * 2 Making clearcut logging for all other types of analytics events + */ +@interface FIRIAMAnalyticsEventLoggerImpl : NSObject +- (instancetype)init NS_UNAVAILABLE; + +/** + * + * @param userDefaults needed for tracking upload timing info persistently.If nil, using + * NSUserDefaults standardUserDefaults. It's defined as a parameter to help with + * unit testing mocking + */ +- (instancetype)initWithClearcutLogger:(FIRIAMClearcutLogger *)ctLogger + usingTimeFetcher:(id)timeFetcher + usingUserDefaults:(nullable NSUserDefaults *)userDefaults + analytics:(nullable id)analytics; +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Analytics/FIRIAMAnalyticsEventLoggerImpl.m b/Firebase/InAppMessaging/Analytics/FIRIAMAnalyticsEventLoggerImpl.m new file mode 100644 index 00000000000..9e99a370308 --- /dev/null +++ b/Firebase/InAppMessaging/Analytics/FIRIAMAnalyticsEventLoggerImpl.m @@ -0,0 +1,170 @@ +/* + * Copyright 2018 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 "FIRIAMAnalyticsEventLoggerImpl.h" + +#import +#import +#import "FIRCore+InAppMessaging.h" +#import "FIRIAMClearcutLogger.h" + +typedef void (^FIRAUserPropertiesCallback)(NSDictionary *userProperties); + +@interface FIRIAMAnalyticsEventLoggerImpl () +@property(readonly, nonatomic) FIRIAMClearcutLogger *clearCutLogger; +@property(readonly, nonatomic) id timeFetcher; +@property(nonatomic, readonly) NSUserDefaults *userDefaults; +@end + +// in these kFAXX constants, FA represents FirebaseAnalytics +static NSString *const kFIREventOriginFIAM = @"fiam"; +; +static NSString *const kFAEventNameForImpression = @"firebase_in_app_message_impression"; +static NSString *const kFAEventNameForAction = @"firebase_in_app_message_action"; +static NSString *const kFAEventNameForDismiss = @"firebase_in_app_message_dismiss"; + +// In order to support tracking conversions from clicking a fiam event, we need to set +// an analytics user property with the fiam message's campaign id. +// This is the user property as kFIRUserPropertyLastNotification defined for FCM. +// Unlike FCM, FIAM would only allow the user property to exist up to certain expiration time +// after which, we stop attributing any further conversions to that fiam message click. +// So we include kFAUserPropertyPrefixForFIAM as the prefix for the entry written by fiam SDK +// to avoid removing entries written by FCM SDK +static NSString *const kFAUserPropertyForLastNotification = @"_ln"; +static NSString *const kFAUserPropertyPrefixForFIAM = @"fiam:"; + +// This user defaults key is for the entry to tell when we should remove the private user +// property from a prior action url click to stop conversion attribution for a campaign +static NSString *const kFIAMUserDefaualtsKeyForRemoveUserPropertyTimeInSeconds = + @"firebase-iam-conversion-tracking-expires-in-seconds"; + +@implementation FIRIAMAnalyticsEventLoggerImpl { + id _analytics; +} + +- (instancetype)initWithClearcutLogger:(FIRIAMClearcutLogger *)ctLogger + usingTimeFetcher:(id)timeFetcher + usingUserDefaults:(nullable NSUserDefaults *)userDefaults + analytics:(nullable id)analytics { + if (self = [super init]) { + _clearCutLogger = ctLogger; + _timeFetcher = timeFetcher; + _analytics = analytics; + _userDefaults = userDefaults ? userDefaults : [NSUserDefaults standardUserDefaults]; + + if (!_analytics) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM280002", + @"Firebase In App Messaging was not configured with FirebaseAnalytics."); + } + } + return self; +} + +- (NSDictionary *)constructFAEventParamsWithCampaignID:(NSString *)campaignID + campaignName:(NSString *)campaignName { + // event parameter names are aligned with definitions in event_names_util.cc + return @{ + @"_nmn" : campaignName ?: @"unknown", + @"_nmid" : campaignID ?: @"unknown", + @"_ndt" : @([self.timeFetcher currentTimestampInSeconds]) + }; +} + +- (void)logFAEventsForMessageImpressionWithcampaignID:(NSString *)campaignID + campaignName:(NSString *)campaignName { + if (_analytics) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM280001", + @"Log campaign impression Firebase Analytics event for campaign ID %@", campaignID); + + NSDictionary *params = [self constructFAEventParamsWithCampaignID:campaignID + campaignName:campaignName]; + [_analytics logEventWithOrigin:kFIREventOriginFIAM + name:kFAEventNameForImpression + parameters:params]; + } +} + +- (BOOL)setAnalyticsUserPropertyForKey:(NSString *)key withValue:(NSString *)value { + if (!_analytics || !key || !value) { + return NO; + } + [_analytics setUserPropertyWithOrigin:kFIREventOriginFIAM name:key value:value]; + return YES; +} + +- (void)logFAEventsForMessageActionWithCampaignID:(NSString *)campaignID + campaignName:(NSString *)campaignName { + if (_analytics) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM280004", + @"Log action click Firebase Analytics event for campaign ID %@", campaignID); + + NSDictionary *params = [self constructFAEventParamsWithCampaignID:campaignID + campaignName:campaignName]; + + [_analytics logEventWithOrigin:kFIREventOriginFIAM + name:kFAEventNameForAction + parameters:params]; + } + + // set a special user property so that conversion events can be queried based on that + // for reporting purpose + NSString *conversionTrackingUserPropertyValue = + [NSString stringWithFormat:@"%@%@", kFAUserPropertyPrefixForFIAM, campaignID]; + + if ([self setAnalyticsUserPropertyForKey:kFAUserPropertyForLastNotification + withValue:conversionTrackingUserPropertyValue]) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM280009", + @"User property for conversion tracking was set for campaign %@", campaignID); + } +} + +- (void)logFAEventsForMessageDismissWithcampaignID:(NSString *)campaignID + campaignName:(NSString *)campaignName { + if (_analytics) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM280007", + @"Log message dismiss Firebase Analytics event for campaign ID %@", campaignID); + + NSDictionary *params = [self constructFAEventParamsWithCampaignID:campaignID + campaignName:campaignName]; + [_analytics logEventWithOrigin:kFIREventOriginFIAM + name:kFAEventNameForDismiss + parameters:params]; + } +} + +- (void)logAnalyticsEventForType:(FIRIAMAnalyticsLogEventType)eventType + forCampaignID:(NSString *)campaignID + withCampaignName:(NSString *)campaignName + eventTimeInMs:(nullable NSNumber *)eventTimeInMs + completion:(void (^)(BOOL success))completion { + // log Firebase Analytics event first + if (eventType == FIRIAMAnalyticsEventMessageImpression) { + [self logFAEventsForMessageImpressionWithcampaignID:campaignID campaignName:campaignName]; + } else if (eventType == FIRIAMAnalyticsEventActionURLFollow) { + [self logFAEventsForMessageActionWithCampaignID:campaignID campaignName:campaignName]; + } else if (eventType == FIRIAMAnalyticsEventMessageDismissAuto || + eventType == FIRIAMAnalyticsEventMessageDismissClick) { + [self logFAEventsForMessageDismissWithcampaignID:campaignID campaignName:campaignName]; + } + + // and do clearcut logging as well + [self.clearCutLogger logAnalyticsEventForType:eventType + forCampaignID:campaignID + withCampaignName:campaignName + eventTimeInMs:eventTimeInMs + completion:completion]; +} +@end diff --git a/Firebase/InAppMessaging/Analytics/FIRIAMClearcutHttpRequestSender.h b/Firebase/InAppMessaging/Analytics/FIRIAMClearcutHttpRequestSender.h new file mode 100644 index 00000000000..7cacd37eaa3 --- /dev/null +++ b/Firebase/InAppMessaging/Analytics/FIRIAMClearcutHttpRequestSender.h @@ -0,0 +1,51 @@ +/* + * Copyright 2018 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 + +@class FIRIAMClearcutLogRecord; +@protocol FIRIAMTimeFetcher; + +NS_ASSUME_NONNULL_BEGIN +// class for sending requests to clearcut over its http API +@interface FIRIAMClearcutHttpRequestSender : NSObject + +/** + * Create an FIRIAMClearcutHttpRequestSender instance with specified clearcut server. + * + * @param serverHost API server host. + * @param osMajorVersion detected iOS major version of the current device + */ +- (instancetype)initWithClearcutHost:(NSString *)serverHost + usingTimeFetcher:(id)timeFetcher + withOSMajorVersion:(NSString *)osMajorVersion; + +/** + * Sends a batch of FIRIAMClearcutLogRecord records to clearcut server. + * @param logs an array of log records to be sent. + * @param completion is the handler to triggered upon completion. 'success' is a bool + * to indicate if the sending is successful. 'shouldRetryLogs' indicates if these + * logs need to be retried later on. On success case, waitTimeInMills is the value + * returned from clearcut server to indicate the minimal wait time before another + * send request can be attempted. + */ + +- (void)sendClearcutHttpRequestForLogs:(NSArray *)logs + withCompletion:(void (^)(BOOL success, + BOOL shouldRetryLogs, + int64_t waitTimeInMills))completion; +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Analytics/FIRIAMClearcutHttpRequestSender.m b/Firebase/InAppMessaging/Analytics/FIRIAMClearcutHttpRequestSender.m new file mode 100644 index 00000000000..ad292c721f0 --- /dev/null +++ b/Firebase/InAppMessaging/Analytics/FIRIAMClearcutHttpRequestSender.m @@ -0,0 +1,202 @@ +/* + * Copyright 2018 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 + +#import "FIRCore+InAppMessaging.h" +#import "FIRIAMClearcutHttpRequestSender.h" +#import "FIRIAMClearcutLogStorage.h" +#import "FIRIAMClientInfoFetcher.h" +#import "FIRIAMTimeFetcher.h" + +@interface FIRIAMClearcutHttpRequestSender () +@property(readonly, copy, nonatomic) NSString *serverHostName; + +@property(readwrite, nonatomic) id timeFetcher; +@property(readonly, copy, nonatomic) NSString *osMajorVersion; +@end + +@implementation FIRIAMClearcutHttpRequestSender + +- (instancetype)initWithClearcutHost:(NSString *)serverHost + usingTimeFetcher:(id)timeFetcher + withOSMajorVersion:(NSString *)osMajorVersion { + if (self = [super init]) { + _serverHostName = [serverHost copy]; + _timeFetcher = timeFetcher; + _osMajorVersion = [osMajorVersion copy]; + } + return self; +} + +- (void)updateRequestBodyWithClearcutEnvelopeFields:(NSMutableDictionary *)bodyDict { + bodyDict[@"client_info"] = @{ + @"client_type" : @15, // 15 is the enum value for IOS_FIREBASE client + @"ios_client_info" : @{@"os_major_version" : self.osMajorVersion ?: @""} + }; + bodyDict[@"log_source"] = @"FIREBASE_INAPPMESSAGING"; + + NSTimeInterval nowInMs = [self.timeFetcher currentTimestampInSeconds] * 1000; + bodyDict[@"request_time_ms"] = @((long)nowInMs); +} + +- (NSArray *)constructLogEventsArrayLogRecords: + (NSArray *)logRecords { + NSMutableArray *logEvents = [[NSMutableArray alloc] init]; + for (id next in logRecords) { + FIRIAMClearcutLogRecord *logRecord = (FIRIAMClearcutLogRecord *)next; + [logEvents addObject:@{ + @"event_time_ms" : @((long)logRecord.eventTimestampInSeconds * 1000), + @"source_extension_json" : logRecord.eventExtensionJsonString ?: @"" + }]; + } + + return [logEvents copy]; +} + +// @return nil if error happened in constructing the body +- (NSDictionary *)constructRequestBodyWithRetryRecords: + (NSArray *)logRecords { + NSMutableDictionary *body = [[NSMutableDictionary alloc] init]; + [self updateRequestBodyWithClearcutEnvelopeFields:body]; + body[@"log_event"] = [self constructLogEventsArrayLogRecords:logRecords]; + return [body copy]; +} + +// a helper method for dealing with the response received from +// executing NSURLSessionDataTask. Triggers the completion callback accordingly +- (void)handleClearcutAPICallResponseWithData:(NSData *)data + response:(NSURLResponse *)response + error:(NSError *)error + completion: + (nonnull void (^)(BOOL success, + BOOL shouldRetryLogs, + int64_t waitTimeInMills))completion { + if (error) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM250003", + @"Internal error: encountered error in uploading clearcut message" + ":%@", + error); + completion(NO, YES, 0); + return; + } + + if (![response isKindOfClass:[NSHTTPURLResponse class]]) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM250008", + @"Received non http response from sending " + "clearcut requests %@", + response); + completion(NO, YES, 0); + return; + } + + NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; + if (httpResponse.statusCode == 200) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM250004", + @"Sending clearcut logging request was successful"); + + NSError *errorJson = nil; + NSDictionary *responseDict = [NSJSONSerialization JSONObjectWithData:data + options:kNilOptions + error:&errorJson]; + + int64_t waitTimeFromClearcutServer = 0; + if (!errorJson && responseDict[@"next_request_wait_millis"]) { + waitTimeFromClearcutServer = [responseDict[@"next_request_wait_millis"] longLongValue]; + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM250007", + @"Wait time from clearcut server response is %d seconds", + (int)waitTimeFromClearcutServer / 1000); + } + completion(YES, NO, waitTimeFromClearcutServer); + } else if (httpResponse.statusCode == 400) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM250012", + @"Seeing 400 status code in response and we are discarding this log" + @"record"); + // 400 means bad request data and it won't be successful with retries. So + // we give up on these log records + completion(NO, NO, 0); + } else { + // May need to handle 401 errors if we do authentication in the future + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM250005", + @"Other http status code seen in clearcut request response %d", + (int)httpResponse.statusCode); + // can be retried + completion(NO, YES, 0); + } +} + +- (void)sendClearcutHttpRequestForLogs:(NSArray *)logs + withCompletion:(nonnull void (^)(BOOL success, + BOOL shouldRetryLogs, + int64_t waitTimeInMills))completion { + NSDictionary *requestBody = [self constructRequestBodyWithRetryRecords:logs]; + + if (!requestBody) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM250014", + @"Not able to construct request body for clearcut request, giving up"); + completion(NO, NO, 0); + } else { + // sending the log via a http request + NSURLSession *URLSession = [NSURLSession sharedSession]; + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init]; + [request setHTTPMethod:@"POST"]; + [request addValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; + [request addValue:@"application/json" forHTTPHeaderField:@"Accept"]; + + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM250001", + @"Request body dictionary is %@ for clearcut logging request", requestBody); + + NSError *error; + NSData *requestBodyData = [NSJSONSerialization dataWithJSONObject:requestBody + options:0 + error:&error]; + + if (error) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM250011", + @"Error in creating request body json for clearcut requests:%@", error); + completion(NO, NO, 0); + return; + } + + NSString *requestURLString = + [NSString stringWithFormat:@"https://%@/log?format=json_proto", self.serverHostName]; + [request setURL:[NSURL URLWithString:requestURLString]]; + [request setHTTPBody:requestBodyData]; + + NSURLSessionDataTask *clearCutLogDataTask = + [URLSession dataTaskWithRequest:request + completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + [self handleClearcutAPICallResponseWithData:data + response:response + error:error + completion:completion]; + }]; + + if (clearCutLogDataTask == nil) { + NSString *errorDesc = @"Internal error: NSURLSessionDataTask failed to be created due to " + "possibly incorrect parameters"; + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM250005", @"%@", errorDesc); + completion(NO, NO, 0); + } else { + [clearCutLogDataTask resume]; + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM250002", + @"Making a restful api for sending clearcut logging data with " + "a NSURLSessionDataTask request as %@", + clearCutLogDataTask.currentRequest); + } + } +} +@end diff --git a/Firebase/InAppMessaging/Analytics/FIRIAMClearcutLogStorage.h b/Firebase/InAppMessaging/Analytics/FIRIAMClearcutLogStorage.h new file mode 100644 index 00000000000..d24a3177934 --- /dev/null +++ b/Firebase/InAppMessaging/Analytics/FIRIAMClearcutLogStorage.h @@ -0,0 +1,48 @@ +/* + * Copyright 2018 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 + +NS_ASSUME_NONNULL_BEGIN +@interface FIRIAMClearcutLogRecord : NSObject +@property(nonatomic, copy, readonly) NSString *eventExtensionJsonString; +@property(nonatomic, readonly) NSInteger eventTimestampInSeconds; +- (instancetype)initWithExtensionJsonString:(NSString *)jsonString + eventTimestampInSeconds:(NSInteger)eventTimestampInSeconds; +@end + +@protocol FIRIAMTimeFetcher; + +// A local persistent storage for saving FIRIAMClearcutLogRecord objects +// so that they can be delivered to clearcut server. +// Based on the clearcut log structure, our strategy is to store the json string +// for the source extension since it does not need to be modified upon delivery retries. +// The envelope of the clearcut log will be reconstructed when delivery is +// attempted. + +@interface FIRIAMClearcutLogStorage : NSObject +- (instancetype)initWithExpireAfterInSeconds:(NSInteger)expireInSeconds + withTimeFetcher:(id)timeFetcher; + +// add new records into the storage +- (void)pushRecords:(NSArray *)newRecords; + +// pop all the records that have not expired yet. With this call, these +// records are removed from the book of this local storage object. +// @param upTo the cap on how many records to be popped. +- (NSArray *)popStillValidRecordsForUpTo:(NSInteger)upTo; +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Analytics/FIRIAMClearcutLogStorage.m b/Firebase/InAppMessaging/Analytics/FIRIAMClearcutLogStorage.m new file mode 100644 index 00000000000..e4ee2d272db --- /dev/null +++ b/Firebase/InAppMessaging/Analytics/FIRIAMClearcutLogStorage.m @@ -0,0 +1,171 @@ +/* + * Copyright 2018 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 + +#import + +#import "FIRCore+InAppMessaging.h" +#import "FIRIAMClearcutLogStorage.h" +#import "FIRIAMTimeFetcher.h" + +@implementation FIRIAMClearcutLogRecord +static NSString *const kEventTimestampKey = @"event_ts_seconds"; +static NSString *const kEventExtensionJson = @"extension_js"; + ++ (BOOL)supportsSecureCoding { + return YES; +} + +- (instancetype)initWithExtensionJsonString:(NSString *)jsonString + eventTimestampInSeconds:(NSInteger)eventTimestampInSeconds { + self = [super init]; + if (self != nil) { + _eventTimestampInSeconds = eventTimestampInSeconds; + _eventExtensionJsonString = jsonString; + } + return self; +} + +- (id)initWithCoder:(NSCoder *)decoder { + self = [super init]; + if (self != nil) { + _eventTimestampInSeconds = [decoder decodeIntegerForKey:kEventTimestampKey]; + _eventExtensionJsonString = [decoder decodeObjectOfClass:[NSString class] + forKey:kEventExtensionJson]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)encoder { + [encoder encodeInteger:self.eventTimestampInSeconds forKey:kEventTimestampKey]; + [encoder encodeObject:self.eventExtensionJsonString forKey:kEventExtensionJson]; +} +@end + +@interface FIRIAMClearcutLogStorage () +@property(nonatomic) NSInteger recordExpiresInSeconds; +@property(nonatomic) NSMutableArray *records; +@property(nonatomic) id timeFetcher; +@end + +// We keep all the records in memory and flush them into files upon receiving +// applicationDidEnterBackground notifications. +@implementation FIRIAMClearcutLogStorage + ++ (NSString *)determineCacheFilePath { + static NSString *logCachePath; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + NSString *libraryDirPath = + NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES)[0]; + logCachePath = + [NSString stringWithFormat:@"%@/firebase-iam-clearcut-retry-records", libraryDirPath]; + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM230001", + @"Persistent file path for clearcut log records is %@", logCachePath); + }); + return logCachePath; +} + +- (instancetype)initWithExpireAfterInSeconds:(NSInteger)expireInSeconds + withTimeFetcher:(id)timeFetcher { + if (self = [super init]) { + _records = [[NSMutableArray alloc] init]; + _timeFetcher = timeFetcher; + _recordExpiresInSeconds = expireInSeconds; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(appWillBecomeInactive) + name:UIApplicationWillResignActiveNotification + object:nil]; + @try { + [self loadFromCachePath:nil]; + } @catch (NSException *exception) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM230004", + @"Non-fatal exception in loading persisted clearcut log records: %@.", + exception); + } + } + return self; +} + +- (void)appWillBecomeInactive { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul), ^{ + [self saveIntoCacheWithPath:nil]; + }); +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (void)pushRecords:(NSArray *)newRecords { + @synchronized(self) { + [self.records addObjectsFromArray:newRecords]; + } +} + +- (NSArray *)popStillValidRecordsForUpTo:(NSInteger)upTo { + NSMutableArray *resultArray = [[NSMutableArray alloc] init]; + NSInteger nowInSeconds = (NSInteger)[self.timeFetcher currentTimestampInSeconds]; + + NSInteger next = 0; + + @synchronized(self) { + while (resultArray.count < upTo && next < self.records.count) { + FIRIAMClearcutLogRecord *nextRecord = self.records[next++]; + if (nextRecord.eventTimestampInSeconds > nowInSeconds - self.recordExpiresInSeconds) { + // record not expired yet + [resultArray addObject:nextRecord]; + } + } + + [self.records removeObjectsInRange:NSMakeRange(0, next)]; + } + + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM230005", + @"Returning %d clearcut retry records from popStillValidRecords", + (int)resultArray.count); + return resultArray; +} + +- (void)loadFromCachePath:(NSString *)cacheFilePath { + NSString *filePath = cacheFilePath == nil ? [self.class determineCacheFilePath] : cacheFilePath; + + NSTimeInterval start = [self.timeFetcher currentTimestampInSeconds]; + id fetchedClearcutRetryRecords = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath]; + if (fetchedClearcutRetryRecords) { + @synchronized(self) { + self.records = (NSMutableArray *)fetchedClearcutRetryRecords; + } + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM230002", + @"Loaded %d clearcut log records from file in %lf seconds", (int)self.records.count, + (double)[self.timeFetcher currentTimestampInSeconds] - start); + } +} + +- (BOOL)saveIntoCacheWithPath:(NSString *)cacheFilePath { + NSString *filePath = cacheFilePath == nil ? [self.class determineCacheFilePath] : cacheFilePath; + @synchronized(self) { + BOOL saveResult = [NSKeyedArchiver archiveRootObject:self.records toFile:filePath]; + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM230003", + @"Saving %d clearcut log records into file is %@", (int)self.records.count, + saveResult ? @"successful" : @"failure"); + + return saveResult; + } +} +@end diff --git a/Firebase/InAppMessaging/Analytics/FIRIAMClearcutLogger.h b/Firebase/InAppMessaging/Analytics/FIRIAMClearcutLogger.h new file mode 100644 index 00000000000..d894a00f14f --- /dev/null +++ b/Firebase/InAppMessaging/Analytics/FIRIAMClearcutLogger.h @@ -0,0 +1,46 @@ +/* + * Copyright 2018 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 +#import "FIRIAMAnalyticsEventLogger.h" +#import "FIRIAMClientInfoFetcher.h" +#import "FIRIAMTimeFetcher.h" + +@class FIRIAMClearcutUploader; + +NS_ASSUME_NONNULL_BEGIN +// FIRIAMAnalyticsEventLogger implementation using Clearcut. It turns a IAM analytics event +// into the corresponding FIRIAMClearcutLogRecord and then hand it over to +// a FIRIAMClearcutUploader instance for the actual sending and potential failure and retry +// logic +@interface FIRIAMClearcutLogger : NSObject + +- (instancetype)init NS_UNAVAILABLE; + +/** + * Create an instance which uses NSURLSession to make clearcut api calls. + * + * @param clientInfoFetcher used to fetch iid info for the current app. + * @param timeFetcher time fetcher object + * @param uploader FIRIAMClearcutUploader object for receiving the log record + */ +- (instancetype)initWithFBProjectNumber:(NSString *)fbProjectNumber + fbAppId:(NSString *)fbAppId + clientInfoFetcher:(FIRIAMClientInfoFetcher *)clientInfoFetcher + usingTimeFetcher:(id)timeFetcher + usingUploader:(FIRIAMClearcutUploader *)uploader; +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Analytics/FIRIAMClearcutLogger.m b/Firebase/InAppMessaging/Analytics/FIRIAMClearcutLogger.m new file mode 100644 index 00000000000..4cb8a134b89 --- /dev/null +++ b/Firebase/InAppMessaging/Analytics/FIRIAMClearcutLogger.m @@ -0,0 +1,203 @@ +/* + * Copyright 2018 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 +#import + +#import "FIRCore+InAppMessaging.h" +#import "FIRIAMClearcutLogStorage.h" +#import "FIRIAMClearcutLogger.h" +#import "FIRIAMClearcutUploader.h" + +@interface FIRIAMClearcutLogger () + +// these two writable for assisting unit testing need +@property(readwrite, nonatomic) FIRIAMClearcutHttpRequestSender *requestSender; +@property(readwrite, nonatomic) id timeFetcher; + +@property(readonly, nonatomic) FIRIAMClientInfoFetcher *clientInfoFetcher; +@property(readonly, nonatomic) FIRIAMClearcutUploader *ctUploader; + +@property(readonly, copy, nonatomic) NSString *fbProjectNumber; +@property(readonly, copy, nonatomic) NSString *fbAppId; + +@end + +@implementation FIRIAMClearcutLogger { + NSString *_iid; +} +- (instancetype)initWithFBProjectNumber:(NSString *)fbProjectNumber + fbAppId:(NSString *)fbAppId + clientInfoFetcher:(FIRIAMClientInfoFetcher *)clientInfoFetcher + usingTimeFetcher:(id)timeFetcher + usingUploader:(FIRIAMClearcutUploader *)uploader { + if (self = [super init]) { + _fbProjectNumber = fbProjectNumber; + _fbAppId = fbAppId; + _clientInfoFetcher = clientInfoFetcher; + _timeFetcher = timeFetcher; + _ctUploader = uploader; + } + return self; +} + ++ (void)updateSourceExtensionDictWithAnalyticsEventEnumType:(FIRIAMAnalyticsLogEventType)eventType + forDict:(NSMutableDictionary *)dict { + switch (eventType) { + case FIRIAMAnalyticsEventMessageImpression: + dict[@"event_type"] = @"IMPRESSION_EVENT_TYPE"; + break; + case FIRIAMAnalyticsEventActionURLFollow: + dict[@"event_type"] = @"CLICK_EVENT_TYPE"; + break; + case FIRIAMAnalyticsEventMessageDismissAuto: + dict[@"dismiss_type"] = @"AUTO"; + break; + case FIRIAMAnalyticsEventMessageDismissClick: + dict[@"dismiss_type"] = @"CLICK"; + break; + case FIRIAMAnalyticsEventMessageDismissSwipe: + dict[@"dismiss_type"] = @"SWIPE"; + break; + case FIRIAMAnalyticsEventImageFetchError: + dict[@"render_error_reason"] = @"IMAGE_FETCH_ERROR"; + break; + case FIRIAMAnalyticsEventImageFormatUnsupported: + dict[@"render_error_reason"] = @"IMAGE_UNSUPPORTED_FORMAT"; + break; + case FIRIAMAnalyticsEventFetchAPIClientError: + dict[@"fetch_error_reason"] = @"CLIENT_ERROR"; + break; + case FIRIAMAnalyticsEventFetchAPIServerError: + dict[@"fetch_error_reason"] = @"SERVER_ERROR"; + break; + case FIRIAMAnalyticsEventFetchAPINetworkError: + dict[@"fetch_error_reason"] = @"NETWORK_ERROR"; + break; + case FIRIAMAnalyticsEventTestMessageImpression: + dict[@"event_type"] = @"TEST_MESSAGE_IMPRESSION_EVENT_TYPE"; + break; + case FIRIAMAnalyticsEventTestMessageClick: + dict[@"event_type"] = @"TEST_MESSAGE_CLICK_EVENT_TYPE"; + break; + case FIRIAMAnalyticsLogEventUnknown: + break; + } +} + +// constructing CampaignAnalytics proto defined in campaign_analytics.proto and serialize it into +// a string. +// @return nil if error happened +- (NSString *)constructSourceExtensionJsonForClearcutWithEventType: + (FIRIAMAnalyticsLogEventType)eventType + forCampaignID:(NSString *)campaignID + eventTimeInMs:(NSNumber *)eventTimeInMs + instanceID:(NSString *)instanceID { + NSMutableDictionary *campaignAnalyticsDict = [[NSMutableDictionary alloc] init]; + + campaignAnalyticsDict[@"project_number"] = self.fbProjectNumber; + campaignAnalyticsDict[@"campaign_id"] = campaignID; + campaignAnalyticsDict[@"client_app"] = + @{@"google_app_id" : self.fbAppId, @"firebase_instance_id" : instanceID}; + campaignAnalyticsDict[@"client_timestamp_millis"] = eventTimeInMs; + [self.class updateSourceExtensionDictWithAnalyticsEventEnumType:eventType + forDict:campaignAnalyticsDict]; + + campaignAnalyticsDict[@"fiam_sdk_version"] = [self.clientInfoFetcher getIAMSDKVersion]; + + // turn campaignAnalyticsDict into a json string + NSError *error; + NSData *jsonData = [NSJSONSerialization + dataWithJSONObject:campaignAnalyticsDict // Here you can pass array or dictionary + options:0 // Pass 0 if you don't care about the readability of the generated + // string + error:&error]; + + if (jsonData) { + NSString *jsonString; + jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM210006", + @"Source extension json string produced as %@", jsonString); + return jsonString; + } else { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM210007", + @"Error in generating source extension json string: %@", error); + return nil; + } +} + +- (void)logAnalyticsEventForType:(FIRIAMAnalyticsLogEventType)eventType + forCampaignID:(NSString *)campaignID + withEventTimeInMs:(nullable NSNumber *)eventTimeInMs + IID:(NSString *)iid + completion:(void (^)(BOOL success))completion { + NSTimeInterval nowInMs = [self.timeFetcher currentTimestampInSeconds] * 1000; + if (!eventTimeInMs) { + eventTimeInMs = @((long)nowInMs); + } + + NSString *sourceExtensionJsonString = + [self constructSourceExtensionJsonForClearcutWithEventType:eventType + forCampaignID:campaignID + eventTimeInMs:eventTimeInMs + instanceID:iid]; + + FIRIAMClearcutLogRecord *newRecord = [[FIRIAMClearcutLogRecord alloc] + initWithExtensionJsonString:sourceExtensionJsonString + eventTimestampInSeconds:eventTimeInMs.integerValue / 1000]; + [self.ctUploader addNewLogRecord:newRecord]; + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM210003", + @"One more clearcut log record created and sent to uploader with source extension %@", + sourceExtensionJsonString); + completion(YES); +} + +- (void)logAnalyticsEventForType:(FIRIAMAnalyticsLogEventType)eventType + forCampaignID:(NSString *)campaignID + withCampaignName:(NSString *)campaignName + eventTimeInMs:(nullable NSNumber *)eventTimeInMs + completion:(void (^)(BOOL success))completion { + if (!_iid) { + [self.clientInfoFetcher + fetchFirebaseIIDDataWithProjectNumber:self.fbProjectNumber + withCompletion:^(NSString *_Nullable iid, NSString *_Nullable token, + NSError *_Nullable error) { + if (error) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM210001", + @"Failed to get iid value for clearcut logging %@", + error); + completion(NO); + } else { + // persist iid through the whole life-cycle + self->_iid = iid; + [self logAnalyticsEventForType:eventType + forCampaignID:campaignID + withEventTimeInMs:eventTimeInMs + IID:iid + completion:completion]; + } + }]; + } else { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM210004", + @"Using remembered iid for event logging"); + [self logAnalyticsEventForType:eventType + forCampaignID:campaignID + withEventTimeInMs:eventTimeInMs + IID:_iid + completion:completion]; + } +} +@end diff --git a/Firebase/InAppMessaging/Analytics/FIRIAMClearcutUploader.h b/Firebase/InAppMessaging/Analytics/FIRIAMClearcutUploader.h new file mode 100644 index 00000000000..9c0d1139b36 --- /dev/null +++ b/Firebase/InAppMessaging/Analytics/FIRIAMClearcutUploader.h @@ -0,0 +1,75 @@ +/* + * Copyright 2018 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 + +@class FIRIAMClearcutLogRecord; +@class FIRIAMClearcutHttpRequestSender; +@class FIRIAMClearcutLogStorage; + +@protocol FIRIAMTimeFetcher; + +NS_ASSUME_NONNULL_BEGIN + +// class for defining a number of configs to control clearcut upload behavior +@interface FIRIAMClearcutStrategy : NSObject + +// minimalWaitTimeInMills and maximumWaitTimeInMills defines the bottom and +// upper bound of the wait time before next upload if prior upload attempt was +// successful. Clearcut may return a value to give the wait time guidance in +// the upload response, but we also use these two values for sanity check to avoid +// too crazy behavior if the guidance value from server does not make sense +@property(nonatomic, readonly) NSInteger minimalWaitTimeInMills; +@property(nonatomic, readonly) NSInteger maximumWaitTimeInMills; + +// back off wait time in mills if a prior upload attempt fails +@property(nonatomic, readonly) NSInteger failureBackoffTimeInMills; + +// the maximum number of log records to be sent in one upload attempt +@property(nonatomic, readonly) NSInteger batchSendSize; + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithMinWaitTimeInMills:(NSInteger)minWaitTimeInMills + maxWaitTimeInMills:(NSInteger)maxWaitTimeInMills + failureBackoffTimeInMills:(NSInteger)failureBackoffTimeInMills + batchSendSize:(NSInteger)batchSendSize; + +- (NSString *)description; +@end + +// A class for accepting new clearcut logs and scheduling the uploading of the logs in batches +// based on defined strategies. +@interface FIRIAMClearcutUploader : NSObject +- (instancetype)init NS_UNAVAILABLE; + +/** + * + * @param userDefaults needed for tracking upload timing info persistently.If nil, using + * NSUserDefaults standardUserDefaults. It's defined as a parameter to help with + * unit testing mocking + */ +- (instancetype)initWithRequestSender:(FIRIAMClearcutHttpRequestSender *)requestSender + timeFetcher:(id)timeFetcher + logStorage:(FIRIAMClearcutLogStorage *)retryStorage + usingStrategy:(FIRIAMClearcutStrategy *)strategy + usingUserDefaults:(nullable NSUserDefaults *)userDefaults; +/** + * This should return very quickly without blocking on and actual log uploading to + * clearcut server, which is done asynchronously + */ +- (void)addNewLogRecord:(FIRIAMClearcutLogRecord *)record; +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Analytics/FIRIAMClearcutUploader.m b/Firebase/InAppMessaging/Analytics/FIRIAMClearcutUploader.m new file mode 100644 index 00000000000..1e676536f1c --- /dev/null +++ b/Firebase/InAppMessaging/Analytics/FIRIAMClearcutUploader.m @@ -0,0 +1,233 @@ +/* + * Copyright 2018 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 +#import + +#import "FIRCore+InAppMessaging.h" +#import "FIRIAMClearcutUploader.h" +#import "FIRIAMTimeFetcher.h" + +#import "FIRIAMClearcutHttpRequestSender.h" +#import "FIRIAMClearcutLogStorage.h" + +// a macro for turning a millisecond value into seconds +#define MILLS_TO_SECONDS(x) (((long)x) / 1000) + +@implementation FIRIAMClearcutStrategy +- (instancetype)initWithMinWaitTimeInMills:(NSInteger)minWaitTimeInMills + maxWaitTimeInMills:(NSInteger)maxWaitTimeInMills + failureBackoffTimeInMills:(NSInteger)failureBackoffTimeInMills + batchSendSize:(NSInteger)batchSendSize { + if (self = [super init]) { + _minimalWaitTimeInMills = minWaitTimeInMills; + _maximumWaitTimeInMills = maxWaitTimeInMills; + _failureBackoffTimeInMills = failureBackoffTimeInMills; + _batchSendSize = batchSendSize; + } + return self; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"min wait time in seconds:%ld;max wait time in seconds:%ld;" + "failure backoff time in seconds:%ld;batch send size:%d", + MILLS_TO_SECONDS(self.minimalWaitTimeInMills), + MILLS_TO_SECONDS(self.maximumWaitTimeInMills), + MILLS_TO_SECONDS(self.failureBackoffTimeInMills), + (int)self.batchSendSize]; +} +@end + +@interface FIRIAMClearcutUploader () { + dispatch_queue_t _queue; + BOOL _nextSendScheduled; +} + +@property(readwrite, nonatomic) FIRIAMClearcutHttpRequestSender *requestSender; +@property(nonatomic, assign) int64_t nextValidSendTimeInMills; + +@property(nonatomic, readonly) id timeFetcher; +@property(nonatomic, readonly) FIRIAMClearcutLogStorage *logStorage; + +@property(nonatomic, readonly) FIRIAMClearcutStrategy *strategy; +@property(nonatomic, readonly) NSUserDefaults *userDefaults; +@end + +static NSString *FIRIAM_UserDefaultsKeyForNextValidClearcutUploadTimeInMills = + @"firebase-iam-next-clearcut-upload-timestamp-in-mills"; + +/** + * The high level behavior in this implementation is like this + * 1 New records always pushed into FIRIAMClearcutLogStorage first. + * 2 Upload log records in batches. + * 3 If prior upload was successful, next upload would wait for the time parsed out of the + * clearcut response body. + * 4 If prior upload failed, next upload attempt would wait for failureBackoffTimeInMills defined + * in strategy + * 5 When app + */ + +@implementation FIRIAMClearcutUploader + +- (instancetype)initWithRequestSender:(FIRIAMClearcutHttpRequestSender *)requestSender + timeFetcher:(id)timeFetcher + logStorage:(FIRIAMClearcutLogStorage *)logStorage + usingStrategy:(FIRIAMClearcutStrategy *)strategy + usingUserDefaults:(nullable NSUserDefaults *)userDefaults { + if (self = [super init]) { + _nextSendScheduled = NO; + _timeFetcher = timeFetcher; + _requestSender = requestSender; + _logStorage = logStorage; + _strategy = strategy; + _queue = dispatch_queue_create("com.google.firebase.inappmessaging.clearcut_upload", NULL); + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(appWillEnterForeground:) + name:UIApplicationWillEnterForegroundNotification + object:nil]; + + _userDefaults = userDefaults ? userDefaults : [NSUserDefaults standardUserDefaults]; + // it would be 0 if it does not exist, which is equvilent to saying that + // you can send now + _nextValidSendTimeInMills = (int64_t) + [_userDefaults doubleForKey:FIRIAM_UserDefaultsKeyForNextValidClearcutUploadTimeInMills]; + + // seed the first send upon SDK start-up + [self scheduleNextSend]; + + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260001", + @"FIRIAMClearcutUploader created with strategy as %@", self.strategy); + } + return self; +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (void)appWillEnterForeground:(UIApplication *)application { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260010", + @"App foregrounded, FIRIAMClearcutUploader will seed next send"); + [self scheduleNextSend]; +} + +- (void)addNewLogRecord:(FIRIAMClearcutLogRecord *)record { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260002", + @"New log record sent to clearcut uploader"); + + [self.logStorage pushRecords:@[ record ]]; + [self scheduleNextSend]; +} + +- (void)attemptUploading { + NSArray *availbleLogs = + [self.logStorage popStillValidRecordsForUpTo:self.strategy.batchSendSize]; + + if (availbleLogs.count > 0) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260011", @"Deliver %d clearcut records", + (int)availbleLogs.count); + [self.requestSender + sendClearcutHttpRequestForLogs:availbleLogs + withCompletion:^(BOOL success, BOOL shouldRetryLogs, + int64_t waitTimeInMills) { + if (success) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260003", + @"Delivering %d clearcut records was successful", + (int)availbleLogs.count); + // make sure the effective wait time is between two bounds + // defined in strategy + waitTimeInMills = + MAX(self.strategy.minimalWaitTimeInMills, waitTimeInMills); + + waitTimeInMills = + MIN(waitTimeInMills, self.strategy.maximumWaitTimeInMills); + } else { + // failed to deliver + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260004", + @"Failed to attempt the delivery of %d clearcut " + @"records and should-retry for them is %@", + (int)availbleLogs.count, shouldRetryLogs ? @"YES" : @"NO"); + if (shouldRetryLogs) { + /** + * Note that there is a chance that the app crashes before we can + * call pushRecords: on the logStorage below which means we lost + * these log records permanently. This is a trade-off between handling + * duplicate records on server side vs taking the risk of lossing + * data. This implementation picks the latter. + */ + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260007", + @"Push failed log records back to storage"); + [self.logStorage pushRecords:availbleLogs]; + } + + waitTimeInMills = (int64_t)self.strategy.failureBackoffTimeInMills; + } + + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260005", + @"Wait for at least %ld seconds before next upload attempt", + MILLS_TO_SECONDS(waitTimeInMills)); + + self.nextValidSendTimeInMills = + (int64_t)[self.timeFetcher currentTimestampInSeconds] * 1000 + + waitTimeInMills; + + // persisted so that it can be recovered next time the app runs + [self.userDefaults + setDouble:(double)self.nextValidSendTimeInMills + forKey: + FIRIAM_UserDefaultsKeyForNextValidClearcutUploadTimeInMills]; + + @synchronized(self) { + self->_nextSendScheduled = NO; + } + [self scheduleNextSend]; + }]; + + } else { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260007", @"No clearcut records to be uploaded"); + @synchronized(self) { + _nextSendScheduled = NO; + } + } +} + +- (void)scheduleNextSend { + @synchronized(self) { + if (_nextSendScheduled) { + // already scheduled next send, don't do it again + return; + } else { + _nextSendScheduled = YES; + } + } + + int64_t delayTimeInMills = + self.nextValidSendTimeInMills - (int64_t)[self.timeFetcher currentTimestampInSeconds] * 1000; + + if (delayTimeInMills <= 0) { + delayTimeInMills = 0; // no need to delay since we can send now + } + + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260006", + @"Next upload attempt scheduled in %d seconds", (int)delayTimeInMills / 1000); + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delayTimeInMills * (int64_t)NSEC_PER_MSEC), + _queue, ^{ + [self attemptUploading]; + }); +} + +@end diff --git a/Firebase/InAppMessaging/CHANGELOG.md b/Firebase/InAppMessaging/CHANGELOG.md new file mode 100644 index 00000000000..a9c4ea6812e --- /dev/null +++ b/Firebase/InAppMessaging/CHANGELOG.md @@ -0,0 +1,6 @@ +# 2018-09-25 -- v0.12.0 +- Separated UI functionality into a new open source SDK called FirebaseInAppMessagingDisplay. +- Respect fetch between wait time returned from API responses. + +# 2018-08-15 -- v0.11.0 +- First Beta Release. diff --git a/Firebase/InAppMessaging/Data/FIRIAMFetchResponseParser.h b/Firebase/InAppMessaging/Data/FIRIAMFetchResponseParser.h new file mode 100644 index 00000000000..3146a0570e1 --- /dev/null +++ b/Firebase/InAppMessaging/Data/FIRIAMFetchResponseParser.h @@ -0,0 +1,40 @@ +/* + * 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 + +@class FIRIAMMessageDefinition; +@protocol FIRIAMTimeFetcher; + +NS_ASSUME_NONNULL_BEGIN + +// Class responsible for parsing the json response data from the restful API endpoint +// for serving eligible messages for the current SDK clients. +@interface FIRIAMFetchResponseParser : NSObject + +// Turn the API response into a number of FIRIAMMessageDefinition objects. If any of them is invalid +// it would be ignored and not represented in the response array. +// @param discardCount if not nil, it would contain, on return, the number of invalid messages +// detected uring parsing. +// @param fetchWaitTime would be non nil if fetch wait time data is found in the api response. +- (NSArray *)parseAPIResponseDictionary:(NSDictionary *)responseDict + discardedMsgCount:(NSInteger *)discardCount + fetchWaitTimeInSeconds:(NSNumber **)fetchWaitTime; + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithTimeFetcher:(id)timeFetcher; +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Data/FIRIAMFetchResponseParser.m b/Firebase/InAppMessaging/Data/FIRIAMFetchResponseParser.m new file mode 100644 index 00000000000..771c4eb51c2 --- /dev/null +++ b/Firebase/InAppMessaging/Data/FIRIAMFetchResponseParser.m @@ -0,0 +1,313 @@ +/* + * 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 + +#import "FIRCore+InAppMessaging.h" +#import "FIRIAMDisplayTriggerDefinition.h" +#import "FIRIAMFetchResponseParser.h" +#import "FIRIAMMessageContentData.h" +#import "FIRIAMMessageContentDataWithImageURL.h" +#import "FIRIAMMessageDefinition.h" +#import "FIRIAMTimeFetcher.h" +#import "UIColor+FIRIAMHexString.h" + +@interface FIRIAMFetchResponseParser () +@property(nonatomic) id timeFetcher; +@end + +@implementation FIRIAMFetchResponseParser + +- (instancetype)initWithTimeFetcher:(id)timeFetcher { + if (self = [super init]) { + _timeFetcher = timeFetcher; + } + return self; +} + +- (NSArray *)parseAPIResponseDictionary:(NSDictionary *)responseDict + discardedMsgCount:(NSInteger *)discardCount + fetchWaitTimeInSeconds:(NSNumber **)fetchWaitTime { + if (fetchWaitTime != nil) { + *fetchWaitTime = nil; // It would be set to non nil value if it's detected in responseDict + if ([responseDict[@"expirationEpochTimestampMillis"] isKindOfClass:NSString.class]) { + NSTimeInterval nextFetchTimeInResponse = + [responseDict[@"expirationEpochTimestampMillis"] doubleValue] / 1000; + NSTimeInterval fetchWaitTimeInSeconds = + nextFetchTimeInResponse - [self.timeFetcher currentTimestampInSeconds]; + + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM900005", + @"Detected next fetch epoch time in API response as %f seconds and wait for %f " + "seconds before next fetch.", + nextFetchTimeInResponse, fetchWaitTimeInSeconds); + + if (fetchWaitTimeInSeconds > 0.01) { + *fetchWaitTime = @(fetchWaitTimeInSeconds); + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM900018", + @"Fetch wait time calculated from server response is negative. Discard it."); + } + } else { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM900014", + @"No fetch epoch time detected in API response."); + } + } + + NSArray *messageArray = responseDict[@"messages"]; + NSInteger discarded = 0; + + NSMutableArray *definitions = [[NSMutableArray alloc] init]; + for (NSDictionary *nextMsg in messageArray) { + FIRIAMMessageDefinition *nextDefinition = + [self convertToMessageDefinitionWithMessageDict:nextMsg]; + if (nextDefinition) { + [definitions addObject:nextDefinition]; + } else { + FIRLogInfo(kFIRLoggerInAppMessaging, @"I-IAM900001", + @"No definition generated for message node %@", nextMsg); + discarded++; + } + } + FIRLogDebug( + kFIRLoggerInAppMessaging, @"I-IAM900002", + @"%lu message definitions were parsed out successfully and %lu messages are discarded", + (unsigned long)definitions.count, (unsigned long)discarded); + + if (discardCount) { + *discardCount = discarded; + } + return [definitions copy]; +} + +// Return nil if no valid triggering condition can be detected +- (NSArray *)parseTriggeringCondition: + (NSArray *)triggerConditions { + if (triggerConditions == nil || triggerConditions.count == 0) { + return nil; + } + + NSMutableArray *triggers = [[NSMutableArray alloc] init]; + + for (NSDictionary *nextTriggerCondition in triggerConditions) { + if (nextTriggerCondition[@"fiamTrigger"]) { + if ([nextTriggerCondition[@"fiamTrigger"] isEqualToString:@"ON_FOREGROUND"]) { + [triggers addObject:[[FIRIAMDisplayTriggerDefinition alloc] initForAppForegroundTrigger]]; + } + } else if ([nextTriggerCondition[@"event"] isKindOfClass:[NSDictionary class]]) { + NSDictionary *triggeringEvent = (NSDictionary *)nextTriggerCondition[@"event"]; + if (triggeringEvent[@"name"]) { + [triggers addObject:[[FIRIAMDisplayTriggerDefinition alloc] + initWithFirebaseAnalyticEvent:triggeringEvent[@"name"]]]; + } + } + } + + return [triggers copy]; +} + +// For one element in the restful API response's messages array, convert into +// a FIRIAMMessageDefinition object. If the conversion fails, a nil is returned. +- (FIRIAMMessageDefinition *)convertToMessageDefinitionWithMessageDict:(NSDictionary *)messageNode { + @try { + BOOL isTestMessage = NO; + + id isTestCampaignNode = messageNode[@"isTestCampaign"]; + if ([isTestCampaignNode isKindOfClass:[NSNumber class]]) { + isTestMessage = [isTestCampaignNode boolValue]; + } + + id vanillaPayloadNode = messageNode[@"vanillaPayload"]; + if (![vanillaPayloadNode isKindOfClass:[NSDictionary class]]) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900012", + @"vanillaPayload does not exist or does not represent a dictionary in " + "message node %@", + messageNode); + return nil; + } + + NSString *messageID = vanillaPayloadNode[@"campaignId"]; + if (!messageID) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900010", + @"messsage id is missing in message node %@", messageNode); + return nil; + } + + NSString *messageName = vanillaPayloadNode[@"campaignName"]; + if (!messageName && !isTestMessage) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900011", + @"campaign name is missing in non-test message node %@", messageNode); + return nil; + } + + NSTimeInterval startTimeInSeconds = 0; + NSTimeInterval endTimeInSeconds = 0; + if (!isTestMessage) { + // Parsing start/end times out of non-test messages. They are strings in the + // json response. + id startTimeNode = vanillaPayloadNode[@"campaignStartTimeMillis"]; + if ([startTimeNode isKindOfClass:[NSString class]]) { + startTimeInSeconds = [startTimeNode doubleValue] / 1000.0; + } + + id endTimeNode = vanillaPayloadNode[@"campaignEndTimeMillis"]; + if ([endTimeNode isKindOfClass:[NSString class]]) { + endTimeInSeconds = [endTimeNode doubleValue] / 1000.0; + } + } + + id contentNode = messageNode[@"content"]; + if (![contentNode isKindOfClass:[NSDictionary class]]) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900013", + @"content node does not exist or does not represent a dictionary in " + "message node %@", + messageNode); + return nil; + } + + NSDictionary *content = (NSDictionary *)contentNode; + FIRIAMRenderingMode mode; + UIColor *viewCardBackgroundColor, *btnBgColor, *btnTxtColor, *titleTextColor; + viewCardBackgroundColor = btnBgColor = btnTxtColor = titleTextColor = nil; + + NSString *title, *body, *imageURLStr, *actionURLStr, *actionButtonText; + title = body = imageURLStr = actionButtonText = actionURLStr = nil; + + if ([content[@"banner"] isKindOfClass:[NSDictionary class]]) { + NSDictionary *bannerNode = (NSDictionary *)contentNode[@"banner"]; + mode = FIRIAMRenderAsBannerView; + + title = bannerNode[@"title"][@"text"]; + titleTextColor = [UIColor firiam_colorWithHexString:bannerNode[@"title"][@"hexColor"]]; + + body = bannerNode[@"body"][@"text"]; + + imageURLStr = bannerNode[@"imageUrl"]; + actionURLStr = bannerNode[@"action"][@"actionUrl"]; + viewCardBackgroundColor = + [UIColor firiam_colorWithHexString:bannerNode[@"backgroundHexColor"]]; + + } else if ([content[@"modal"] isKindOfClass:[NSDictionary class]]) { + mode = FIRIAMRenderAsModalView; + + NSDictionary *modalNode = (NSDictionary *)contentNode[@"modal"]; + title = modalNode[@"title"][@"text"]; + titleTextColor = [UIColor firiam_colorWithHexString:modalNode[@"title"][@"hexColor"]]; + + body = modalNode[@"body"][@"text"]; + + imageURLStr = modalNode[@"imageUrl"]; + actionButtonText = modalNode[@"actionButton"][@"text"][@"text"]; + btnBgColor = + [UIColor firiam_colorWithHexString:modalNode[@"actionButton"][@"buttonHexColor"]]; + + actionURLStr = modalNode[@"action"][@"actionUrl"]; + viewCardBackgroundColor = + [UIColor firiam_colorWithHexString:modalNode[@"backgroundHexColor"]]; + } else if ([content[@"imageOnly"] isKindOfClass:[NSDictionary class]]) { + mode = FIRIAMRenderAsImageOnlyView; + NSDictionary *imageOnlyNode = (NSDictionary *)contentNode[@"imageOnly"]; + + imageURLStr = imageOnlyNode[@"imageUrl"]; + + if (!imageURLStr) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900007", + @"Image url is missing for image-only message %@", messageNode); + return nil; + } + actionURLStr = imageOnlyNode[@"action"][@"actionUrl"]; + } else { + // Unknown message type + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900003", + @"Unknown message type in message node %@", messageNode); + return nil; + } + + if (title == nil && mode != FIRIAMRenderAsImageOnlyView) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900004", + @"Title text is missing in message node %@", messageNode); + return nil; + } + + NSURL *imageURL = (imageURLStr.length == 0) ? nil : [NSURL URLWithString:imageURLStr]; + NSURL *actionURL = (actionURLStr.length == 0) ? nil : [NSURL URLWithString:actionURLStr]; + FIRIAMRenderingEffectSetting *renderEffect = + [FIRIAMRenderingEffectSetting getDefaultRenderingEffectSetting]; + renderEffect.viewMode = mode; + + if (viewCardBackgroundColor) { + renderEffect.displayBGColor = viewCardBackgroundColor; + } + + if (btnBgColor) { + renderEffect.btnBGColor = btnBgColor; + } + + if (btnTxtColor) { + renderEffect.btnTextColor = btnTxtColor; + } + + if (titleTextColor) { + renderEffect.textColor = titleTextColor; + } + + NSArray *triggersDefinition = + [self parseTriggeringCondition:messageNode[@"triggeringConditions"]]; + + if (isTestMessage) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900008", + @"A test message with id %@ was parsed successfully.", messageID); + renderEffect.isTestMessage = YES; + } else { + // Triggering definitions should always be present for a non-test message. + if (!triggersDefinition || triggersDefinition.count == 0) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900009", + @"No valid triggering condition is detected in message definition" + " with id %@", + messageID); + return nil; + } + } + + FIRIAMMessageContentDataWithImageURL *msgData = + [[FIRIAMMessageContentDataWithImageURL alloc] initWithMessageTitle:title + messageBody:body + actionButtonText:actionButtonText + actionURL:actionURL + imageURL:imageURL + usingURLSession:nil]; + + FIRIAMMessageRenderData *renderData = + [[FIRIAMMessageRenderData alloc] initWithMessageID:messageID + messageName:messageName + contentData:msgData + renderingEffect:renderEffect]; + + if (isTestMessage) { + return [[FIRIAMMessageDefinition alloc] initTestMessageWithRenderData:renderData]; + } else { + return [[FIRIAMMessageDefinition alloc] initWithRenderData:renderData + startTime:startTimeInSeconds + endTime:endTimeInSeconds + triggerDefinition:triggersDefinition]; + } + } @catch (NSException *e) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900006", + @"Error in parsing message node %@ " + "with error %@", + messageNode, e); + return nil; + } +} +@end diff --git a/Firebase/InAppMessaging/Data/FIRIAMMessageContentData.h b/Firebase/InAppMessaging/Data/FIRIAMMessageContentData.h new file mode 100644 index 00000000000..663096e192a --- /dev/null +++ b/Firebase/InAppMessaging/Data/FIRIAMMessageContentData.h @@ -0,0 +1,42 @@ +/* + * 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 + +NS_ASSUME_NONNULL_BEGIN +/** + * This protocol models the message content (non-ui related) data for an in-app message. + */ +@protocol FIRIAMMessageContentData +@property(nonatomic, readonly, nonnull) NSString *titleText; +@property(nonatomic, readonly, nonnull) NSString *bodyText; +@property(nonatomic, readonly, nullable) NSString *actionButtonText; +@property(nonatomic, readonly, nullable) NSURL *actionURL; +@property(nonatomic, readonly, nullable) NSURL *imageURL; + +// Load image data and report the result in the callback block. +// Expect these cases in the callback block +// If error happens, error parameter will be non-nil. +// If no error happens and imageData parameter is nil, it indicates the case that there +// is no image assoicated with the message. +// If error is nil and imageData is not nil, then the image data is loaded successfully +- (void)loadImageDataWithBlock:(void (^)(NSData *_Nullable imageData, + NSError *_Nullable error))block; + +// convert to a description string of the content +- (NSString *)description; +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Data/FIRIAMMessageContentDataWithImageURL.h b/Firebase/InAppMessaging/Data/FIRIAMMessageContentDataWithImageURL.h new file mode 100644 index 00000000000..eecff0986e5 --- /dev/null +++ b/Firebase/InAppMessaging/Data/FIRIAMMessageContentDataWithImageURL.h @@ -0,0 +1,47 @@ +/* + * 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 + +#import "FIRIAMMessageContentData.h" + +NS_ASSUME_NONNULL_BEGIN +/** + * An implementation for protocol FIRIAMMessageContentData. This class takes a image url + * and fetch it over the network to retrieve the image data. + */ +@interface FIRIAMMessageContentDataWithImageURL : NSObject +/** + * Create an instance which uses NSURLSession to do the image data fetching. + * + * @param title Message title text. + * @param body Message body text. + * @param actionButtonText Text for action button. + * @param actionURL url string for action. + * @param imageURL the url to the image. It can be nil to indicate the non-image in-app + * message case. + * @param URLSession can be nil in which case the class would create NSURLSession + * internally to perform the network request. Having it here so that + * it's easier for doing mocking with unit testing. + */ +- (instancetype)initWithMessageTitle:(NSString *)title + messageBody:(NSString *)body + actionButtonText:(nullable NSString *)actionButtonText + actionURL:(nullable NSURL *)actionURL + imageURL:(nullable NSURL *)imageURL + usingURLSession:(nullable NSURLSession *)URLSession; +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Data/FIRIAMMessageContentDataWithImageURL.m b/Firebase/InAppMessaging/Data/FIRIAMMessageContentDataWithImageURL.m new file mode 100644 index 00000000000..82a09f87b9f --- /dev/null +++ b/Firebase/InAppMessaging/Data/FIRIAMMessageContentDataWithImageURL.m @@ -0,0 +1,138 @@ +/* + * 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 +#import + +#import "FIRCore+InAppMessaging.h" +#import "FIRIAMMessageContentData.h" +#import "FIRIAMMessageContentDataWithImageURL.h" +#import "FIRIAMSDKRuntimeErrorCodes.h" + +static NSInteger const SuccessHTTPStatusCode = 200; + +@interface FIRIAMMessageContentDataWithImageURL () +@property(nonatomic, readwrite, nonnull, copy) NSString *titleText; +@property(nonatomic, readwrite, nonnull, copy) NSString *bodyText; +@property(nonatomic, copy, nullable) NSString *actionButtonText; +@property(nonatomic, copy, nullable) NSURL *actionURL; +@property(nonatomic, nullable, copy) NSURL *imageURL; +@property(readonly) NSURLSession *URLSession; +@end + +@implementation FIRIAMMessageContentDataWithImageURL +- (instancetype)initWithMessageTitle:(NSString *)title + messageBody:(NSString *)body + actionButtonText:(nullable NSString *)actionButtonText + actionURL:(nullable NSURL *)actionURL + imageURL:(nullable NSURL *)imageURL + usingURLSession:(nullable NSURLSession *)URLSession { + if (self = [super init]) { + _titleText = title; + _bodyText = body; + _imageURL = imageURL; + _actionButtonText = actionButtonText; + _actionURL = actionURL; + + if (imageURL) { + _URLSession = URLSession ? URLSession : [NSURLSession sharedSession]; + } + } + return self; +} + +#pragma protocol FIRIAMMessageContentData + +- (NSString *)description { + return [NSString stringWithFormat:@"Message content: title '%@'," + "body '%@', imageURL '%@', action URL '%@'", + self.titleText, self.bodyText, self.imageURL, self.actionURL]; +} + +- (NSString *)getTitleText { + return _titleText; +} + +- (NSString *)getBodyText { + return _bodyText; +} + +- (nullable NSString *)getActionButtonText { + return _actionButtonText; +} + +- (void)loadImageDataWithBlock:(void (^)(NSData *_Nullable imageData, + NSError *_Nullable error))block { + if (!block) { + // no need for any further action if block is nil + return; + } + + if (!_imageURL) { + // no image data since image url is nil + block(nil, nil); + } else { + NSURLRequest *imageDataRequest = [NSURLRequest requestWithURL:_imageURL]; + NSURLSessionDataTask *task = [_URLSession + dataTaskWithRequest:imageDataRequest + completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + if (error) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM000003", + @"Error in fetching image: %@", error); + block(nil, error); + } else { + if ([response isKindOfClass:[NSHTTPURLResponse class]]) { + NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; + if (httpResponse.statusCode == SuccessHTTPStatusCode) { + if (httpResponse.MIMEType == nil || ![httpResponse.MIMEType hasPrefix:@"image"]) { + NSString *errorDesc = + [NSString stringWithFormat:@"None image MIME type %@" + " detected for url %@", + httpResponse.MIMEType, self.imageURL]; + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM000004", @"%@", errorDesc); + + NSError *error = + [NSError errorWithDomain:kFirebaseInAppMessagingErrorDomain + code:FIRIAMSDKRuntimeErrorNonImageMimetypeFromImageURL + userInfo:@{NSLocalizedDescriptionKey : errorDesc}]; + block(nil, error); + } else { + block(data, nil); + } + } else { + NSString *errorDesc = + [NSString stringWithFormat:@"Failed HTTP request to crawl image %@: " + "HTTP status code as %ld", + self->_imageURL, (long)httpResponse.statusCode]; + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM000001", @"%@", errorDesc); + NSError *error = + [NSError errorWithDomain:NSURLErrorDomain + code:httpResponse.statusCode + userInfo:@{NSLocalizedDescriptionKey : errorDesc}]; + block(nil, error); + } + } else { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM000002", + @"Internal error: got a non http response from fetching image for " + @"image url as %@", + self->_imageURL); + } + } + }]; + [task resume]; + } +} +@end diff --git a/Firebase/InAppMessaging/Data/FIRIAMMessageDefinition.h b/Firebase/InAppMessaging/Data/FIRIAMMessageDefinition.h new file mode 100644 index 00000000000..ecce5246c55 --- /dev/null +++ b/Firebase/InAppMessaging/Data/FIRIAMMessageDefinition.h @@ -0,0 +1,60 @@ +/* + * 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 + +#import "FIRIAMMessageRenderData.h" + +@class FIRIAMDisplayTriggerDefinition; + +NS_ASSUME_NONNULL_BEGIN +@interface FIRIAMMessageDefinition : NSObject +@property(nonatomic, nonnull, readonly) FIRIAMMessageRenderData *renderData; + +// metadata data that does not affect the rendering content/effect directly +@property(nonatomic, readonly) NSTimeInterval startTime; +@property(nonatomic, readonly) NSTimeInterval endTime; + +// a fiam message can have multiple triggers and any of them on its own can cause +// the message to be rendered +@property(nonatomic, readonly) NSArray *renderTriggers; + +/// A flag for client-side testing messages +@property(nonatomic, readonly) BOOL isTestMessage; + +- (instancetype)init NS_UNAVAILABLE; + +/** + * Create a regular message definition. + */ +- (instancetype)initWithRenderData:(FIRIAMMessageRenderData *)renderData + startTime:(NSTimeInterval)startTime + endTime:(NSTimeInterval)endTime + triggerDefinition:(NSArray *)renderTriggers; + +/** + * Create a test message definition. + */ +- (instancetype)initTestMessageWithRenderData:(FIRIAMMessageRenderData *)renderData; + +- (BOOL)messageHasExpired; +- (BOOL)messageHasStarted; + +// should this message be rendered when the app gets foregrounded? +- (BOOL)messageRenderedOnAppForegroundEvent; +// should this message be rendered when a given analytics event is fired? +- (BOOL)messageRenderedOnAnalyticsEvent:(NSString *)eventName; +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Data/FIRIAMMessageDefinition.m b/Firebase/InAppMessaging/Data/FIRIAMMessageDefinition.m new file mode 100644 index 00000000000..fa083fbe448 --- /dev/null +++ b/Firebase/InAppMessaging/Data/FIRIAMMessageDefinition.m @@ -0,0 +1,85 @@ +/* + * 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 "FIRIAMMessageDefinition.h" +#import "FIRIAMDisplayTriggerDefinition.h" + +@implementation FIRIAMMessageRenderData + +- (instancetype)initWithMessageID:(NSString *)messageID + messageName:(NSString *)messageName + contentData:(id)contentData + renderingEffect:(FIRIAMRenderingEffectSetting *)renderEffect { + if (self = [super init]) { + _contentData = contentData; + _renderingEffectSettings = renderEffect; + _messageID = [messageID copy]; + _name = [messageName copy]; + } + return self; +} +@end + +@implementation FIRIAMMessageDefinition +- (instancetype)initWithRenderData:(FIRIAMMessageRenderData *)renderData + startTime:(NSTimeInterval)startTime + endTime:(NSTimeInterval)endTime + triggerDefinition:(NSArray *)renderTriggers { + if (self = [super init]) { + _renderData = renderData; + _renderTriggers = renderTriggers; + _startTime = startTime; + _endTime = endTime; + _isTestMessage = NO; + } + return self; +} + +- (instancetype)initTestMessageWithRenderData:(FIRIAMMessageRenderData *)renderData { + if (self = [super init]) { + _renderData = renderData; + _isTestMessage = YES; + } + return self; +} + +- (BOOL)messageHasExpired { + return self.endTime < [[NSDate date] timeIntervalSince1970]; +} + +- (BOOL)messageRenderedOnAppForegroundEvent { + for (FIRIAMDisplayTriggerDefinition *nextTrigger in self.renderTriggers) { + if (nextTrigger.triggerType == FIRIAMRenderTriggerOnAppForeground) { + return YES; + } + } + return NO; +} + +- (BOOL)messageRenderedOnAnalyticsEvent:(NSString *)eventName { + for (FIRIAMDisplayTriggerDefinition *nextTrigger in self.renderTriggers) { + if (nextTrigger.triggerType == FIRIAMRenderTriggerOnFirebaseAnalyticsEvent && + [nextTrigger.firebaseEventName isEqualToString:eventName]) { + return YES; + } + } + return NO; +} + +- (BOOL)messageHasStarted { + return self.startTime < [[NSDate date] timeIntervalSince1970]; +} +@end diff --git a/Firebase/InAppMessaging/Data/FIRIAMMessageRenderData.h b/Firebase/InAppMessaging/Data/FIRIAMMessageRenderData.h new file mode 100644 index 00000000000..b414657dcd4 --- /dev/null +++ b/Firebase/InAppMessaging/Data/FIRIAMMessageRenderData.h @@ -0,0 +1,36 @@ +/* + * 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 +#import "FIRIAMRenderingEffectSetting.h" + +@protocol FIRIAMMessageContentData; +NS_ASSUME_NONNULL_BEGIN +// This wraps the data that's needed for render the message's content in UI. It also contains +// certain meta data that's needed in responding to user's action +@interface FIRIAMMessageRenderData : NSObject +@property(nonatomic, nonnull, readonly) id contentData; +@property(nonatomic, nonnull, readonly) FIRIAMRenderingEffectSetting *renderingEffectSettings; +@property(nonatomic, nonnull, copy, readonly) NSString *messageID; +@property(nonatomic, nonnull, copy, readonly) NSString *name; + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithMessageID:(NSString *)messageID + messageName:(NSString *)messageName + contentData:(id)contentData + renderingEffect:(FIRIAMRenderingEffectSetting *)renderEffect; +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Data/FIRIAMRenderingEffectSetting.h b/Firebase/InAppMessaging/Data/FIRIAMRenderingEffectSetting.h new file mode 100644 index 00000000000..dfc648ca344 --- /dev/null +++ b/Firebase/InAppMessaging/Data/FIRIAMRenderingEffectSetting.h @@ -0,0 +1,56 @@ +/* + * 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 +#import + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(NSInteger, FIRIAMRenderingMode) { + FIRIAMRenderAsBannerView, + FIRIAMRenderAsModalView, + FIRIAMRenderAsImageOnlyView +}; + +/** + * A class for modeling rendering effect settings for in-app messaging + */ +@interface FIRIAMRenderingEffectSetting : NSObject + +@property(nonatomic) FIRIAMRenderingMode viewMode; + +// background color for the display area, including both the text's background and +// padding's background +@property(nonatomic, copy) UIColor *displayBGColor; + +// text color, covering both the title and body texts +@property(nonatomic, copy) UIColor *textColor; + +// text color for action button +@property(nonatomic, copy) UIColor *btnTextColor; + +// background color for action button +@property(nonatomic, copy) UIColor *btnBGColor; + +// duration of the banner view before triggering auto-dismiss +@property(nonatomic) CGFloat autoDimissBannerAfterNSeconds; + +// A flag to control rendering the message as a client-side testing message +@property(nonatomic) BOOL isTestMessage; + ++ (instancetype)getDefaultRenderingEffectSetting; +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Data/FIRIAMRenderingEffectSetting.m b/Firebase/InAppMessaging/Data/FIRIAMRenderingEffectSetting.m new file mode 100644 index 00000000000..6fd7fb43f7e --- /dev/null +++ b/Firebase/InAppMessaging/Data/FIRIAMRenderingEffectSetting.m @@ -0,0 +1,31 @@ +/* + * 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 "FIRIAMRenderingEffectSetting.h" + +@implementation FIRIAMRenderingEffectSetting + ++ (instancetype)getDefaultRenderingEffectSetting { + FIRIAMRenderingEffectSetting *setting = [[FIRIAMRenderingEffectSetting alloc] init]; + + setting.btnBGColor = [UIColor colorWithRed:0.3 green:0.55 blue:0.28 alpha:1.0]; + setting.displayBGColor = [UIColor whiteColor]; + setting.textColor = [UIColor blackColor]; + setting.btnTextColor = [UIColor whiteColor]; + setting.autoDimissBannerAfterNSeconds = 12; + setting.isTestMessage = NO; + return setting; +} +@end diff --git a/Firebase/InAppMessaging/DisplayTrigger/FIRIAMDisplayTriggerDefinition.h b/Firebase/InAppMessaging/DisplayTrigger/FIRIAMDisplayTriggerDefinition.h new file mode 100644 index 00000000000..cba59391c45 --- /dev/null +++ b/Firebase/InAppMessaging/DisplayTrigger/FIRIAMDisplayTriggerDefinition.h @@ -0,0 +1,34 @@ +/* + * 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 + +typedef NS_ENUM(NSInteger, FIRIAMRenderTrigger) { + FIRIAMRenderTriggerOnAppForeground, + FIRIAMRenderTriggerOnFirebaseAnalyticsEvent +}; + +NS_ASSUME_NONNULL_BEGIN +@interface FIRIAMDisplayTriggerDefinition : NSObject +@property(nonatomic, readonly) FIRIAMRenderTrigger triggerType; + +// applicable only when triggerType == FIRIAMRenderTriggerOnFirebaseAnalyticsEvent +@property(nonatomic, copy, nullable, readonly) NSString *firebaseEventName; + +- (instancetype)initForAppForegroundTrigger; +- (instancetype)initWithFirebaseAnalyticEvent:(NSString *)title; +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/DisplayTrigger/FIRIAMDisplayTriggerDefinition.m b/Firebase/InAppMessaging/DisplayTrigger/FIRIAMDisplayTriggerDefinition.m new file mode 100644 index 00000000000..3613de24696 --- /dev/null +++ b/Firebase/InAppMessaging/DisplayTrigger/FIRIAMDisplayTriggerDefinition.m @@ -0,0 +1,33 @@ +/* + * 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 "FIRIAMDisplayTriggerDefinition.h" + +@implementation FIRIAMDisplayTriggerDefinition +- (instancetype)initForAppForegroundTrigger { + if (self = [super init]) { + _triggerType = FIRIAMRenderTriggerOnAppForeground; + } + return self; +} +- (instancetype)initWithFirebaseAnalyticEvent:(NSString *)title { + if (self = [super init]) { + _triggerType = FIRIAMRenderTriggerOnFirebaseAnalyticsEvent; + _firebaseEventName = title; + } + return self; +} +@end diff --git a/Firebase/InAppMessaging/FIRCore+InAppMessaging.h b/Firebase/InAppMessaging/FIRCore+InAppMessaging.h new file mode 100644 index 00000000000..e38593b26a5 --- /dev/null +++ b/Firebase/InAppMessaging/FIRCore+InAppMessaging.h @@ -0,0 +1,36 @@ +/* + * 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 + +// This file contains declarations that should go into FirebaseCore when +// Firebase InAppMessaging is merged into master. Keep them separate now to help +// with build from development folder and avoid merge conflicts. + +// this should eventually be in FIRLogger.h +extern FIRLoggerService kFIRLoggerInAppMessaging; + +// this should eventually be in FIRError.h +extern NSString *const kFirebaseInAppMessagingErrorDomain; + +// this should eventually be in FIRError.h FIRAppInternal.h:46: +extern NSString *const kFIRServiceInAppMessaging; + +// InAppMessaging doesn't provide any functionality to other components, +// so it provides a private, empty protocol that it conforms to and use it for registration. + +@protocol FIRInAppMessagingInstanceProvider +@end diff --git a/Firebase/InAppMessaging/FIRCore+InAppMessaging.m b/Firebase/InAppMessaging/FIRCore+InAppMessaging.m new file mode 100644 index 00000000000..219c48bedf4 --- /dev/null +++ b/Firebase/InAppMessaging/FIRCore+InAppMessaging.m @@ -0,0 +1,22 @@ +/* + * 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 +#import "FIRCore+InAppMessaging.h" + +NSString *const kFIRServiceInAppMessaging = @"InAppMessaging"; +NSString *const kFirebaseInAppMessagingErrorDomain = @"com.firebase.inappmessaging"; +FIRLoggerService kFIRLoggerInAppMessaging = @"[Firebase/InAppMessaging]"; diff --git a/Firebase/InAppMessaging/FIRInAppMessaging.m b/Firebase/InAppMessaging/FIRInAppMessaging.m new file mode 100644 index 00000000000..8ae243cbde3 --- /dev/null +++ b/Firebase/InAppMessaging/FIRInAppMessaging.m @@ -0,0 +1,144 @@ +/* + * 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 "FIRInAppMessaging.h" + +#import + +#import +#import +#import +#import +#import + +#import "FIRCore+InAppMessaging.h" +#import "FIRIAMDisplayExecutor.h" +#import "FIRIAMRuntimeManager.h" +#import "FIRInAppMessaging+Bootstrap.h" +#import "FIRInAppMessagingPrivate.h" + +static BOOL _autoBootstrapOnFIRAppInit = YES; + +@implementation FIRInAppMessaging { + BOOL _messageDisplaySuppressed; +} + +// Call this to present the SDK being auto bootstrapped with other Firebase SDKs. It needs +// to be triggered before [FIRApp configure] is executed. This should only be needed for +// testing app that wants to use custom fiam SDK settings. ++ (void)disableAutoBootstrapWithFIRApp { + _autoBootstrapOnFIRAppInit = NO; +} + +// extract macro value into a C string +#define STR_FROM_MACRO(x) #x +#define STR(x) STR_FROM_MACRO(x) + ++ (void)load { + [FIRApp + registerInternalLibrary:(Class)self + withName:@"fire-iam" + withVersion:[NSString stringWithUTF8String:STR(FIRInAppMessaging_LIB_VERSION)]]; +} + ++ (nonnull NSArray *)componentsToRegister { + FIRDependency *analyticsDep = [FIRDependency dependencyWithProtocol:@protocol(FIRAnalyticsInterop) + isRequired:YES]; + FIRComponentCreationBlock creationBlock = + ^id _Nullable(FIRComponentContainer *container, BOOL *isCacheable) { + // Ensure it's cached so it returns the same instance every time fiam is called. + *isCacheable = YES; + id analytics = FIR_COMPONENT(FIRAnalyticsInterop, container); + return [[FIRInAppMessaging alloc] initWithAnalytics:analytics]; + }; + FIRComponent *fiamProvider = + [FIRComponent componentWithProtocol:@protocol(FIRInAppMessagingInstanceProvider) + instantiationTiming:FIRInstantiationTimingLazy + dependencies:@[ analyticsDep ] + creationBlock:creationBlock]; + + return @[ fiamProvider ]; +} + ++ (void)configureWithApp:(FIRApp *)app { + if (!app.isDefaultApp) { + // Only configure for the default FIRApp. + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM170000", + @"Firebase InAppMessaging only works with the default app."); + return; + } + + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM170001", + @"Got notification for kFIRAppReadyToConfigureSDKNotification"); + if (_autoBootstrapOnFIRAppInit) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM170002", + @"Auto bootstrap Firebase in-app messaging SDK"); + [self bootstrapIAMFromFIRApp:app]; + } else { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM170003", + @"No auto bootstrap Firebase in-app messaging SDK"); + } +} + +- (instancetype)initWithAnalytics:(id)analytics { + if (self = [super init]) { + _messageDisplaySuppressed = NO; + _analytics = analytics; + } + return self; +} + ++ (FIRInAppMessaging *)inAppMessaging { + FIRApp *defaultApp = [FIRApp defaultApp]; // Missing configure will be logged here. + id inAppMessaging = + FIR_COMPONENT(FIRInAppMessagingInstanceProvider, defaultApp.container); + return (FIRInAppMessaging *)inAppMessaging; +} + +- (BOOL)messageDisplaySuppressed { + return _messageDisplaySuppressed; +} + +- (void)setMessageDisplaySuppressed:(BOOL)suppressed { + _messageDisplaySuppressed = suppressed; + [[FIRIAMRuntimeManager getSDKRuntimeInstance] setShouldSuppressMessageDisplay:suppressed]; +} + +- (BOOL)automaticDataCollectionEnabled { + return [FIRIAMRuntimeManager getSDKRuntimeInstance].automaticDataCollectionEnabled; +} + +- (void)setAutomaticDataCollectionEnabled:(BOOL)automaticDataCollectionEnabled { + [FIRIAMRuntimeManager getSDKRuntimeInstance].automaticDataCollectionEnabled = + automaticDataCollectionEnabled; +} + +- (void)setMessageDisplayComponent:(id)messageDisplayComponent { + _messageDisplayComponent = messageDisplayComponent; + + if (messageDisplayComponent == nil) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM290002", @"messageDisplayComponent set to nil."); + } else { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM290001", + @"Setting a non-nil message display component"); + } + + // Forward the setting to the display executor. + [FIRIAMRuntimeManager getSDKRuntimeInstance].displayExecutor.messageDisplayComponent = + messageDisplayComponent; +} + +@end diff --git a/Firebase/InAppMessaging/FIRInAppMessagingPrivate.h b/Firebase/InAppMessaging/FIRInAppMessagingPrivate.h new file mode 100644 index 00000000000..87c0a3cd7b7 --- /dev/null +++ b/Firebase/InAppMessaging/FIRInAppMessagingPrivate.h @@ -0,0 +1,26 @@ +/* + * Copyright 2019 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 +#import "FIRCore+InAppMessaging.h" +#import "FIRInAppMessaging.h" + +@protocol FIRInAppMessagingInstanceProvider; +@protocol FIRLibrary; + +@interface FIRInAppMessaging () +@property(nonatomic, readwrite, strong) id _Nullable analytics; +@end diff --git a/Firebase/InAppMessaging/Flows/FIRIAMActivityLogger.h b/Firebase/InAppMessaging/Flows/FIRIAMActivityLogger.h new file mode 100644 index 00000000000..82fb06f41ae --- /dev/null +++ b/Firebase/InAppMessaging/Flows/FIRIAMActivityLogger.h @@ -0,0 +1,89 @@ +/* + * 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 + +/// Values for different fiam activity types. +typedef NS_ENUM(NSInteger, FIRIAMActivityType) { + FIRIAMActivityTypeFetchMessage = 0, + FIRIAMActivityTypeRenderMessage = 1, + FIRIAMActivityTypeDismissMessage = 2, + + // Triggered checks + FIRIAMActivityTypeCheckForOnOpenMessage = 3, + FIRIAMActivityTypeCheckForAnalyticsEventMessage = 4, + FIRIAMActivityTypeCheckForFetch = 5, +}; + +NS_ASSUME_NONNULL_BEGIN +@interface FIRIAMActivityRecord : NSObject +@property(nonatomic, nonnull, readonly) NSDate *timestamp; +@property(nonatomic, readonly) FIRIAMActivityType activityType; +@property(nonatomic, readonly) BOOL success; +@property(nonatomic, copy, nonnull, readonly) NSString *detail; + +- (instancetype)init NS_UNAVAILABLE; +// Current timestamp would be fetched if parameter 'timestamp' is passed in as null +- (instancetype)initWithActivityType:(FIRIAMActivityType)type + isSuccessful:(BOOL)isSuccessful + withDetail:(NSString *)detail + timestamp:(nullable NSDate *)timestamp; + +- (NSString *)displayStringForActivityType; +@end + +/** + * This is the class for tracking fiam flow related activity logs. Its content can later on be + * retrieved for debugging/reporting purpose. + */ +@interface FIRIAMActivityLogger : NSObject + +// If it's NO, activity logs of certain types won't get recorded by Logger. Consult +// isMandatoryType implementation to tell what are the types belong to verbose mode +// Turn it on for debugging cases +@property(nonatomic, readonly) BOOL verboseMode; + +- (instancetype)init NS_UNAVAILABLE; + +/** + * Parameter maxBeforeReduce and sizeAfterReduce defines the shrinking behavior when we reach + * the size cap of log storage: when we see the number of log records goes beyond + * maxBeforeReduce, we would trigger a reduction action which would bring the array length to be + * the size as defined by sizeAfterReduce + * + * @param verboseMode see the comments for the verboseMode property + * @param loadFromCache loads from cache to initialize the log list if it's true. Be aware that + * in this case, you should not call this method in main thread since reading the cache file + * can take time. + */ +- (instancetype)initWithMaxCountBeforeReduce:(NSInteger)maxBeforeReduce + withSizeAfterReduce:(NSInteger)sizeAfterReduce + verboseMode:(BOOL)verboseMode + loadFromCache:(BOOL)loadFromCache; + +/** + * Inserting a new record into activity log. + * + * @param newRecord new record to be inserted + */ +- (void)addLogRecord:(FIRIAMActivityRecord *)newRecord; + +/** + * Get a immutable copy of the existing activity log records. + */ +- (NSArray *)readRecords; +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Flows/FIRIAMActivityLogger.m b/Firebase/InAppMessaging/Flows/FIRIAMActivityLogger.m new file mode 100644 index 00000000000..95c344847af --- /dev/null +++ b/Firebase/InAppMessaging/Flows/FIRIAMActivityLogger.m @@ -0,0 +1,215 @@ +/* + * 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 + +#import + +#import "FIRCore+InAppMessaging.h" +#import "FIRIAMActivityLogger.h" +@implementation FIRIAMActivityRecord + +static NSString *const kActiveTypeArchiveKey = @"type"; +static NSString *const kIsSuccessArchiveKey = @"is_success"; +static NSString *const kTimeStampArchiveKey = @"timestamp"; +static NSString *const kDetailArchiveKey = @"detail"; + +- (id)initWithCoder:(NSCoder *)decoder { + self = [super init]; + if (self != nil) { + _activityType = [decoder decodeIntegerForKey:kActiveTypeArchiveKey]; + _timestamp = [decoder decodeObjectForKey:kTimeStampArchiveKey]; + _success = [decoder decodeBoolForKey:kIsSuccessArchiveKey]; + _detail = [decoder decodeObjectForKey:kDetailArchiveKey]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)encoder { + [encoder encodeInteger:self.activityType forKey:kActiveTypeArchiveKey]; + [encoder encodeObject:self.timestamp forKey:kTimeStampArchiveKey]; + [encoder encodeBool:self.success forKey:kIsSuccessArchiveKey]; + [encoder encodeObject:self.detail forKey:kDetailArchiveKey]; +} + +- (instancetype)initWithActivityType:(FIRIAMActivityType)type + isSuccessful:(BOOL)isSuccessful + withDetail:(NSString *)detail + timestamp:(nullable NSDate *)timestamp { + if (self = [super init]) { + _activityType = type; + _success = isSuccessful; + _detail = detail; + _timestamp = timestamp ? timestamp : [[NSDate alloc] init]; + } + return self; +} + +- (NSString *)displayStringForActivityType { + switch (self.activityType) { + case FIRIAMActivityTypeFetchMessage: + return @"Message Fetching"; + case FIRIAMActivityTypeRenderMessage: + return @"Message Rendering"; + case FIRIAMActivityTypeDismissMessage: + return @"Message Dismiss"; + case FIRIAMActivityTypeCheckForOnOpenMessage: + return @"OnOpen Msg Check"; + case FIRIAMActivityTypeCheckForAnalyticsEventMessage: + return @"Analytic Msg Check"; + case FIRIAMActivityTypeCheckForFetch: + return @"Fetch Check"; + } +} +@end + +@interface FIRIAMActivityLogger () +@property(nonatomic) BOOL isDirty; + +// always insert at the head of this array so that they are always in anti-chronological order +@property(nonatomic, nonnull) NSMutableArray *activityRecords; + +// When we see the number of log records goes beyond maxRecordCountBeforeReduce, we would trigger +// a reduction action which would bring the array length to be the size as defined by +// newSizeAfterReduce +@property(nonatomic, readonly) NSInteger maxRecordCountBeforeReduce; +@property(nonatomic, readonly) NSInteger newSizeAfterReduce; + +@end + +@implementation FIRIAMActivityLogger +- (instancetype)initWithMaxCountBeforeReduce:(NSInteger)maxBeforeReduce + withSizeAfterReduce:(NSInteger)sizeAfterReduce + verboseMode:(BOOL)verboseMode + loadFromCache:(BOOL)loadFromCache { + if (self = [super init]) { + _maxRecordCountBeforeReduce = maxBeforeReduce; + _newSizeAfterReduce = sizeAfterReduce; + _activityRecords = [[NSMutableArray alloc] init]; + _verboseMode = verboseMode; + _isDirty = NO; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(appWillBecomeInactive) + name:UIApplicationWillResignActiveNotification + object:nil]; + + if (loadFromCache) { + @try { + [self loadFromCachePath:nil]; + } @catch (NSException *exception) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM310003", + @"Non-fatal exception in loading persisted activity log records: %@.", + exception); + } + } + } + return self; +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + ++ (NSString *)determineCacheFilePath { + static NSString *logCachePath; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + NSString *cacheDirPath = + NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES)[0]; + logCachePath = [NSString stringWithFormat:@"%@/firebase-iam-activity-log-cache", cacheDirPath]; + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM310001", + @"Persistent file path for activity log data is %@", logCachePath); + }); + return logCachePath; +} + +- (void)loadFromCachePath:(NSString *)cacheFilePath { + NSString *filePath = cacheFilePath == nil ? [self.class determineCacheFilePath] : cacheFilePath; + + id fetchedActivityRecords = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath]; + + if (fetchedActivityRecords) { + @synchronized(self) { + self.activityRecords = (NSMutableArray *)fetchedActivityRecords; + self.isDirty = NO; + } + } +} + +- (BOOL)saveIntoCacheWithPath:(NSString *)cacheFilePath { + NSString *filePath = cacheFilePath == nil ? [self.class determineCacheFilePath] : cacheFilePath; + @synchronized(self) { + BOOL result = [NSKeyedArchiver archiveRootObject:self.activityRecords toFile:filePath]; + if (result) { + self.isDirty = NO; + } + return result; + } +} + +- (void)appWillBecomeInactive { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM310004", + @"App will become inactive, save" + " activity logs"); + + if (self.isDirty) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul), ^{ + if ([self saveIntoCacheWithPath:nil]) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM310002", + @"Persisting activity log data is was successful"); + } else { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM310005", + @"Persisting activity log data has failed"); + } + }); + } +} + +// Helper function to determine if a given activity type should be recorded under +// non verbose type. ++ (BOOL)isMandatoryType:(FIRIAMActivityType)type { + switch (type) { + case FIRIAMActivityTypeFetchMessage: + case FIRIAMActivityTypeRenderMessage: + case FIRIAMActivityTypeDismissMessage: + return YES; + default: + return NO; + } +} + +- (void)addLogRecord:(FIRIAMActivityRecord *)newRecord { + if (self.verboseMode || [FIRIAMActivityLogger isMandatoryType:newRecord.activityType]) { + @synchronized(self) { + [self.activityRecords insertObject:newRecord atIndex:0]; + + if (self.activityRecords.count >= self.maxRecordCountBeforeReduce) { + NSRange removeRange; + removeRange.location = self.newSizeAfterReduce; + removeRange.length = self.maxRecordCountBeforeReduce - self.newSizeAfterReduce; + [self.activityRecords removeObjectsInRange:removeRange]; + } + self.isDirty = YES; + } + } +} + +- (NSArray *)readRecords { + return [self.activityRecords copy]; +} +@end diff --git a/Firebase/InAppMessaging/Flows/FIRIAMBookKeeper.h b/Firebase/InAppMessaging/Flows/FIRIAMBookKeeper.h new file mode 100644 index 00000000000..1c9a11045a4 --- /dev/null +++ b/Firebase/InAppMessaging/Flows/FIRIAMBookKeeper.h @@ -0,0 +1,75 @@ +/* + * 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 + +NS_ASSUME_NONNULL_BEGIN +@interface FIRIAMImpressionRecord : NSObject +@property(nonatomic, readonly, copy) NSString *messageID; +@property(nonatomic, readonly) long impressionTimeInSeconds; + +- (NSString *)description; + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithMessageID:(NSString *)messageID + impressionTimeInSeconds:(long)impressionTime NS_DESIGNATED_INITIALIZER; +@end + +// this protocol defines the interface for classes that can be used to track info regarding +// display & fetch of iam messages. The info tracked here can be used to decide if it's due for +// next display and/or fetch of iam messages. +@protocol FIRIAMBookKeeper +@property(nonatomic, readonly) double lastDisplayTime; +@property(nonatomic, readonly) double lastFetchTime; +@property(nonatomic, readonly) NSTimeInterval nextFetchWaitTime; + +// only call this when it's considered to be a valid impression (for example, meeting the minimum +// display time requirement). +- (void)recordNewImpressionForMessage:(NSString *)messageID + withStartTimestampInSeconds:(double)timestamp; + +- (void)recordNewFetchWithFetchCount:(NSInteger)fetchedMsgCount + withTimestampInSeconds:(double)fetchTimestamp + nextFetchWaitTime:(nullable NSNumber *)nextFetchWaitTime; + +// When we fetch the eligible message list from the sdk server, it can contain messages that are +// already impressed for those that are defined to be displayed repeatedly (messages with custom +// display frequency). We need then clean up the impression records for these messages so that +// they can be displayed again on client side. +- (void)clearImpressionsWithMessageList:(NSArray *)messageList; +// fetch the impression list +- (NSArray *)getImpressions; + +// For certain clients, they only need to get the list of the message ids in existing impression +// records. This is a helper method for that. +- (NSArray *)getMessageIDsFromImpressions; +@end + +// implementation of FIRIAMBookKeeper protocol by storing data within iOS UserDefaults. +// TODO: switch to something else if there is risks for the data being unintentionally deleted by +// the app +@interface FIRIAMBookKeeperViaUserDefaults : NSObject + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithUserDefaults:(NSUserDefaults *)userDefaults NS_DESIGNATED_INITIALIZER; + +// for testing, don't use them for production purpose +- (void)cleanupImpressions; +- (void)cleanupFetchRecords; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Flows/FIRIAMBookKeeper.m b/Firebase/InAppMessaging/Flows/FIRIAMBookKeeper.m new file mode 100644 index 00000000000..a7019d7bd80 --- /dev/null +++ b/Firebase/InAppMessaging/Flows/FIRIAMBookKeeper.m @@ -0,0 +1,260 @@ +/* + * 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 + +#import "FIRCore+InAppMessaging.h" +#import "FIRIAMBookKeeper.h" + +NSString *const FIRIAM_UserDefaultsKeyForImpressions = @"firebase-iam-message-impressions"; +NSString *const FIRIAM_UserDefaultsKeyForLastImpressionTimestamp = + @"firebase-iam-last-impression-timestamp"; +NSString *FIRIAM_UserDefaultsKeyForLastFetchTimestamp = @"firebase-iam-last-fetch-timestamp"; + +// The two keys used to map FIRIAMImpressionRecord object to a NSDictionary object for +// persistence. +NSString *const FIRIAM_ImpressionDictKeyForID = @"message_id"; +NSString *const FIRIAM_ImpressionDictKeyForTimestamp = @"impression_time"; + +static NSString *const kUserDefaultsKeyForFetchWaitTime = @"firebase-iam-fetch-wait-time"; + +// 24 hours +static NSTimeInterval kDefaultFetchWaitTimeInSeconds = 24 * 60 * 60; + +// 3 days +static NSTimeInterval kMaxFetchWaitTimeInSeconds = 3 * 24 * 60 * 60; + +@interface FIRIAMBookKeeperViaUserDefaults () +@property(nonatomic) double lastDisplayTime; +@property(nonatomic) double lastFetchTime; +@property(nonatomic) double nextFetchWaitTime; +@property(nonatomic, nonnull) NSUserDefaults *defaults; +@end + +@interface FIRIAMImpressionRecord () +- (instancetype)initWithStorageDictionary:(NSDictionary *)dict; +@end + +@implementation FIRIAMImpressionRecord + +- (instancetype)initWithMessageID:(NSString *)messageID + impressionTimeInSeconds:(long)impressionTime { + if (self = [super init]) { + _messageID = messageID; + _impressionTimeInSeconds = impressionTime; + } + return self; +} + +- (instancetype)initWithStorageDictionary:(NSDictionary *)dict { + id timestamp = dict[FIRIAM_ImpressionDictKeyForTimestamp]; + id messageID = dict[FIRIAM_ImpressionDictKeyForID]; + + if (![timestamp isKindOfClass:[NSNumber class]] || ![messageID isKindOfClass:[NSString class]]) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM270003", + @"Incorrect data in the dictionary object for creating a FIRIAMImpressionRecord" + " object"); + return nil; + } else { + return [self initWithMessageID:messageID + impressionTimeInSeconds:((NSNumber *)timestamp).longValue]; + } +} + +- (NSString *)description { + return [NSString stringWithFormat:@"%@ impressed at %ld in seconds", self.messageID, + self.impressionTimeInSeconds]; +} +@end + +@implementation FIRIAMBookKeeperViaUserDefaults + +- (instancetype)initWithUserDefaults:(NSUserDefaults *)userDefaults { + if (self = [super init]) { + _defaults = userDefaults; + + // ok if it returns 0 due to the entry being absent + _lastDisplayTime = [_defaults doubleForKey:FIRIAM_UserDefaultsKeyForLastImpressionTimestamp]; + _lastFetchTime = [_defaults doubleForKey:FIRIAM_UserDefaultsKeyForLastFetchTimestamp]; + + id fetchWaitTimeEntry = [_defaults objectForKey:kUserDefaultsKeyForFetchWaitTime]; + + if (![fetchWaitTimeEntry isKindOfClass:NSNumber.class]) { + // This corresponds to the case there is no wait time entry is set in user defaults yet + _nextFetchWaitTime = kDefaultFetchWaitTimeInSeconds; + } else { + _nextFetchWaitTime = ((NSNumber *)fetchWaitTimeEntry).doubleValue; + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM270009", + @"Next fetch wait time loaded from user defaults is %lf", _nextFetchWaitTime); + } + } + return self; +} + +// A helper function for reading and verifying the stored array data for impressions +// in UserDefaults. It returns nil if it does not exist or fail to pass the data type +// checking. +- (NSArray *)fetchImpressionArrayFromStorage { + id impressionsData = [self.defaults objectForKey:FIRIAM_UserDefaultsKeyForImpressions]; + + if (impressionsData && ![impressionsData isKindOfClass:[NSArray class]]) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM270007", + @"Found non-array data from impression userdefaults storage with key %@", + FIRIAM_UserDefaultsKeyForImpressions); + return nil; + } + return (NSArray *)impressionsData; +} + +- (void)recordNewImpressionForMessage:(NSString *)messageID + withStartTimestampInSeconds:(double)timestamp { + @synchronized(self) { + NSArray *oldImpressions = [self fetchImpressionArrayFromStorage]; + // oldImpressions could be nil at the first time + NSMutableArray *newImpressions = + oldImpressions ? [oldImpressions mutableCopy] : [[NSMutableArray alloc] init]; + + // Two cases + // If a prior impression exists for that messageID, update its impression timestamp + // If a prior impression for that messageID does not exist, add a new entry for the + // messageID. + + NSDictionary *newImpressionEntry = @{ + FIRIAM_ImpressionDictKeyForID : messageID, + FIRIAM_ImpressionDictKeyForTimestamp : [NSNumber numberWithDouble:timestamp] + }; + + BOOL oldImpressionRecordFound = NO; + + for (int i = 0; i < newImpressions.count; i++) { + if ([newImpressions[i] isKindOfClass:[NSDictionary class]]) { + NSDictionary *currentItem = (NSDictionary *)newImpressions[i]; + if ([messageID isEqualToString:currentItem[FIRIAM_ImpressionDictKeyForID]]) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM270001", + @"Updating timestamp of existing impression record to be %f for " + "message %@", + timestamp, messageID); + + [newImpressions replaceObjectAtIndex:i withObject:newImpressionEntry]; + oldImpressionRecordFound = YES; + break; + } + } + } + + if (!oldImpressionRecordFound) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM270002", + @"Insert the first impression record for message %@ with timestamp in seconds " + "as %f", + messageID, timestamp); + [newImpressions addObject:newImpressionEntry]; + } + + [self.defaults setObject:newImpressions forKey:FIRIAM_UserDefaultsKeyForImpressions]; + [self.defaults setDouble:timestamp forKey:FIRIAM_UserDefaultsKeyForLastImpressionTimestamp]; + self.lastDisplayTime = timestamp; + } +} + +- (void)clearImpressionsWithMessageList:(NSArray *)messageList { + @synchronized(self) { + NSArray *existingImpressions = [self fetchImpressionArrayFromStorage]; + + NSSet *messageIDSet = [NSSet setWithArray:messageList]; + NSPredicate *notInMessageListPredicate = + [NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) { + if (![evaluatedObject isKindOfClass:[NSDictionary class]]) { + return NO; // unexpected item. Throw it away + } + NSDictionary *impression = (NSDictionary *)evaluatedObject; + return impression[FIRIAM_ImpressionDictKeyForID] && + ![messageIDSet containsObject:impression[FIRIAM_ImpressionDictKeyForID]]; + }]; + + NSArray *updatedImpressions = + [existingImpressions filteredArrayUsingPredicate:notInMessageListPredicate]; + + if (existingImpressions.count != updatedImpressions.count) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM270004", + @"Updating the impression records after purging %d items based on the " + "server fetch response", + (int)(existingImpressions.count - updatedImpressions.count)); + [self.defaults setObject:updatedImpressions forKey:FIRIAM_UserDefaultsKeyForImpressions]; + } else { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM270005", + @"No impression records update due to no change after applying the server " + "message list"); + } + } +} + +- (NSArray *)getImpressions { + NSArray *impressionsFromStorage = [self fetchImpressionArrayFromStorage]; + + NSMutableArray *resultArray = [[NSMutableArray alloc] init]; + + for (NSDictionary *next in impressionsFromStorage) { + FIRIAMImpressionRecord *nextImpression = + [[FIRIAMImpressionRecord alloc] initWithStorageDictionary:next]; + [resultArray addObject:nextImpression]; + } + + return resultArray; +} + +- (NSArray *)getMessageIDsFromImpressions { + NSArray *impressionsFromStorage = [self fetchImpressionArrayFromStorage]; + + NSMutableArray *resultArray = [[NSMutableArray alloc] init]; + + for (NSDictionary *next in impressionsFromStorage) { + [resultArray addObject:next[FIRIAM_ImpressionDictKeyForID]]; + } + + return resultArray; +} + +- (void)recordNewFetchWithFetchCount:(NSInteger)fetchedMsgCount + withTimestampInSeconds:(double)fetchTimestamp + nextFetchWaitTime:(nullable NSNumber *)nextFetchWaitTime; +{ + [self.defaults setDouble:fetchTimestamp forKey:FIRIAM_UserDefaultsKeyForLastFetchTimestamp]; + self.lastFetchTime = fetchTimestamp; + + if (nextFetchWaitTime) { + if (nextFetchWaitTime.doubleValue > kMaxFetchWaitTimeInSeconds) { + FIRLogInfo(kFIRLoggerInAppMessaging, @"I-IAM270006", + @"next fetch wait time %lf is too large. Ignore it.", + nextFetchWaitTime.doubleValue); + } else { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM270008", + @"Setting next fetch wait time as %lf from fetch response.", + nextFetchWaitTime.doubleValue); + self.nextFetchWaitTime = nextFetchWaitTime.doubleValue; + [self.defaults setObject:nextFetchWaitTime forKey:kUserDefaultsKeyForFetchWaitTime]; + } + } +} + +- (void)cleanupImpressions { + [self.defaults setObject:@[] forKey:FIRIAM_UserDefaultsKeyForImpressions]; +} + +- (void)cleanupFetchRecords { + [self.defaults setDouble:0 forKey:FIRIAM_UserDefaultsKeyForLastFetchTimestamp]; + self.lastFetchTime = 0; +} +@end diff --git a/Firebase/InAppMessaging/Flows/FIRIAMClientInfoFetcher.h b/Firebase/InAppMessaging/Flows/FIRIAMClientInfoFetcher.h new file mode 100644 index 00000000000..09b1e6a2ff3 --- /dev/null +++ b/Firebase/InAppMessaging/Flows/FIRIAMClientInfoFetcher.h @@ -0,0 +1,39 @@ +/* + * 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 + +// A class for wrapping the interactions for retrieving client side info to be used in request +// parameter for interacting with Firebase iam servers. + +NS_ASSUME_NONNULL_BEGIN +@interface FIRIAMClientInfoFetcher : NSObject +// Fetch the up-to-date Firebase instance id and token data. Since it involves a server interaction, +// completion callback is provided for receiving the result. +- (void)fetchFirebaseIIDDataWithProjectNumber:(NSString *)projectNumber + withCompletion:(void (^)(NSString *_Nullable iid, + NSString *_Nullable token, + NSError *_Nullable error))completion; + +// Following are synchronous methods for fetching data +- (nullable NSString *)getDeviceLanguageCode; +- (nullable NSString *)getAppVersion; +- (nullable NSString *)getOSVersion; +- (nullable NSString *)getOSMajorVersion; +- (nullable NSString *)getTimezone; +- (NSString *)getIAMSDKVersion; +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Flows/FIRIAMClientInfoFetcher.m b/Firebase/InAppMessaging/Flows/FIRIAMClientInfoFetcher.m new file mode 100644 index 00000000000..b7809770524 --- /dev/null +++ b/Firebase/InAppMessaging/Flows/FIRIAMClientInfoFetcher.m @@ -0,0 +1,120 @@ +/* + * 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 +#import +#import + +#import "FIRCore+InAppMessaging.h" +#import "FIRIAMClientInfoFetcher.h" + +// declaratons for FIRInstanceID SDK +@implementation FIRIAMClientInfoFetcher +- (void)fetchFirebaseIIDDataWithProjectNumber:(NSString *)projectNumber + withCompletion:(void (^)(NSString *_Nullable iid, + NSString *_Nullable token, + NSError *_Nullable error))completion { + FIRInstanceID *iid = [FIRInstanceID instanceID]; + + // tokenWithAuthorizedEntity would only communicate with server on periodical cycles. + // For other times, it's going to fetch from local cache, so it's not causing any performance + // concern in the fetch flow. + [iid tokenWithAuthorizedEntity:projectNumber + scope:@"fiam" + options:nil + handler:^(NSString *_Nullable token, NSError *_Nullable error) { + if (error) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM190001", + @"Error in fetching iid token: %@", + error.localizedDescription); + completion(nil, nil, error); + } else { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM190002", + @"Successfully generated iid token"); + // now we can go ahead to fetch the id + [iid getIDWithHandler:^(NSString *_Nullable identity, + NSError *_Nullable error) { + if (error) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM190004", + @"Error in fetching iid value: %@", + error.localizedDescription); + } else { + FIRLogDebug( + kFIRLoggerInAppMessaging, @"I-IAM190005", + @"Successfully in fetching both iid value as %@ and iid token" + " as %@", + identity, token); + completion(identity, token, nil); + } + }]; + } + }]; +} + +- (nullable NSString *)getDeviceLanguageCode { + // No caching since it's requested at pretty low frequency and we get the benefit of seeing + // updated info the setting has changed + NSArray *preferredLanguages = [NSLocale preferredLanguages]; + return preferredLanguages.firstObject; +} + +- (nullable NSString *)getAppVersion { + // Since this won't change, read it once in the whole life-cycle of the app and cache its value + static NSString *appVersion = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + appVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"]; + }); + return appVersion; +} + +- (nullable NSString *)getOSVersion { + // Since this won't change, read it once in the whole life-cycle of the app and cache its value + static NSString *OSVersion = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSOperatingSystemVersion systemVersion = [NSProcessInfo processInfo].operatingSystemVersion; + OSVersion = [NSString stringWithFormat:@"%ld.%ld.%ld", (long)systemVersion.majorVersion, + (long)systemVersion.minorVersion, + (long)systemVersion.patchVersion]; + }); + return OSVersion; +} + +- (nullable NSString *)getOSMajorVersion { + NSArray *versionItems = [[self getOSVersion] componentsSeparatedByString:@"."]; + + if (versionItems.count > 0) { + return (NSString *)versionItems[0]; + } else { + return nil; + } +} + +- (nullable NSString *)getTimezone { + // No caching to deal with potential changes. + return [NSTimeZone localTimeZone].name; +} + +// extract macro value into a C string +#define STR_FROM_MACRO(x) #x +#define STR(x) STR_FROM_MACRO(x) + +- (NSString *)getIAMSDKVersion { + // FIRInAppMessaging_LIB_VERSION macro comes from pod definition + return [NSString stringWithUTF8String:STR(FIRInAppMessaging_LIB_VERSION)]; +} +@end diff --git a/Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckOnAnalyticEventsFlow.h b/Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckOnAnalyticEventsFlow.h new file mode 100644 index 00000000000..5d379911fe2 --- /dev/null +++ b/Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckOnAnalyticEventsFlow.h @@ -0,0 +1,22 @@ +/* + * 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 "FIRIAMDisplayCheckTriggerFlow.h" + +// An implementation of FIRIAMDisplayCheckTriggerFlow by triggering the display check when +// a Firebase Analytics event is fired. +@interface FIRIAMDisplayCheckOnAnalyticEventsFlow : FIRIAMDisplayCheckTriggerFlow +@end diff --git a/Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckOnAnalyticEventsFlow.m b/Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckOnAnalyticEventsFlow.m new file mode 100644 index 00000000000..e83ba0a62cd --- /dev/null +++ b/Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckOnAnalyticEventsFlow.m @@ -0,0 +1,66 @@ +/* + * 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 +#import +#import + +#import "FIRCore+InAppMessaging.h" +#import "FIRIAMDisplayCheckOnAnalyticEventsFlow.h" +#import "FIRIAMDisplayExecutor.h" +#import "FIRInAppMessagingPrivate.h" + +@interface FIRIAMDisplayCheckOnAnalyticEventsFlow () +@end + +@implementation FIRIAMDisplayCheckOnAnalyticEventsFlow { + dispatch_queue_t eventListenerQueue; +} + +- (void)start { + @synchronized(self) { + if (eventListenerQueue == nil) { + eventListenerQueue = + dispatch_queue_create("com.google.firebase.inappmessage.firevent_listener", NULL); + } + + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM140002", + @"Start observing Firebase Analytics events for rendering messages."); + + [[FIRInAppMessaging inAppMessaging].analytics registerAnalyticsListener:self + withOrigin:@"fiam"]; + } +} + +- (void)messageTriggered:(NSString *)name parameters:(NSDictionary *)parameters { + // Dispatch to a serial queue eventListenerQueue to avoid the complications that two + // concurrent Firebase Analytics events triggering the + // checkAndDisplayNextContextualMessageForAnalyticsEvent flow concurrently. + dispatch_async(self->eventListenerQueue, ^{ + [self.displayExecutor checkAndDisplayNextContextualMessageForAnalyticsEvent:name]; + }); +} + +- (void)stop { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM140003", + @"Stop observing Firebase Analytics events for display check."); + + @synchronized(self) { + [[FIRInAppMessaging inAppMessaging].analytics unregisterAnalyticsListenerWithOrigin:@"fiam"]; + } +} + +@end diff --git a/Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckOnAppForegroundFlow.h b/Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckOnAppForegroundFlow.h new file mode 100644 index 00000000000..f69ee97c794 --- /dev/null +++ b/Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckOnAppForegroundFlow.h @@ -0,0 +1,23 @@ +/* + * 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 "FIRIAMDisplayCheckTriggerFlow.h" + +// an implementation of FIRIAMDisplayExecutor by triggering the display when app is foregrounded +@interface FIRIAMDisplayCheckOnAppForegroundFlow : FIRIAMDisplayCheckTriggerFlow +- (void)start; +- (void)stop; +@end diff --git a/Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckOnAppForegroundFlow.m b/Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckOnAppForegroundFlow.m new file mode 100644 index 00000000000..11d880ae15a --- /dev/null +++ b/Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckOnAppForegroundFlow.m @@ -0,0 +1,55 @@ +/* + * 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 + +#import "FIRCore+InAppMessaging.h" +#import "FIRIAMDisplayCheckOnAppForegroundFlow.h" +#import "FIRIAMDisplayExecutor.h" + +@implementation FIRIAMDisplayCheckOnAppForegroundFlow + +- (void)start { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM500002", + @"Start observing app foreground notifications for rendering messages."); + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(appWillEnterForeground:) + name:UIApplicationWillEnterForegroundNotification + object:nil]; +} + +- (void)appWillEnterForeground:(UIApplication *)application { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM500001", + @"App foregrounded, wake up to check in-app messaging."); + + // Show the message with 0.5 second delay so that the app's UI is more stable. + // When messages are displayed, the UI operation will be dispatched back to main UI thread. + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 500 * (int64_t)NSEC_PER_MSEC), + dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul), ^{ + [self.displayExecutor checkAndDisplayNextAppForegroundMessage]; + }); +} + +- (void)stop { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM500004", + @"Stop observing app foreground notifications."); + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} +@end diff --git a/Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckOnFetchDoneNotificationFlow.h b/Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckOnFetchDoneNotificationFlow.h new file mode 100644 index 00000000000..834561dd4d7 --- /dev/null +++ b/Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckOnFetchDoneNotificationFlow.h @@ -0,0 +1,22 @@ +/* + * Copyright 2018 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 "FIRIAMDisplayCheckTriggerFlow.h" + +@interface FIRIAMDisplayCheckOnFetchDoneNotificationFlow : FIRIAMDisplayCheckTriggerFlow +- (void)start; +- (void)stop; +@end diff --git a/Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckOnFetchDoneNotificationFlow.m b/Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckOnFetchDoneNotificationFlow.m new file mode 100644 index 00000000000..227900e1cf1 --- /dev/null +++ b/Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckOnFetchDoneNotificationFlow.m @@ -0,0 +1,62 @@ +/* + * Copyright 2018 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 + +#import "FIRCore+InAppMessaging.h" + +#import "FIRIAMDisplayCheckOnFetchDoneNotificationFlow.h" +#import "FIRIAMDisplayExecutor.h" + +extern NSString *const kFIRIAMFetchIsDoneNotification; + +@implementation FIRIAMDisplayCheckOnFetchDoneNotificationFlow + +- (void)start { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240001", + @"Start observing fetch done notifications for rendering messages."); + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(fetchIsDone) + name:kFIRIAMFetchIsDoneNotification + object:nil]; +} + +- (void)checkAndRenderMessage { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul), ^{ + [self.displayExecutor checkAndDisplayNextAppForegroundMessage]; + }); +} + +- (void)fetchIsDone { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240002", + @"Fetch is done. Start message rendering flow."); + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 500 * (int64_t)NSEC_PER_MSEC), + dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul), ^{ + [self checkAndRenderMessage]; + }); +} + +- (void)stop { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240003", + @"Stop observing fetch is done notifications."); + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} +@end diff --git a/Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckTriggerFlow.h b/Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckTriggerFlow.h new file mode 100644 index 00000000000..ed05c784b7d --- /dev/null +++ b/Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckTriggerFlow.h @@ -0,0 +1,37 @@ +/* + * 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 + +@class FIRIAMDisplayExecutor; +NS_ASSUME_NONNULL_BEGIN + +// Parent class for modeling different flows in which we would trigger the check to see if there +// is appropriate in-app messaging to be rendered. Notice that the flow only triggers the check +// and whether it turns out to have any eligible message to be displayed depending on if certain +// conditions are met +@interface FIRIAMDisplayCheckTriggerFlow : NSObject + +// Accessed by subclasses, not intended by other clients +@property(nonatomic, nonnull, readonly) FIRIAMDisplayExecutor *displayExecutor; +- (instancetype)initWithDisplayFlow:(FIRIAMDisplayExecutor *)displayExecutor; + +// subclasses should implement the follow two methods to start/stop their concrete +// display check flow +- (void)start; +- (void)stop; +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckTriggerFlow.m b/Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckTriggerFlow.m new file mode 100644 index 00000000000..e95e786ef2e --- /dev/null +++ b/Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckTriggerFlow.m @@ -0,0 +1,32 @@ +/* + * 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 "FIRIAMDisplayCheckTriggerFlow.h" + +@implementation FIRIAMDisplayCheckTriggerFlow +- (instancetype)initWithDisplayFlow:(FIRIAMDisplayExecutor *)displayExecutor { + if (self = [super init]) { + _displayExecutor = displayExecutor; + } + return self; +} + +// Providing fake implementations to avoid xcode complain about incomplete implementation. +- (void)start { +} +- (void)stop { +} +@end diff --git a/Firebase/InAppMessaging/Flows/FIRIAMDisplayExecutor.h b/Firebase/InAppMessaging/Flows/FIRIAMDisplayExecutor.h new file mode 100644 index 00000000000..7137121b477 --- /dev/null +++ b/Firebase/InAppMessaging/Flows/FIRIAMDisplayExecutor.h @@ -0,0 +1,61 @@ +/* + * 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 + +#import "FIRIAMActionURLFollower.h" +#import "FIRIAMActivityLogger.h" +#import "FIRIAMBookKeeper.h" +#import "FIRIAMClearcutLogger.h" +#import "FIRIAMMessageClientCache.h" +#import "FIRIAMTimeFetcher.h" +#import "FIRInAppMessagingRendering.h" + +NS_ASSUME_NONNULL_BEGIN +@interface FIRIAMDisplaySetting : NSObject +@property(nonatomic) NSTimeInterval displayMinIntervalInMinutes; +@end + +// The class for checking if there are appropriate messages to be displayed and if so, render it. +// There are other flows that would determine the timing for the checking and then use this class +// instance for the actual check/display. +// +// In addition to fetch eligible message from message cache, this class also ensures certain +// conditions are satisfied for the rendering +// 1 No current in-app message is being displayed +// 2 For non-contextual messages, the display interval in display setting is met. +@interface FIRIAMDisplayExecutor : NSObject + +- (instancetype)initWithSetting:(FIRIAMDisplaySetting *)setting + messageCache:(FIRIAMMessageClientCache *)cache + timeFetcher:(id)timeFetcher + bookKeeper:(id)displayBookKeeper + actionURLFollower:(FIRIAMActionURLFollower *)actionURLFollower + activityLogger:(FIRIAMActivityLogger *)activityLogger + analyticsEventLogger:(id)analyticsEventLogger; + +// Check and display next in-app message eligible for app open trigger +- (void)checkAndDisplayNextAppForegroundMessage; +// Check and display next in-app message eligible for analytics event trigger with given event name. +- (void)checkAndDisplayNextContextualMessageForAnalyticsEvent:(NSString *)eventName; + +// a boolean flag that can be used to suppress/resume displaying messages. +@property(nonatomic) BOOL suppressMessageDisplay; + +// This is the display component used by display executor for actual message rendering. +@property(nonatomic) id messageDisplayComponent; +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Flows/FIRIAMDisplayExecutor.m b/Firebase/InAppMessaging/Flows/FIRIAMDisplayExecutor.m new file mode 100644 index 00000000000..5cc188df474 --- /dev/null +++ b/Firebase/InAppMessaging/Flows/FIRIAMDisplayExecutor.m @@ -0,0 +1,497 @@ +/* + * 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 + +#import "FIRCore+InAppMessaging.h" +#import "FIRIAMActivityLogger.h" +#import "FIRIAMDisplayExecutor.h" +#import "FIRIAMMessageContentData.h" +#import "FIRIAMMessageDefinition.h" +#import "FIRIAMSDKRuntimeErrorCodes.h" + +@implementation FIRIAMDisplaySetting +@end + +@interface FIRIAMDisplayExecutor () +@property(nonatomic) id timeFetcher; + +// YES if a message is being rendered at this time +@property(nonatomic) BOOL isMsgBeingDisplayed; +@property(nonatomic) NSTimeInterval lastDisplayTime; +@property(nonatomic, nonnull, readonly) FIRIAMDisplaySetting *setting; +@property(nonatomic, nonnull, readonly) FIRIAMMessageClientCache *messageCache; +@property(nonatomic, nonnull, readonly) id displayBookKeeper; +@property(nonatomic) BOOL impressionRecorded; +@property(nonatomic, nonnull, readonly) id analyticsEventLogger; +@property(nonatomic, nonnull, readonly) FIRIAMActionURLFollower *actionURLFollower; +@end + +@implementation FIRIAMDisplayExecutor { + FIRIAMMessageDefinition *_currentMsgBeingDisplayed; +} + +#pragma mark - FIRInAppMessagingDisplayDelegate methods +- (void)messageClicked { + self.isMsgBeingDisplayed = NO; + if (!_currentMsgBeingDisplayed.renderData.messageID) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM400030", + @"messageClicked called but " + "there is no current message ID."); + return; + } + + if (_currentMsgBeingDisplayed.isTestMessage) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400031", + @"A test message clicked. Do test event impression/click analytics logging"); + + [self.analyticsEventLogger + logAnalyticsEventForType:FIRIAMAnalyticsEventTestMessageImpression + forCampaignID:_currentMsgBeingDisplayed.renderData.messageID + withCampaignName:_currentMsgBeingDisplayed.renderData.name + eventTimeInMs:nil + completion:^(BOOL success) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400036", + @"Logging analytics event for url following %@", + success ? @"succeeded" : @"failed"); + }]; + + [self.analyticsEventLogger + logAnalyticsEventForType:FIRIAMAnalyticsEventTestMessageClick + forCampaignID:_currentMsgBeingDisplayed.renderData.messageID + withCampaignName:_currentMsgBeingDisplayed.renderData.name + eventTimeInMs:nil + completion:^(BOOL success) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400039", + @"Logging analytics event for url following %@", + success ? @"succeeded" : @"failed"); + }]; + } else { + // Logging the impression + [self recordValidImpression:_currentMsgBeingDisplayed.renderData.messageID + withMessageName:_currentMsgBeingDisplayed.renderData.name]; + + [self.analyticsEventLogger + logAnalyticsEventForType:FIRIAMAnalyticsEventActionURLFollow + forCampaignID:_currentMsgBeingDisplayed.renderData.messageID + withCampaignName:_currentMsgBeingDisplayed.renderData.name + eventTimeInMs:nil + completion:^(BOOL success) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400032", + @"Logging analytics event for url following %@", + success ? @"succeeded" : @"failed"); + }]; + } + + NSURL *actionURL = _currentMsgBeingDisplayed.renderData.contentData.actionURL; + + if (!actionURL) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400033", + @"messageClicked called but " + "there is no action url specified in the message data."); + // it's equivalent to closing the message with no further action + return; + } else { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400037", @"Following action url %@", + actionURL.absoluteString); + @try { + [self.actionURLFollower + followActionURL:actionURL + withCompletionBlock:^(BOOL success) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400034", + @"Seeing %@ from following action URL", success ? @"success" : @"error"); + }]; + } @catch (NSException *e) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM400035", + @"Exception encountered in following " + "action url (%@): %@ ", + actionURL, e.description); + @throw; + } + } +} + +- (void)messageDismissedWithType:(FIRInAppMessagingDismissType)dismissType { + self.isMsgBeingDisplayed = NO; + if (!_currentMsgBeingDisplayed.renderData.messageID) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM400014", + @"messageDismissedWithType called but " + "there is no current message ID."); + return; + } + + if (_currentMsgBeingDisplayed.isTestMessage) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400020", + @"A test message dismissed. Record the impression event."); + [self.analyticsEventLogger + logAnalyticsEventForType:FIRIAMAnalyticsEventTestMessageImpression + forCampaignID:_currentMsgBeingDisplayed.renderData.messageID + withCampaignName:_currentMsgBeingDisplayed.renderData.name + eventTimeInMs:nil + completion:^(BOOL success) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400038", + @"Logging analytics event for url following %@", + success ? @"succeeded" : @"failed"); + }]; + + return; + } + + // Logging the impression + [self recordValidImpression:_currentMsgBeingDisplayed.renderData.messageID + withMessageName:_currentMsgBeingDisplayed.renderData.name]; + + FIRIAMAnalyticsLogEventType logEventType = dismissType == FIRInAppMessagingDismissTypeAuto + ? FIRIAMAnalyticsEventMessageDismissAuto + : FIRIAMAnalyticsEventMessageDismissClick; + + [self.analyticsEventLogger + logAnalyticsEventForType:logEventType + forCampaignID:_currentMsgBeingDisplayed.renderData.messageID + withCampaignName:_currentMsgBeingDisplayed.renderData.name + eventTimeInMs:nil + completion:^(BOOL success) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400004", + @"Logging analytics event for message dismiss %@", + success ? @"succeeded" : @"failed"); + }]; +} + +- (void)impressionDetected { + if (!_currentMsgBeingDisplayed.renderData.messageID) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM400022", + @"impressionDetected called but " + "there is no current message ID."); + return; + } + + if (!_currentMsgBeingDisplayed.isTestMessage) { + // Displayed long enough to be a valid impression. + [self recordValidImpression:_currentMsgBeingDisplayed.renderData.messageID + withMessageName:_currentMsgBeingDisplayed.renderData.name]; + } else { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400011", + @"A test message. Record the test message impression event."); + return; + } +} + +- (void)displayErrorEncountered:(NSError *)error { + self.isMsgBeingDisplayed = NO; + + if (!_currentMsgBeingDisplayed.renderData.messageID) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM400017", + @"displayErrorEncountered called but " + "there is no current message ID."); + return; + } + + NSString *messageID = _currentMsgBeingDisplayed.renderData.messageID; + + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400009", + @"Display ran into error for message %@: %@", messageID, error); + + if (_currentMsgBeingDisplayed.isTestMessage) { + [self displayMessageLoadError:error]; + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400012", + @"A test message. No analytics tracking " + "from image data loading failure"); + return; + } + + // we remove the message from the client side cache so that it won't be retried until next time + // it's fetched again from server. + [self.messageCache removeMessageWithId:messageID]; + NSString *messageName = _currentMsgBeingDisplayed.renderData.name; + + if ([error.domain isEqualToString:NSURLErrorDomain]) { + [self.analyticsEventLogger + logAnalyticsEventForType:FIRIAMAnalyticsEventImageFetchError + forCampaignID:messageID + withCampaignName:messageName + eventTimeInMs:nil + completion:^(BOOL success) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400010", + @"Logging analytics event for image fetch error %@", + success ? @"succeeded" : @"failed"); + }]; + } else if (error.code == FIRIAMSDKRuntimeErrorNonImageMimetypeFromImageURL) { + [self.analyticsEventLogger + logAnalyticsEventForType:FIRIAMAnalyticsEventImageFormatUnsupported + forCampaignID:messageID + withCampaignName:messageName + eventTimeInMs:nil + completion:^(BOOL success) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400013", + @"Logging analytics event for image format error %@", + success ? @"succeeded" : @"failed"); + }]; + } +} + +- (void)recordValidImpression:(NSString *)messageID withMessageName:(NSString *)messageName { + if (!self.impressionRecorded) { + [self.displayBookKeeper recordNewImpressionForMessage:messageID + withStartTimestampInSeconds:self.lastDisplayTime]; + self.impressionRecorded = YES; + [self.messageCache removeMessageWithId:messageID]; + // Log an impression analytics event as well. + [self.analyticsEventLogger + logAnalyticsEventForType:FIRIAMAnalyticsEventMessageImpression + forCampaignID:messageID + withCampaignName:messageName + eventTimeInMs:nil + completion:^(BOOL success) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400007", + @"Logging analytics event for impression %@", + success ? @"succeeded" : @"failed"); + }]; + } +} + +- (void)displayMessageLoadError:(NSError *)error { + NSString *errorMsg = error.userInfo[NSLocalizedDescriptionKey] + ? error.userInfo[NSLocalizedDescriptionKey] + : @"Message loading failed"; + UIAlertController *alert = [UIAlertController + alertControllerWithTitle:@"Firebase InAppMessaging fail to load a test message" + message:errorMsg + preferredStyle:UIAlertControllerStyleAlert]; + + UIAlertAction *defaultAction = [UIAlertAction actionWithTitle:@"OK" + style:UIAlertActionStyleDefault + handler:^(UIAlertAction *action){ + }]; + + [alert addAction:defaultAction]; + + [[UIApplication sharedApplication].keyWindow.rootViewController presentViewController:alert + animated:YES + completion:nil]; +} + +- (instancetype)initWithSetting:(FIRIAMDisplaySetting *)setting + messageCache:(FIRIAMMessageClientCache *)cache + timeFetcher:(id)timeFetcher + bookKeeper:(id)displayBookKeeper + actionURLFollower:(FIRIAMActionURLFollower *)actionURLFollower + activityLogger:(FIRIAMActivityLogger *)activityLogger + analyticsEventLogger:(id)analyticsEventLogger { + if (self = [super init]) { + _timeFetcher = timeFetcher; + _lastDisplayTime = displayBookKeeper.lastDisplayTime; + _setting = setting; + _messageCache = cache; + _displayBookKeeper = displayBookKeeper; + _isMsgBeingDisplayed = NO; + _analyticsEventLogger = analyticsEventLogger; + _actionURLFollower = actionURLFollower; + _suppressMessageDisplay = NO; // always allow message display on startup + } + return self; +} + +- (void)checkAndDisplayNextContextualMessageForAnalyticsEvent:(NSString *)eventName { + // synchronizing on self so that we won't potentially enter the render flow from two + // threads: example like showing analytics triggered message and a regular app open + // triggered message + @synchronized(self) { + if (self.suppressMessageDisplay) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400015", + @"Message display is being suppressed. No contextual message rendering."); + return; + } + + if (!self.messageDisplayComponent) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400026", + @"Message display component is not present yet. No display should happen."); + return; + } + + if (self.isMsgBeingDisplayed) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400008", + @"An in-app message display is in progress, do not check analytics event " + "based message for now."); + + return; + } + + // Pop up next analytics event based message to be displayed + FIRIAMMessageDefinition *nextAnalyticsBasedMessage = + [self.messageCache nextOnFirebaseAnalyticEventDisplayMsg:eventName]; + + if (nextAnalyticsBasedMessage) { + [self displayForMessage:nextAnalyticsBasedMessage]; + } + } +} + +- (FIRInAppMessagingBannerDisplay *) + bannerMessageWithMessageDefinition:(FIRIAMMessageDefinition *)definition + imageData:(FIRInAppMessagingImageData *)imageData { + NSString *title = definition.renderData.contentData.titleText; + NSString *body = definition.renderData.contentData.bodyText; + + FIRInAppMessagingBannerDisplay *bannerMessage = [[FIRInAppMessagingBannerDisplay alloc] + initWithMessageID:definition.renderData.messageID + renderAsTestMessage:definition.isTestMessage + titleText:title + bodyText:body + textColor:definition.renderData.renderingEffectSettings.textColor + backgroundColor:definition.renderData.renderingEffectSettings.displayBGColor + imageData:imageData]; + + return bannerMessage; +} + +- (FIRInAppMessagingImageOnlyDisplay *) + imageOnlyMessageWithMessageDefinition:(FIRIAMMessageDefinition *)definition + imageData:(FIRInAppMessagingImageData *)imageData { + FIRInAppMessagingImageOnlyDisplay *imageOnlyMessage = + [[FIRInAppMessagingImageOnlyDisplay alloc] initWithMessageID:definition.renderData.messageID + renderAsTestMessage:definition.isTestMessage + imageData:imageData]; + + return imageOnlyMessage; +} + +- (FIRInAppMessagingModalDisplay *) + modalViewMessageWithMessageDefinition:(FIRIAMMessageDefinition *)definition + imageData:(FIRInAppMessagingImageData *)imageData { + // For easier reference in this method. + FIRIAMMessageRenderData *renderData = definition.renderData; + + NSString *title = renderData.contentData.titleText; + NSString *body = renderData.contentData.bodyText; + + FIRInAppMessagingActionButton *actionButton = nil; + + if (definition.renderData.contentData.actionButtonText) { + actionButton = [[FIRInAppMessagingActionButton alloc] + initWithButtonText:renderData.contentData.actionButtonText + buttonTextColor:renderData.renderingEffectSettings.btnTextColor + backgroundColor:renderData.renderingEffectSettings.btnBGColor]; + } + + FIRInAppMessagingModalDisplay *modalViewMessage = [[FIRInAppMessagingModalDisplay alloc] + initWithMessageID:definition.renderData.messageID + renderAsTestMessage:definition.isTestMessage + titleText:title + bodyText:body + textColor:renderData.renderingEffectSettings.textColor + backgroundColor:renderData.renderingEffectSettings.displayBGColor + imageData:imageData + actionButton:actionButton]; + + return modalViewMessage; +} + +- (FIRInAppMessagingDisplayMessageBase *) + displayMessageWithMessageDefinition:(FIRIAMMessageDefinition *)definition + imageData:(FIRInAppMessagingImageData *)imageData { + switch (definition.renderData.renderingEffectSettings.viewMode) { + case FIRIAMRenderAsBannerView: + return [self bannerMessageWithMessageDefinition:definition imageData:imageData]; + case FIRIAMRenderAsModalView: + return [self modalViewMessageWithMessageDefinition:definition imageData:imageData]; + case FIRIAMRenderAsImageOnlyView: + return [self imageOnlyMessageWithMessageDefinition:definition imageData:imageData]; + default: + return nil; + } +} + +- (void)displayForMessage:(FIRIAMMessageDefinition *)message { + _currentMsgBeingDisplayed = message; + [message.renderData.contentData + loadImageDataWithBlock:^(NSData *_Nullable imageNSData, NSError *error) { + FIRInAppMessagingImageData *imageData = nil; + + if (error) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400019", + @"Error in loading image data for the message."); + + // short-circuit to display error handling + [self displayErrorEncountered:error]; + return; + } else if (imageNSData != nil) { + imageData = [[FIRInAppMessagingImageData alloc] + initWithImageURL:message.renderData.contentData.imageURL.absoluteString + imageData:imageNSData]; + } + + self.impressionRecorded = NO; + self.isMsgBeingDisplayed = YES; + + FIRInAppMessagingDisplayMessageBase *displayMessage = + [self displayMessageWithMessageDefinition:message imageData:imageData]; + [self.messageDisplayComponent displayMessage:displayMessage displayDelegate:self]; + }]; +} + +- (BOOL)enoughIntervalFromLastDisplay { + NSTimeInterval intervalFromLastDisplayInSeconds = + [self.timeFetcher currentTimestampInSeconds] - self.lastDisplayTime; + + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400005", + @"Interval time from last display is %lf seconds", intervalFromLastDisplayInSeconds); + + return intervalFromLastDisplayInSeconds >= self.setting.displayMinIntervalInMinutes * 60.0; +} + +- (void)checkAndDisplayNextAppForegroundMessage { + // synchronizing on self so that we won't potentially enter the render flow from two + // threads: example like showing analytics triggered message and a regular app open + // triggered message concurrently + @synchronized(self) { + if (!self.messageDisplayComponent) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400027", + @"Message display component is not present yet. No display should happen."); + return; + } + + if (self.suppressMessageDisplay) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400016", + @"Message display is being suppressed. No regular message rendering."); + return; + } + + if (self.isMsgBeingDisplayed) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400002", + @"An in-app message display is in progress, do not over-display on top of it."); + return; + } + + if ([self.messageCache hasTestMessage] || [self enoughIntervalFromLastDisplay]) { + // We can display test messages anytime or display regular messages when + // the display time interval has been reached + FIRIAMMessageDefinition *nextForegroundMessage = [self.messageCache nextOnAppOpenDisplayMsg]; + + if (nextForegroundMessage) { + [self displayForMessage:nextForegroundMessage]; + self.lastDisplayTime = [self.timeFetcher currentTimestampInSeconds]; + } else { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400001", + @"No appropriate in-app message detected for display."); + } + } else { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400003", + @"Minimal display interval of %lf seconds has not been reached yet.", + self.setting.displayMinIntervalInMinutes * 60.0); + } + } +} +@end diff --git a/Firebase/InAppMessaging/Flows/FIRIAMFetchFlow.h b/Firebase/InAppMessaging/Flows/FIRIAMFetchFlow.h new file mode 100644 index 00000000000..cd88606bcc7 --- /dev/null +++ b/Firebase/InAppMessaging/Flows/FIRIAMFetchFlow.h @@ -0,0 +1,59 @@ +/* + * 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 + +#import "FIRIAMActivityLogger.h" +#import "FIRIAMBookKeeper.h" +#import "FIRIAMMessageClientCache.h" +#import "FIRIAMSDKModeManager.h" +#import "FIRIAMTimeFetcher.h" + +@protocol FIRIAMAnalyticsEventLogger; + +NS_ASSUME_NONNULL_BEGIN +@interface FIRIAMFetchSetting : NSObject +@property(nonatomic) NSTimeInterval fetchMinIntervalInMinutes; +@end + +typedef void (^FIRIAMFetchMessageCompletionHandler)( + NSArray *_Nullable messages, + NSNumber *_Nullable nextFetchWaitTime, + NSInteger discardedMessageCount, + NSError *_Nullable error); + +@protocol FIRIAMMessageFetcher +- (void)fetchMessagesWithImpressionList:(NSArray *)impressonList + withCompletion:(FIRIAMFetchMessageCompletionHandler)completion; +@end + +// Parent class for supporting different fetching flows. Subclass is supposed to trigger +// checkAndFetch at appropriate moments based on its fetch strategy +@interface FIRIAMFetchFlow : NSObject +- (instancetype)initWithSetting:(FIRIAMFetchSetting *)setting + messageCache:(FIRIAMMessageClientCache *)cache + messageFetcher:(id)messageFetcher + timeFetcher:(id)timeFetcher + bookKeeper:(id)displayBookKeeper + activityLogger:(FIRIAMActivityLogger *)activityLogger + analyticsEventLogger:(id)analyticsEventLogger + FIRIAMSDKModeManager:(FIRIAMSDKModeManager *)sdkModeManager; + +// Triggers a potential fetch of in-app messaging from the source. It would check and respect the +// the fetchMinIntervalInMinutes defined in setting +- (void)checkAndFetch; +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Flows/FIRIAMFetchFlow.m b/Firebase/InAppMessaging/Flows/FIRIAMFetchFlow.m new file mode 100644 index 00000000000..133b79a85fc --- /dev/null +++ b/Firebase/InAppMessaging/Flows/FIRIAMFetchFlow.m @@ -0,0 +1,253 @@ +/* + * 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 + +#import "FIRCore+InAppMessaging.h" +#import "FIRIAMClearcutLogger.h" +#import "FIRIAMFetchFlow.h" + +@implementation FIRIAMFetchSetting +@end + +// the notification message to say that the fetch flow is done +NSString *const kFIRIAMFetchIsDoneNotification = @"FIRIAMFetchIsDoneNotification"; + +@interface FIRIAMFetchFlow () +@property(nonatomic) id timeFetcher; +@property(nonatomic) NSTimeInterval lastFetchTime; +@property(nonatomic, nonnull, readonly) FIRIAMFetchSetting *setting; +@property(nonatomic, nonnull, readonly) FIRIAMMessageClientCache *messageCache; +@property(nonatomic) id messageFetcher; +@property(nonatomic, nonnull, readonly) id fetchBookKeeper; +@property(nonatomic, nonnull, readonly) FIRIAMActivityLogger *activityLogger; +@property(nonatomic, nonnull, readonly) id analyticsEventLogger; + +@property(nonatomic, nonnull, readonly) FIRIAMSDKModeManager *sdkModeManager; +@end + +@implementation FIRIAMFetchFlow +- (instancetype)initWithSetting:(FIRIAMFetchSetting *)setting + messageCache:(FIRIAMMessageClientCache *)cache + messageFetcher:(id)messageFetcher + timeFetcher:(id)timeFetcher + bookKeeper:(id)fetchBookKeeper + activityLogger:(FIRIAMActivityLogger *)activityLogger + analyticsEventLogger:(id)analyticsEventLogger + FIRIAMSDKModeManager:(FIRIAMSDKModeManager *)sdkModeManager { + if (self = [super init]) { + _timeFetcher = timeFetcher; + _lastFetchTime = [fetchBookKeeper lastFetchTime]; + _setting = setting; + _messageCache = cache; + _messageFetcher = messageFetcher; + _fetchBookKeeper = fetchBookKeeper; + _activityLogger = activityLogger; + _analyticsEventLogger = analyticsEventLogger; + _sdkModeManager = sdkModeManager; + } + return self; +} + +- (FIRIAMAnalyticsLogEventType)fetchErrorToLogEventType:(NSError *)error { + if ([error.domain isEqual:NSURLErrorDomain]) { + if (error.code == NSURLErrorNotConnectedToInternet) { + return FIRIAMAnalyticsEventFetchAPINetworkError; + } else { + // error.code could be a non 2xx status code + if (error.code > 0) { + if (error.code >= 400 && error.code < 500) { + return FIRIAMAnalyticsEventFetchAPIClientError; + } else { + if (error.code >= 500 && error.code < 600) { + return FIRIAMAnalyticsEventFetchAPIServerError; + } + } + } + } + } + + return FIRIAMAnalyticsLogEventUnknown; +} + +- (void)sendFetchIsDoneNotification { + [[NSNotificationCenter defaultCenter] postNotificationName:kFIRIAMFetchIsDoneNotification + object:self]; +} + +- (void)handleSuccessullyFetchedMessages:(NSArray *)messagesInResponse + withFetchWaitTime:(NSNumber *_Nullable)fetchWaitTime + requestImpressions:(NSArray *)requestImpressions { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM700004", @"%lu messages were fetched successfully.", + (unsigned long)messagesInResponse.count); + + for (FIRIAMMessageDefinition *next in messagesInResponse) { + if (next.isTestMessage && self.sdkModeManager.currentMode != FIRIAMSDKModeTesting) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM700006", + @"Seeing test message in fetch response. Turn " + "the current instance into a testing instance."); + [self.sdkModeManager becomeTestingInstance]; + } + } + + NSArray *responseMessageIDs = + [messagesInResponse valueForKeyPath:@"renderData.messageID"]; + NSArray *impressionMessageIDs = [requestImpressions valueForKey:@"messageID"]; + + // We are going to clear impression records for those IDs that are in both impressionMessageIDs + // and responseMessageIDs. This is to avoid incorrectly clearing impressions records that come + // in between the sending the request and receiving the response for the fetch operation. + // So we are computing intersection between responseMessageIDs and impressionMessageIDs and use + // that for impression log clearing. + NSMutableSet *idIntersection = [NSMutableSet setWithArray:responseMessageIDs]; + [idIntersection intersectSet:[NSSet setWithArray:impressionMessageIDs]]; + + [self.fetchBookKeeper clearImpressionsWithMessageList:[idIntersection allObjects]]; + [self.messageCache setMessageData:messagesInResponse]; + + [self.sdkModeManager registerOneMoreFetch]; + [self.fetchBookKeeper recordNewFetchWithFetchCount:messagesInResponse.count + withTimestampInSeconds:[self.timeFetcher currentTimestampInSeconds] + nextFetchWaitTime:fetchWaitTime]; +} + +- (void)checkAndFetch { + NSTimeInterval intervalFromLastFetchInSeconds = + [self.timeFetcher currentTimestampInSeconds] - self.fetchBookKeeper.lastFetchTime; + + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM700005", + @"Interval from last time fetch is %lf seconds", intervalFromLastFetchInSeconds); + + BOOL fetchIsAllowedNow = NO; + + if (intervalFromLastFetchInSeconds >= self.fetchBookKeeper.nextFetchWaitTime) { + // it's enough wait time interval from last fetch. + fetchIsAllowedNow = YES; + } else { + FIRIAMSDKMode sdkMode = [self.sdkModeManager currentMode]; + if (sdkMode == FIRIAMSDKModeNewlyInstalled || sdkMode == FIRIAMSDKModeTesting) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM700007", + @"OK to fetch due to current SDK mode being %@", + FIRIAMDescriptonStringForSDKMode(sdkMode)); + fetchIsAllowedNow = YES; + } else { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM700008", + @"Interval from last time fetch is %lf seconds, smaller than fetch wait time %lf", + intervalFromLastFetchInSeconds, self.fetchBookKeeper.nextFetchWaitTime); + } + } + + if (fetchIsAllowedNow) { + // we are allowed to fetch in-app message from time interval wise + + FIRIAMActivityRecord *record = + [[FIRIAMActivityRecord alloc] initWithActivityType:FIRIAMActivityTypeCheckForFetch + isSuccessful:YES + withDetail:@"OK to do a fetch" + timestamp:nil]; + [self.activityLogger addLogRecord:record]; + + NSArray *impressions = [self.fetchBookKeeper getImpressions]; + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM700001", @"Go ahead to fetch messages"); + + NSTimeInterval fetchStartTime = [[NSDate date] timeIntervalSince1970]; + + [self.messageFetcher + fetchMessagesWithImpressionList:impressions + withCompletion:^(NSArray *_Nullable messages, + NSNumber *_Nullable nextFetchWaitTime, + NSInteger discardedMessageCount, + NSError *_Nullable error) { + if (error) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM700002", + @"Error happened during message fetching %@", error); + + FIRIAMAnalyticsLogEventType eventType = + [self fetchErrorToLogEventType:error]; + + [self.analyticsEventLogger logAnalyticsEventForType:eventType + forCampaignID:@"all" + withCampaignName:@"all" + eventTimeInMs:nil + completion:^(BOOL success){ + // nothing to do + }]; + + FIRIAMActivityRecord *record = [[FIRIAMActivityRecord alloc] + initWithActivityType:FIRIAMActivityTypeFetchMessage + isSuccessful:NO + withDetail:error.description + timestamp:nil]; + [self.activityLogger addLogRecord:record]; + } else { + double fetchOperationLatencyInMills = + ([[NSDate date] timeIntervalSince1970] - fetchStartTime) * 1000; + NSString *impressionListString = + [impressions componentsJoinedByString:@","]; + NSString *activityLogDetail = @""; + + if (discardedMessageCount > 0) { + activityLogDetail = [NSString + stringWithFormat: + @"%lu messages fetched with impression list as [%@]" + " and %lu messages are discarded due to data being " + "invalid. It took" + " %lf milliseconds", + (unsigned long)messages.count, impressionListString, + (unsigned long)discardedMessageCount, + fetchOperationLatencyInMills]; + } else { + activityLogDetail = [NSString + stringWithFormat: + @"%lu messages fetched with impression list as [%@]. It took" + " %lf milliseconds", + (unsigned long)messages.count, impressionListString, + fetchOperationLatencyInMills]; + } + + FIRIAMActivityRecord *record = [[FIRIAMActivityRecord alloc] + initWithActivityType:FIRIAMActivityTypeFetchMessage + isSuccessful:YES + withDetail:activityLogDetail + timestamp:nil]; + [self.activityLogger addLogRecord:record]; + + // Now handle the fetched messages. + [self handleSuccessullyFetchedMessages:messages + withFetchWaitTime:nextFetchWaitTime + requestImpressions:impressions]; + } + // Send this regardless whether fetch is successful or not. + [self sendFetchIsDoneNotification]; + }]; + + } else { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM700003", + @"Only %lf seconds from last fetch time. No action.", + intervalFromLastFetchInSeconds); + // for no fetch case, we still send out the notification so that and display flow can continue + // from here. + [self sendFetchIsDoneNotification]; + FIRIAMActivityRecord *record = + [[FIRIAMActivityRecord alloc] initWithActivityType:FIRIAMActivityTypeCheckForFetch + isSuccessful:NO + withDetail:@"Abort due to check time interval " + "not reached yet" + timestamp:nil]; + [self.activityLogger addLogRecord:record]; + } +} +@end diff --git a/Firebase/InAppMessaging/Flows/FIRIAMFetchOnAppForegroundFlow.h b/Firebase/InAppMessaging/Flows/FIRIAMFetchOnAppForegroundFlow.h new file mode 100644 index 00000000000..ddbdb684f50 --- /dev/null +++ b/Firebase/InAppMessaging/Flows/FIRIAMFetchOnAppForegroundFlow.h @@ -0,0 +1,22 @@ +/* + * 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 "FIRIAMFetchFlow.h" + +// an implementation of FIRIAMDisplayExecutor by triggering the display when app is foregrounded +@interface FIRIAMFetchOnAppForegroundFlow : FIRIAMFetchFlow +- (void)start; +- (void)stop; +@end diff --git a/Firebase/InAppMessaging/Flows/FIRIAMFetchOnAppForegroundFlow.m b/Firebase/InAppMessaging/Flows/FIRIAMFetchOnAppForegroundFlow.m new file mode 100644 index 00000000000..3d7831eb442 --- /dev/null +++ b/Firebase/InAppMessaging/Flows/FIRIAMFetchOnAppForegroundFlow.m @@ -0,0 +1,51 @@ +/* + * 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 + +#import "FIRCore+InAppMessaging.h" +#import "FIRIAMFetchOnAppForegroundFlow.h" +@implementation FIRIAMFetchOnAppForegroundFlow +- (void)start { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM600002", + @"Start observing app foreground notifications for message fetching."); + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(appWillEnterForeground:) + name:UIApplicationWillEnterForegroundNotification + object:nil]; +} + +- (void)appWillEnterForeground:(UIApplication *)application { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM600001", + @"App foregrounded, wake up to see if we can fetch in-app messaging."); + // for fetch operation, dispatch it to non main UI thread to avoid blocking. It's ok to dispatch + // to a concurrent global queue instead of serial queue since app open event won't happen at + // fast speed to cause race conditions + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul), ^{ + [self checkAndFetch]; + }); +} + +- (void)stop { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM600003", + @"Stop observing app foreground notifications."); + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} +@end diff --git a/Firebase/InAppMessaging/Flows/FIRIAMMessageClientCache.h b/Firebase/InAppMessaging/Flows/FIRIAMMessageClientCache.h new file mode 100644 index 00000000000..53c1ed55d2b --- /dev/null +++ b/Firebase/InAppMessaging/Flows/FIRIAMMessageClientCache.h @@ -0,0 +1,91 @@ +/* + * 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 +#import "FIRIAMBookKeeper.h" +#import "FIRIAMFetchResponseParser.h" +#import "FIRIAMMessageDefinition.h" + +NS_ASSUME_NONNULL_BEGIN + +@class FIRIAMServerMsgFetchStorage; +@class FIRIAMDisplayCheckOnAnalyticEventsFlow; + +@interface FIRIAMContextualTrigger +@property(nonatomic, copy, readonly) NSString *eventName; +@end + +@interface FIRIAMContextualTriggerListener ++ (void)listenForTriggers:(NSArray *)triggers + withCallback:(void (^)(FIRIAMContextualTrigger *matchedTrigger))callback; +@end + +@protocol FIRIAMCacheDataObserver +- (void)dataChanged; +@end + +// This class serves as an in-memory cache of the messages that would be searched for finding next +// message to be rendered. Its content can be loaded from client persistent storage upon SDK +// initialization and then updated whenever a new fetch is made to server to receive the last +// list. In the case a message has been rendered, it's removed from the cache so that it's not +// considered next time for the message search. +// +// This class is also responsible for setting up and tearing down appropriate analytics event +// listening flow based on whether the current active event list contains any analytics event +// trigger based messages. +// +// This class exists so that we can do message match more efficiently (in-memory search vs search +// in local persistent storage) by using appropriate in-memory data structure. +@interface FIRIAMMessageClientCache : NSObject + +// used to inform the analytics event display check flow about whether it should start/stop +// analytics event listening based on the latest message definitions +// make it weak to avoid retaining cycle +@property(nonatomic, weak, nullable) + FIRIAMDisplayCheckOnAnalyticEventsFlow *analycisEventDislayCheckFlow; + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithBookkeeper:(id)bookKeeper + usingResponseParser:(FIRIAMFetchResponseParser *)responseParser; + +// set an observer for watching for data changes in the cache +- (void)setDataObserver:(id)observer; + +// Returns YES if there are any test messages in the cache. +- (BOOL)hasTestMessage; + +// read all the messages as a copy stored in cache +- (NSArray *)allRegularMessages; + +// clients that are to display messages should use nextOnAppOpenDisplayMsg or +// nextOnFirebaseAnalyticEventDisplayMsg to fetch the next eligible message and use +// removeMessageWithId to remove it from cache once the message has been correctly rendered + +// Fetch next eligible messages that are appropriate for display at app open time +- (nullable FIRIAMMessageDefinition *)nextOnAppOpenDisplayMsg; +// Fetch next eligible message that matches the event triggering condition +- (nullable FIRIAMMessageDefinition *)nextOnFirebaseAnalyticEventDisplayMsg:(NSString *)eventName; + +// Call this after a message has been rendered to remove it from the cache. +- (void)removeMessageWithId:(NSString *)messgeId; + +// reset messages data +- (void)setMessageData:(NSArray *)messages; +// load messages from persistent storage +- (void)loadMessageDataFromServerFetchStorage:(FIRIAMServerMsgFetchStorage *)fetchStorage + withCompletion:(void (^)(BOOL success))completion; +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Flows/FIRIAMMessageClientCache.m b/Firebase/InAppMessaging/Flows/FIRIAMMessageClientCache.m new file mode 100644 index 00000000000..c6c4933a41f --- /dev/null +++ b/Firebase/InAppMessaging/Flows/FIRIAMMessageClientCache.m @@ -0,0 +1,223 @@ +/* + * 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 + +#import "FIRCore+InAppMessaging.h" +#import "FIRIAMDisplayCheckOnAnalyticEventsFlow.h" +#import "FIRIAMDisplayTriggerDefinition.h" +#import "FIRIAMFetchResponseParser.h" +#import "FIRIAMMessageClientCache.h" +#import "FIRIAMServerMsgFetchStorage.h" + +@interface FIRIAMMessageClientCache () + +// messages not for client-side testing +@property(nonatomic) NSMutableArray *regularMessages; +// messages for client-side testing +@property(nonatomic) NSMutableArray *testMessages; +@property(nonatomic, weak) id observer; +@property(nonatomic) NSMutableSet *firebaseAnalyticEventsToWatch; +@property(nonatomic) id bookKeeper; +@property(readonly, nonatomic) FIRIAMFetchResponseParser *responseParser; + +@end + +// Methods doing read and write operations on messages field is synchronized to avoid +// race conditions like change the array while iterating through it +@implementation FIRIAMMessageClientCache +- (instancetype)initWithBookkeeper:(id)bookKeeper + usingResponseParser:(FIRIAMFetchResponseParser *)responseParser { + if (self = [super init]) { + _bookKeeper = bookKeeper; + _responseParser = responseParser; + } + return self; +} + +- (void)setDataObserver:(id)observer { + self.observer = observer; +} + +// reset messages data +- (void)setMessageData:(NSArray *)messages { + @synchronized(self) { + NSSet *impressionSet = + [NSSet setWithArray:[self.bookKeeper getMessageIDsFromImpressions]]; + + NSMutableArray *regularMessages = [[NSMutableArray alloc] init]; + self.testMessages = [[NSMutableArray alloc] init]; + + // split between test vs non-test messages + for (FIRIAMMessageDefinition *next in messages) { + if (next.isTestMessage) { + [self.testMessages addObject:next]; + } else { + [regularMessages addObject:next]; + } + } + + // while resetting the whole message set, we do prefiltering based on the impressions + // data to get rid of messages we don't care so that the future searches are more efficient + NSPredicate *notImpressedPredicate = + [NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) { + FIRIAMMessageDefinition *message = (FIRIAMMessageDefinition *)evaluatedObject; + return ![impressionSet containsObject:message.renderData.messageID]; + }]; + + self.regularMessages = + [[regularMessages filteredArrayUsingPredicate:notImpressedPredicate] mutableCopy]; + [self setupAnalyticsEventListening]; + } + + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM160001", + @"There are %lu test messages and %lu regular messages and " + "%lu Firebase Analytics events to watch after " + "resetting the message cache", + (unsigned long)self.testMessages.count, (unsigned long)self.regularMessages.count, + (unsigned long)self.firebaseAnalyticEventsToWatch.count); + [self.observer dataChanged]; +} + +// triggered after self.messages are updated so that we can correctly enable/disable listening +// on analytics event based on current fiam message set +- (void)setupAnalyticsEventListening { + self.firebaseAnalyticEventsToWatch = [[NSMutableSet alloc] init]; + for (FIRIAMMessageDefinition *nextMessage in self.regularMessages) { + // if it's event based triggering, add it to the watch set + for (FIRIAMDisplayTriggerDefinition *nextTrigger in nextMessage.renderTriggers) { + if (nextTrigger.triggerType == FIRIAMRenderTriggerOnFirebaseAnalyticsEvent) { + [self.firebaseAnalyticEventsToWatch addObject:nextTrigger.firebaseEventName]; + } + } + } + + if (self.analycisEventDislayCheckFlow) { + if ([self.firebaseAnalyticEventsToWatch count] > 0) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM160010", + @"There are analytics event trigger based messages, enable listening"); + [self.analycisEventDislayCheckFlow start]; + } else { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM160011", + @"No analytics event trigger based messages, disable listening"); + [self.analycisEventDislayCheckFlow stop]; + } + } +} + +- (NSArray *)allRegularMessages { + return [self.regularMessages copy]; +} + +- (BOOL)hasTestMessage { + return self.testMessages.count > 0; +} + +- (nullable FIRIAMMessageDefinition *)nextOnAppOpenDisplayMsg { + // search from the start to end in the list (which implies the display priority) for the + // first match (some messages in the cache may not be eligible for the current display + // message fetch + NSSet *impressionSet = + [NSSet setWithArray:[self.bookKeeper getMessageIDsFromImpressions]]; + + @synchronized(self) { + // always first check test message which always have higher prirority + if (self.testMessages.count > 0) { + FIRIAMMessageDefinition *testMessage = self.testMessages[0]; + // always remove test message right away when being fetched for display + [self.testMessages removeObjectAtIndex:0]; + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM160007", + @"Returning a test message for app foreground display"); + return testMessage; + } + + for (FIRIAMMessageDefinition *next in self.regularMessages) { + // message being active and message not impressed yet + if ([next messageHasStarted] && ![next messageHasExpired] && + ![impressionSet containsObject:next.renderData.messageID] && + [next messageRenderedOnAppForegroundEvent]) { + return next; + } + } + } + return nil; +} + +- (nullable FIRIAMMessageDefinition *)nextOnFirebaseAnalyticEventDisplayMsg:(NSString *)eventName { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM160005", + @"Inside nextOnFirebaseAnalyticEventDisplay for checking contextual trigger match"); + if (![self.firebaseAnalyticEventsToWatch containsObject:eventName]) { + return nil; + } + + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM160006", + @"There could be a potential message match for analytics event %@", eventName); + NSSet *impressionSet = + [NSSet setWithArray:[self.bookKeeper getMessageIDsFromImpressions]]; + @synchronized(self) { + for (FIRIAMMessageDefinition *next in self.regularMessages) { + // message being active and message not impressed yet and the contextual trigger condition + // match + if ([next messageHasStarted] && ![next messageHasExpired] && + ![impressionSet containsObject:next.renderData.messageID] && + [next messageRenderedOnAnalyticsEvent:eventName]) { + return next; + } + } + } + return nil; +} + +- (void)removeMessageWithId:(NSString *)messageID { + FIRIAMMessageDefinition *msgToRemove = nil; + @synchronized(self) { + for (FIRIAMMessageDefinition *next in self.regularMessages) { + if ([next.renderData.messageID isEqualToString:messageID]) { + msgToRemove = next; + break; + } + } + + if (msgToRemove) { + [self.regularMessages removeObject:msgToRemove]; + [self setupAnalyticsEventListening]; + } + } + + // triggers the observer outside synchronization block + if (msgToRemove) { + [self.observer dataChanged]; + } +} + +- (void)loadMessageDataFromServerFetchStorage:(FIRIAMServerMsgFetchStorage *)fetchStorage + withCompletion:(void (^)(BOOL success))completion { + [fetchStorage readResponseDictionary:^(NSDictionary *_Nonnull response, BOOL success) { + if (success) { + NSInteger discardCount; + NSNumber *fetchWaitTime; + NSArray *messagesFromStorage = + [self.responseParser parseAPIResponseDictionary:response + discardedMsgCount:&discardCount + fetchWaitTimeInSeconds:&fetchWaitTime]; + [self setMessageData:messagesFromStorage]; + completion(YES); + } else { + completion(NO); + } + }]; +} +@end diff --git a/Firebase/InAppMessaging/Flows/FIRIAMMsgFetcherUsingRestful.h b/Firebase/InAppMessaging/Flows/FIRIAMMsgFetcherUsingRestful.h new file mode 100644 index 00000000000..4e2777fd2eb --- /dev/null +++ b/Firebase/InAppMessaging/Flows/FIRIAMMsgFetcherUsingRestful.h @@ -0,0 +1,55 @@ +/* + * 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 +#import "FIRIAMClientInfoFetcher.h" +#import "FIRIAMFetchFlow.h" +#import "FIRIAMFetchResponseParser.h" +#import "FIRIAMSDKSettings.h" +#import "FIRIAMServerMsgFetchStorage.h" + +NS_ASSUME_NONNULL_BEGIN + +// implementation of FIRIAMMessageFetcher by making Restful API requests to firebase +// in-app messaging services +@interface FIRIAMMsgFetcherUsingRestful : NSObject +/** + * Create an instance which uses NSURLSession to make the restful api call. + * + * @param serverHost API server host. + * @param fbProjectNumber project number used for the API call. It's the GCM_SENDER_ID + * field in GoogleService-Info.plist. + * @param fbAppId It's the GOOGLE_APP_ID field in GoogleService-Info.plist. + * @param apiKey API key. + * @param fetchStorage used to persist the fetched response. + * @param clientInfoFetcher used to fetch iid info for the current app. + * @param URLSession can be nil in which case the class would create NSURLSession + * internally to perform the network request. Having it here so that + * it's easier for doing mocking with unit testing. + */ +- (instancetype)initWithHost:(NSString *)serverHost + HTTPProtocol:(NSString *)HTTPProtocol + project:(NSString *)fbProjectNumber + firebaseApp:(NSString *)fbAppId + APIKey:(NSString *)apiKey + fetchStorage:(FIRIAMServerMsgFetchStorage *)fetchStorage + instanceIDFetcher:(FIRIAMClientInfoFetcher *)clientInfoFetcher + usingURLSession:(nullable NSURLSession *)URLSession + responseParser:(FIRIAMFetchResponseParser *)responseParser; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Flows/FIRIAMMsgFetcherUsingRestful.m b/Firebase/InAppMessaging/Flows/FIRIAMMsgFetcherUsingRestful.m new file mode 100644 index 00000000000..7256906e3e2 --- /dev/null +++ b/Firebase/InAppMessaging/Flows/FIRIAMMsgFetcherUsingRestful.m @@ -0,0 +1,272 @@ +/* + * 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 +#import + +#import "FIRCore+InAppMessaging.h" +#import "FIRIAMFetchFlow.h" +#import "FIRIAMFetchResponseParser.h" +#import "FIRIAMMessageContentDataWithImageURL.h" +#import "FIRIAMMessageDefinition.h" +#import "FIRIAMMsgFetcherUsingRestful.h" +#import "FIRIAMSDKSettings.h" + +static NSInteger const SuccessHTTPStatusCode = 200; + +@interface FIRIAMMsgFetcherUsingRestful () +@property(readonly) NSURLSession *URLSession; +@property(readonly, copy, nonatomic) NSString *serverHostName; +@property(readonly, copy, nonatomic) NSString *appBundleID; +@property(readonly, copy, nonatomic) NSString *httpProtocol; +@property(readonly, copy, nonatomic) NSString *fbProjectNumber; +@property(readonly, copy, nonatomic) NSString *apiKey; +@property(readonly, copy, nonatomic) NSString *firebaseAppId; +@property(readonly, nonatomic) FIRIAMServerMsgFetchStorage *fetchStorage; +@property(readonly, nonatomic) FIRIAMClientInfoFetcher *clientInfoFetcher; +@property(readonly, nonatomic) FIRIAMFetchResponseParser *responseParser; +@end + +@implementation FIRIAMMsgFetcherUsingRestful +- (instancetype)initWithHost:(NSString *)serverHost + HTTPProtocol:(NSString *)HTTPProtocol + project:(NSString *)fbProjectNumber + firebaseApp:(NSString *)fbAppId + APIKey:(NSString *)apiKey + fetchStorage:(FIRIAMServerMsgFetchStorage *)fetchStorage + instanceIDFetcher:(FIRIAMClientInfoFetcher *)clientInfoFetcher + usingURLSession:(nullable NSURLSession *)URLSession + responseParser:(FIRIAMFetchResponseParser *)responseParser { + if (self = [super init]) { + _URLSession = URLSession ? URLSession : [NSURLSession sharedSession]; + _serverHostName = [serverHost copy]; + _fbProjectNumber = [fbProjectNumber copy]; + _firebaseAppId = [fbAppId copy]; + _httpProtocol = [HTTPProtocol copy]; + _apiKey = [apiKey copy]; + _clientInfoFetcher = clientInfoFetcher; + _fetchStorage = fetchStorage; + _appBundleID = [NSBundle mainBundle].bundleIdentifier; + _responseParser = responseParser; + } + return self; +} + +- (void)updatePostFetchData:(NSMutableDictionary *)postData + withImpressionList:(NSArray *)impressionList + instanceIDString:(nonnull NSString *)IIDValue + IIDToken:(nonnull NSString *)IIDToken { + NSMutableArray *impressionListForPost = [[NSMutableArray alloc] init]; + for (FIRIAMImpressionRecord *nextImpressionRecord in impressionList) { + NSDictionary *nextImpression = @{ + @"campaign_id" : nextImpressionRecord.messageID, + @"impression_timestamp_millis" : @(nextImpressionRecord.impressionTimeInSeconds * 1000) + }; + [impressionListForPost addObject:nextImpression]; + } + [postData setObject:impressionListForPost forKey:@"already_seen_campaigns"]; + + if (IIDValue) { + NSDictionary *clientAppInfo = @{ + @"gmp_app_id" : self.firebaseAppId, + @"app_instance_id" : IIDValue, + @"app_instance_id_token" : IIDToken + }; + [postData setObject:clientAppInfo forKey:@"requesting_client_app"]; + } + + NSMutableArray *clientSignals = [@{} mutableCopy]; + + // set client signal fields only when they are present + if ([self.clientInfoFetcher getAppVersion]) { + [clientSignals setValue:[self.clientInfoFetcher getAppVersion] forKey:@"app_version"]; + } + + if ([self.clientInfoFetcher getOSVersion]) { + [clientSignals setValue:[self.clientInfoFetcher getOSVersion] forKey:@"platform_version"]; + } + + if ([self.clientInfoFetcher getDeviceLanguageCode]) { + [clientSignals setValue:[self.clientInfoFetcher getDeviceLanguageCode] forKey:@"language_code"]; + } + + if ([self.clientInfoFetcher getTimezone]) { + [clientSignals setValue:[self.clientInfoFetcher getTimezone] forKey:@"time_zone"]; + } + + [postData setObject:clientSignals forKey:@"client_signals"]; +} + +- (void)fetchMessagesWithImpressionList:(NSArray *)impressonList + withIIDvalue:(NSString *)iidValue + IIDToken:(NSString *)iidToken + completion:(FIRIAMFetchMessageCompletionHandler)completion { + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init]; + [request setHTTPMethod:@"POST"]; + + if (_appBundleID.length) { + // Handle the case in which the API key is being restricted to specific iOS app bundle, + // which can be set on Google Cloud console side for API key credentials. + [request addValue:_appBundleID forHTTPHeaderField:@"X-Ios-Bundle-Identifier"]; + } + + [request addValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; + [request addValue:@"application/json" forHTTPHeaderField:@"Accept"]; + + NSMutableDictionary *postFetchDict = [[NSMutableDictionary alloc] init]; + [self updatePostFetchData:postFetchDict + withImpressionList:impressonList + instanceIDString:iidValue + IIDToken:iidToken]; + + NSData *postFetchData = [NSJSONSerialization dataWithJSONObject:postFetchDict + options:0 + error:nil]; + + NSString *requestURLString = [NSString + stringWithFormat:@"%@://%@/v1/sdkServing/projects/%@/eligibleCampaigns:fetch?key=%@", + self.httpProtocol, self.serverHostName, self.fbProjectNumber, self.apiKey]; + [request setURL:[NSURL URLWithString:requestURLString]]; + [request setHTTPBody:postFetchData]; + + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM130001", + @"Making a restful API request for pulling messages with fetch POST body as %@ " + "and request headers as %@", + postFetchDict, request.allHTTPHeaderFields); + + NSURLSessionDataTask *postDataTask = [self.URLSession + dataTaskWithRequest:request + completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + if (error) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM130002", + @"Internal error: encountered error in pulling messages from server" + ":%@", + error); + completion(nil, nil, 0, error); + } else { + if ([response isKindOfClass:[NSHTTPURLResponse class]]) { + NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; + if (httpResponse.statusCode == SuccessHTTPStatusCode) { + // got response data successfully + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM130007", + @"Fetch API response headers are %@", [httpResponse allHeaderFields]); + + NSError *errorJson = nil; + NSDictionary *responseDict = [NSJSONSerialization JSONObjectWithData:data + options:kNilOptions + error:&errorJson]; + if (errorJson) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM130003", + @"Failed to parse the response body as JSON string %@", errorJson); + completion(nil, nil, 0, errorJson); + } else { + NSInteger discardCount; + NSNumber *nextFetchWaitTimeFromResponse; + NSArray *messages = [self.responseParser + parseAPIResponseDictionary:responseDict + discardedMsgCount:&discardCount + fetchWaitTimeInSeconds:&nextFetchWaitTimeFromResponse]; + + if (messages) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM130012", + @"API request for fetching messages and parsing the response was " + "successful."); + [self.fetchStorage + saveResponseDictionary:responseDict + withCompletion:^(BOOL success) { + if (!success) + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM130010", + @"Failed to persist server fetch response"); + }]; + // always report success regardless of whether we are able to persist into + // storage. they should get fixed in the next fetch cycle if it happens. + completion(messages, nextFetchWaitTimeFromResponse, discardCount, nil); + } else { + NSString *errorDesc = + @"Failed to recognize the fiam messages in the server response"; + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM130011", @"%@", errorDesc); + NSError *error = + [NSError errorWithDomain:kFirebaseInAppMessagingErrorDomain + code:0 + userInfo:@{NSLocalizedDescriptionKey : errorDesc}]; + completion(nil, nil, 0, error); + } + } + } else { + NSString *responseBody = [[NSString alloc] initWithData:data + encoding:NSUTF8StringEncoding]; + + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM130004", + @"Failed restful api request to fetch in-app messages: seeing http " + @"status code as %ld with body as %@", + (long)httpResponse.statusCode, responseBody); + + NSError *error = [NSError errorWithDomain:NSURLErrorDomain + code:httpResponse.statusCode + userInfo:nil]; + completion(nil, nil, 0, error); + } + } else { + NSString *errorDesc = @"Got a non http response type from fetch endpoint"; + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM130005", @"%@", errorDesc); + + NSError *error = [NSError errorWithDomain:kFirebaseInAppMessagingErrorDomain + code:0 + userInfo:@{NSLocalizedDescriptionKey : errorDesc}]; + completion(nil, nil, 0, error); + } + } + }]; + + if (postDataTask == nil) { + NSString *errorDesc = + @"Internal error: NSURLSessionDataTask failed to be created due to possibly " + "incorrect parameters"; + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM130006", @"%@", errorDesc); + NSError *error = [NSError errorWithDomain:kFirebaseInAppMessagingErrorDomain + code:0 + userInfo:@{NSLocalizedDescriptionKey : errorDesc}]; + completion(nil, nil, 0, error); + } else { + [postDataTask resume]; + } +} + +#pragma mark - protocol FIRIAMMessageFetcher +- (void)fetchMessagesWithImpressionList:(NSArray *)impressonList + withCompletion:(FIRIAMFetchMessageCompletionHandler)completion { + // First step is to fetch the instance id value and token on the fly. We are not caching the data + // since the fetch operation frequency is low enough that we are not concerned about its impact + // on server load and this guarantees that we always have an up-to-date iid values and tokens. + [self.clientInfoFetcher + fetchFirebaseIIDDataWithProjectNumber:self.fbProjectNumber + withCompletion:^(NSString *_Nullable iid, NSString *_Nullable token, + NSError *_Nullable error) { + if (error) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM130008", + @"Not able to get iid value and/or token for " + @"talking to server: %@", + error.localizedDescription); + completion(nil, nil, 0, error); + } else { + [self fetchMessagesWithImpressionList:impressonList + withIIDvalue:iid + IIDToken:token + completion:completion]; + } + }]; +} +@end diff --git a/Firebase/InAppMessaging/Flows/FIRIAMServerMsgFetchStorage.h b/Firebase/InAppMessaging/Flows/FIRIAMServerMsgFetchStorage.h new file mode 100644 index 00000000000..61cfac3c710 --- /dev/null +++ b/Firebase/InAppMessaging/Flows/FIRIAMServerMsgFetchStorage.h @@ -0,0 +1,30 @@ +/* + * 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 + +NS_ASSUME_NONNULL_BEGIN + +// A class that will persist response data fetched from server side into a local file on +// client side. This file can be used as the cache for messages after the app has been +// killed and before it's up for next server fetch. +@interface FIRIAMServerMsgFetchStorage : NSObject +- (void)saveResponseDictionary:(NSDictionary *)response + withCompletion:(void (^)(BOOL success))completion; +- (void)readResponseDictionary:(void (^)(NSDictionary *response, BOOL success))completion; + +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Flows/FIRIAMServerMsgFetchStorage.m b/Firebase/InAppMessaging/Flows/FIRIAMServerMsgFetchStorage.m new file mode 100644 index 00000000000..3a08b61a99a --- /dev/null +++ b/Firebase/InAppMessaging/Flows/FIRIAMServerMsgFetchStorage.m @@ -0,0 +1,64 @@ +/* + * 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 + +#import "FIRCore+InAppMessaging.h" +#import "FIRIAMServerMsgFetchStorage.h" +@implementation FIRIAMServerMsgFetchStorage +- (NSString *)determineCacheFilePath { + NSString *cachePath = + NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES)[0]; + NSString *filePath = [NSString stringWithFormat:@"%@/firebase-iam-messages-cache", cachePath]; + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM150004", + @"Persistent file path for fetch response data is %@", filePath); + return filePath; +} + +- (void)saveResponseDictionary:(NSDictionary *)response + withCompletion:(void (^)(BOOL success))completion { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul), ^{ + if ([response writeToFile:[self determineCacheFilePath] atomically:YES]) { + completion(YES); + } else { + completion(NO); + } + }); +} + +- (void)readResponseDictionary:(void (^)(NSDictionary *response, BOOL success))completion { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul), ^{ + NSString *storageFilePath = [self determineCacheFilePath]; + if ([[NSFileManager defaultManager] fileExistsAtPath:storageFilePath]) { + NSDictionary *dictFromFile = + [[NSMutableDictionary dictionaryWithContentsOfFile:[self determineCacheFilePath]] copy]; + if (dictFromFile) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM150001", + @"Loaded response from fetch storage successfully."); + completion(dictFromFile, YES); + } else { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM150002", + @"Not able to read response from fetch storage."); + completion(dictFromFile, NO); + } + } else { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM150003", + @"Local fetch storage file not existent yet: first time launch of the app."); + completion(nil, YES); + } + }); +} +@end diff --git a/Firebase/InAppMessaging/Public/FIRInAppMessaging.h b/Firebase/InAppMessaging/Public/FIRInAppMessaging.h new file mode 100644 index 00000000000..7764b217777 --- /dev/null +++ b/Firebase/InAppMessaging/Public/FIRInAppMessaging.h @@ -0,0 +1,80 @@ +/* + * 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 + +@class FIRApp; + +#import "FIRInAppMessagingRendering.h" + +NS_ASSUME_NONNULL_BEGIN +/** + * The root object for in-app messaging iOS SDK. + * + * Note: Firebase InApp Messaging depends on using a Firebase Instance ID & token pair to be able + * to retrieve FIAM messages defined for the current app instance. By default, Firebase in-app + * messaging SDK would obtain the ID & token pair on app/SDK startup. As a result of using + * ID & token pair, some device client data (linked to the instance ID) would be collected and sent + * over to Firebase backend periodically. + * + * The app can tune the default data collection behavior via certain controls. They are listed in + * descending order below. If a higher-priority setting exists, lower level settings are ignored. + * + * 1. Dynamically turn on/off data collection behavior by setting the + * `automaticDataCollectionEnabled` property on the `FIRInAppMessaging` instance to true/false + * Swift or YES/NO (objective-c). + * 2. Set `FirebaseInAppMessagingAutomaticDataCollectionEnabled` to false in the app's plist file. + * 3. Global Firebase data collection setting. + **/ +NS_SWIFT_NAME(InAppMessaging) +@interface FIRInAppMessaging : NSObject +/** @fn inAppMessaging + @brief Gets the singleton FIRInAppMessaging object constructed from default Firebase App + settings. +*/ ++ (FIRInAppMessaging *)inAppMessaging NS_SWIFT_NAME(inAppMessaging()); + +/** + * Unavailable. Use +inAppMessaging instead. + */ +- (instancetype)init __attribute__((unavailable("Use +inAppMessaging instead."))); + +/** + * A boolean flag that can be used to suppress messaging display at runtime. It's + * initialized to false at app startup. Once set to true, fiam SDK would stop rendering any + * new messages until it's set back to false. + */ +@property(nonatomic) BOOL messageDisplaySuppressed; + +/** + * A boolean flag that can be set at runtime to allow/disallow fiam SDK automatically + * collect user data on app startup. Settings made via this property is persisted across app + * restarts and has higher priority over FirebaseInAppMessagingAutomaticDataCollectionEnabled + * flag (if present) in plist file. + */ +@property(nonatomic) BOOL automaticDataCollectionEnabled; + +/** + * This is the display component that will be used by FirebaseInAppMessaging to render messages. + * If it's nil (the default case when FirebaseIAppMessaging SDK starts), FirebaseInAppMessaging + * would only perform other non-rendering flows (fetching messages for example). SDK + * FirebaseInAppMessagingDisplay would set itself as the display component if it's included by + * the app. Any other custom implementation of FIRInAppMessagingDisplay would need to set this + * property so that it can be used for rendering fiam message UIs. + */ +@property(nonatomic) id messageDisplayComponent; +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Public/FIRInAppMessagingRendering.h b/Firebase/InAppMessaging/Public/FIRInAppMessagingRendering.h new file mode 100644 index 00000000000..75e0404550d --- /dev/null +++ b/Firebase/InAppMessaging/Public/FIRInAppMessagingRendering.h @@ -0,0 +1,251 @@ +/* + * Copyright 2018 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 + +NS_ASSUME_NONNULL_BEGIN + +/** Contains the display information for an action button. + */ +NS_SWIFT_NAME(InAppMessagingActionButton) +@interface FIRInAppMessagingActionButton : NSObject + +/** + * Gets the text string for the button + */ +@property(nonatomic, nonnull, copy, readonly) NSString *buttonText; + +/** + * Gets the button's text color. + */ +@property(nonatomic, copy, nonnull, readonly) UIColor *buttonTextColor; + +/** + * Gets the button's background color + */ +@property(nonatomic, copy, nonnull, readonly) UIColor *buttonBackgroundColor; + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithButtonText:(NSString *)btnText + buttonTextColor:(UIColor *)textColor + backgroundColor:(UIColor *)bkgColor NS_DESIGNATED_INITIALIZER; +@end + +/** Contain display data for an image for a fiam message. + */ +NS_SWIFT_NAME(InAppMessagingImageData) +@interface FIRInAppMessagingImageData : NSObject +@property(nonatomic, nonnull, copy, readonly) NSString *imageURL; + +/** + * Gets the downloaded image data. It can be null if headless component fails to load it. + */ +@property(nonatomic, readonly, nullable) NSData *imageRawData; + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithImageURL:(NSString *)imageURL + imageData:(NSData *)imageData NS_DESIGNATED_INITIALIZER; +@end + +/** + * Base class representing a FIAM message to be displayed. Don't create instance + * of this class directly. Instantiate one of its subclasses instead. + */ +NS_SWIFT_NAME(InAppMessagingDisplayMessageBase) +@interface FIRInAppMessagingDisplayMessageBase : NSObject +@property(nonatomic, copy, nonnull, readonly) NSString *messageID; +@property(nonatomic, readonly) BOOL renderAsTestMessage; + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithMessageID:(NSString *)messageID + renderAsTestMessage:(BOOL)renderAsTestMessage; +@end + +/** Class for defining a modal message for display. + */ +NS_SWIFT_NAME(InAppMessagingModalDisplay) +@interface FIRInAppMessagingModalDisplay : FIRInAppMessagingDisplayMessageBase + +/** + * Gets the title for a modal fiam message. + */ +@property(nonatomic, nonnull, copy, readonly) NSString *title; + +/** + * Gets the image data for a modal fiam message. + */ +@property(nonatomic, nullable, copy, readonly) FIRInAppMessagingImageData *imageData; + +/** + * Gets the body text for a modal fiam message. + */ +@property(nonatomic, nullable, copy, readonly) NSString *bodyText; + +/** + * Gets the action button metadata for a modal fiam message. + */ +@property(nonatomic, nullable, readonly) FIRInAppMessagingActionButton *actionButton; + +/** + * Gets the background color for a modal fiam message. + */ +@property(nonatomic, copy, nonnull) UIColor *displayBackgroundColor; + +/** + * Gets the color for text in modal fiam message. It would apply to both title and body text. + */ +@property(nonatomic, copy, nonnull) UIColor *textColor; + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithMessageID:(NSString *)messageID + renderAsTestMessage:(BOOL)renderAsTestMessage + titleText:(NSString *)title + bodyText:(NSString *)bodyText + textColor:(UIColor *)textColor + backgroundColor:(UIColor *)backgroundColor + imageData:(nullable FIRInAppMessagingImageData *)imageData + actionButton:(nullable FIRInAppMessagingActionButton *)actionButton + NS_DESIGNATED_INITIALIZER; +@end + +/** Class for defining a banner message for display. + */ +NS_SWIFT_NAME(InAppMessagingBannerDisplay) +@interface FIRInAppMessagingBannerDisplay : FIRInAppMessagingDisplayMessageBase +// Title is always required for modal messages. +@property(nonatomic, nonnull, copy, readonly) NSString *title; + +// Image, body, action URL are all optional for banner messages. +@property(nonatomic, nullable, copy, readonly) FIRInAppMessagingImageData *imageData; +@property(nonatomic, nullable, copy, readonly) NSString *bodyText; + +/** + * Gets banner's background color + */ +@property(nonatomic, copy, nonnull, readonly) UIColor *displayBackgroundColor; + +/** + * Gets the color for text in banner fiam message. It would apply to both title and body text. + */ +@property(nonatomic, copy, nonnull) UIColor *textColor; + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithMessageID:(NSString *)messageID + renderAsTestMessage:(BOOL)renderAsTestMessage + titleText:(NSString *)title + bodyText:(NSString *)bodyText + textColor:(UIColor *)textColor + backgroundColor:(UIColor *)backgroundColor + imageData:(nullable FIRInAppMessagingImageData *)imageData + NS_DESIGNATED_INITIALIZER; +@end + +/** Class for defining a image-only message for display. + */ +NS_SWIFT_NAME(InAppMessagingImageOnlyDisplay) +@interface FIRInAppMessagingImageOnlyDisplay : FIRInAppMessagingDisplayMessageBase + +/** + * Gets the image for this message + */ +@property(nonatomic, nonnull, copy, readonly) FIRInAppMessagingImageData *imageData; +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithMessageID:(NSString *)messageID + renderAsTestMessage:(BOOL)renderAsTestMessage + imageData:(FIRInAppMessagingImageData *)imageData NS_DESIGNATED_INITIALIZER; +@end + +typedef NS_ENUM(NSInteger, FIRInAppMessagingDismissType) { + FIRInAppMessagingDismissTypeUserSwipe, // user swipes away the banner view + FIRInAppMessagingDismissTypeUserTapClose, // user clicks on close buttons + FIRInAppMessagingDismissTypeAuto, // automatic dismiss from banner view + FIRInAppMessagingDismissUnspecified, // message is dismissed, but not belonging to any + // above dismiss category. +}; + +// enum integer value used in as code for NSError reported from displayErrorEncountered: callback +typedef NS_ENUM(NSInteger, FIAMDisplayRenderErrorType) { + FIAMDisplayRenderErrorTypeImageDataInvalid, // provided image data is not valid for image + // rendering + FIAMDisplayRenderErrorTypeUnspecifiedError, // error not classified, mainly unexpected + // failure cases +}; + +/** + * A protocol defining those callbacks to be triggered by the message display component + * under appropriate conditions. + */ +NS_SWIFT_NAME(InAppMessagingDisplayDelegate) +@protocol FIRInAppMessagingDisplayDelegate +/** + * Called when the message is dismissed. Should be called from main thread. + * @param dismissType specifies how the message is closed. + */ +- (void)messageDismissedWithType:(FIRInAppMessagingDismissType)dismissType + NS_SWIFT_NAME(messageDismissed(dismissType:)); + +/** + * Called when the message's action button is followed by the user. + */ +- (void)messageClicked; + +/** + * Use this to mark a message as having gone through enough impression so that + * headless component can make appropriate impression tracking for it. + * + * Calling this is optional. + * + * When messageDismissedWithType: or messageClicked is + * triggered, the message would be marked as having a valid impression implicitly. + * Use impressionDetected if the UI implementation would like to mark valid + * impression in additional cases. One example is that the message is displayed for + * N seconds and then the app is killed by the user. Neither + * onMessageDismissedWithType or onMessageClicked would be triggered + * in this case. But if the app regards this as a valid impression and does not + * want the user to see the same message again, call impressionDetected to mark + * a valid impression. + */ +- (void)impressionDetected; + +/** + * Called when the display component could not render the message due to various reason. + * It's essential for display component to call this when error does arise. On seeing + * this, the headless component of fiam would assume that a prior attempt to render a + * message has finished and therefore it's ready to render a new one when conditions are + * met. Missing this callback in failed rendering attempt would make headless + * component think a fiam message is still being rendered and therefore suppress any + * future message rendering. + */ +- (void)displayErrorEncountered:(NSError *)error; +@end + +/** + * The protocol that a FIAM display component must implement. + */ +NS_SWIFT_NAME(InAppMessagingDisplay) +@protocol FIRInAppMessagingDisplay + +/** + * Method for rendering a specified message on client side. It's called from main thread. + * @param messageForDisplay the message object. It would be of one of the three message + * types at runtime. + * @param displayDelegate the callback object used to trigger notifications about certain + * conditions related to message rendering. + */ +- (void)displayMessage:(FIRInAppMessagingDisplayMessageBase *)messageForDisplay + displayDelegate:(id)displayDelegate; +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/RenderingObjects/FIRInAppMessagingRenderingDataClasses.m b/Firebase/InAppMessaging/RenderingObjects/FIRInAppMessagingRenderingDataClasses.m new file mode 100644 index 00000000000..1148e64ccb1 --- /dev/null +++ b/Firebase/InAppMessaging/RenderingObjects/FIRInAppMessagingRenderingDataClasses.m @@ -0,0 +1,108 @@ +/* + * Copyright 2018 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 + +#import "FIRInAppMessagingRendering.h" + +@implementation FIRInAppMessagingDisplayMessageBase + +- (instancetype)initWithMessageID:(NSString *)messageID + renderAsTestMessage:(BOOL)renderAsTestMessage { + if (self = [super init]) { + _messageID = messageID; + _renderAsTestMessage = renderAsTestMessage; + } + return self; +} +@end + +@implementation FIRInAppMessagingBannerDisplay +- (instancetype)initWithMessageID:(NSString *)messageID + renderAsTestMessage:(BOOL)renderAsTestMessage + titleText:(NSString *)title + bodyText:(NSString *)bodyText + textColor:(UIColor *)textColor + backgroundColor:(UIColor *)backgroundColor + imageData:(nullable FIRInAppMessagingImageData *)imageData { + if (self = [super initWithMessageID:messageID renderAsTestMessage:renderAsTestMessage]) { + _title = title; + _bodyText = bodyText; + _textColor = textColor; + _displayBackgroundColor = backgroundColor; + _imageData = imageData; + } + return self; +} +@end + +@implementation FIRInAppMessagingModalDisplay + +- (instancetype)initWithMessageID:(NSString *)messageID + renderAsTestMessage:(BOOL)renderAsTestMessage + titleText:(NSString *)title + bodyText:(NSString *)bodyText + textColor:(UIColor *)textColor + backgroundColor:(UIColor *)backgroundColor + imageData:(nullable FIRInAppMessagingImageData *)imageData + actionButton:(nullable FIRInAppMessagingActionButton *)actionButton { + if (self = [super initWithMessageID:messageID renderAsTestMessage:renderAsTestMessage]) { + _title = title; + _bodyText = bodyText; + _textColor = textColor; + _displayBackgroundColor = backgroundColor; + _imageData = imageData; + _actionButton = actionButton; + } + return self; +} +@end + +@implementation FIRInAppMessagingImageOnlyDisplay + +- (instancetype)initWithMessageID:(NSString *)messageID + renderAsTestMessage:(BOOL)renderAsTestMessage + imageData:(FIRInAppMessagingImageData *)imageData { + if (self = [super initWithMessageID:messageID renderAsTestMessage:renderAsTestMessage]) { + _imageData = imageData; + } + return self; +} +@end + +@implementation FIRInAppMessagingActionButton + +- (instancetype)initWithButtonText:(NSString *)btnText + buttonTextColor:(UIColor *)textColor + backgroundColor:(UIColor *)bkgColor { + if (self = [super init]) { + _buttonText = btnText; + _buttonTextColor = textColor; + _buttonBackgroundColor = bkgColor; + } + return self; +} +@end + +@implementation FIRInAppMessagingImageData +- (instancetype)initWithImageURL:(NSString *)imageURL imageData:(NSData *)imageData { + if (self = [super init]) { + _imageURL = imageURL; + _imageRawData = imageData; + } + return self; +} +@end diff --git a/Firebase/InAppMessaging/Runtime/FIRIAMActionURLFollower.h b/Firebase/InAppMessaging/Runtime/FIRIAMActionURLFollower.h new file mode 100644 index 00000000000..a05d4b65fbb --- /dev/null +++ b/Firebase/InAppMessaging/Runtime/FIRIAMActionURLFollower.h @@ -0,0 +1,46 @@ +/* + * Copyright 2018 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 +#import + +NS_ASSUME_NONNULL_BEGIN +// A class for handling action url following. +// It tries to handle these cases: +// 1 Follow a universal link. +// 2 Follow a custom url scheme link. +// 3 Follow other types of links. +@interface FIRIAMActionURLFollower : NSObject + +// Create an FIRIAMActionURLFollower object by inspecting the app's main bundle info. ++ (instancetype)actionURLFollower; + +- (instancetype)init NS_UNAVAILABLE; + +// initialize the instance with an array of supported custom url schemes and +// the main application object +- (instancetype)initWithCustomURLSchemeArray:(NSArray *)customURLScheme + withApplication:(UIApplication *)application NS_DESIGNATED_INITIALIZER; + +/** + * Follow a given URL. Report success in the completion block parameter. Notice that + * it can not always be fully sure about whether the operation is successful. So it's a clue + * in some cases. + * Check its implementation about the details in the following logic. + */ +- (void)followActionURL:(NSURL *)actionURL withCompletionBlock:(void (^)(BOOL success))completion; +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Runtime/FIRIAMActionURLFollower.m b/Firebase/InAppMessaging/Runtime/FIRIAMActionURLFollower.m new file mode 100644 index 00000000000..58416179dda --- /dev/null +++ b/Firebase/InAppMessaging/Runtime/FIRIAMActionURLFollower.m @@ -0,0 +1,244 @@ +/* + * Copyright 2018 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 +#import + +#import "FIRCore+InAppMessaging.h" +#import "FIRIAMActionURLFollower.h" + +@interface FIRIAMActionURLFollower () +@property(nonatomic, readonly, nonnull, copy) NSSet *appCustomURLSchemesSet; +@property(nonatomic, readonly) BOOL isOldAppDelegateOpenURLDefined; +@property(nonatomic, readonly) BOOL isNewAppDelegateOpenURLDefined; +@property(nonatomic, readonly) BOOL isContinueUserActivityMethodDefined; + +@property(nonatomic, readonly, nullable) id appDelegate; +@property(nonatomic, readonly, nonnull) UIApplication *mainApplication; +@end + +@implementation FIRIAMActionURLFollower + ++ (FIRIAMActionURLFollower *)actionURLFollower { + static FIRIAMActionURLFollower *URLFollower; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + NSMutableArray *customSchemeURLs = [[NSMutableArray alloc] init]; + + // Reading the custom url list from the environment. + NSBundle *appBundle = [NSBundle mainBundle]; + if (appBundle) { + id URLTypesID = [appBundle objectForInfoDictionaryKey:@"CFBundleURLTypes"]; + if ([URLTypesID isKindOfClass:[NSArray class]]) { + NSArray *urlTypesArray = (NSArray *)URLTypesID; + + for (id nextURLType in urlTypesArray) { + if ([nextURLType isKindOfClass:[NSDictionary class]]) { + NSDictionary *nextURLTypeDict = (NSDictionary *)nextURLType; + id nextSchemeArray = nextURLTypeDict[@"CFBundleURLSchemes"]; + if (nextSchemeArray && [nextSchemeArray isKindOfClass:[NSArray class]]) { + [customSchemeURLs addObjectsFromArray:nextSchemeArray]; + } + } + } + } + } + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM300010", + @"Detected %d custom url schems from environment", (int)customSchemeURLs.count); + + if ([NSThread isMainThread]) { + // We can not dispatch sychronously to main queue if we are already in main queue. That + // can cause deadlock. + URLFollower = [[FIRIAMActionURLFollower alloc] + initWithCustomURLSchemeArray:customSchemeURLs + withApplication:UIApplication.sharedApplication]; + } else { + // If we are not on main thread, dispatch it to main queue since it invovles calling UIKit + // methods, which are required to be carried out on main queue. + dispatch_sync(dispatch_get_main_queue(), ^{ + URLFollower = [[FIRIAMActionURLFollower alloc] + initWithCustomURLSchemeArray:customSchemeURLs + withApplication:UIApplication.sharedApplication]; + }); + } + }); + return URLFollower; +} + +- (instancetype)initWithCustomURLSchemeArray:(NSArray *)customURLScheme + withApplication:(UIApplication *)application { + if (self = [super init]) { + _appCustomURLSchemesSet = [NSSet setWithArray:customURLScheme]; + _mainApplication = application; + _appDelegate = [application delegate]; + + if (_appDelegate) { + _isOldAppDelegateOpenURLDefined = [_appDelegate + respondsToSelector:@selector(application:openURL:sourceApplication:annotation:)]; + + _isNewAppDelegateOpenURLDefined = + [_appDelegate respondsToSelector:@selector(application:openURL:options:)]; + + _isContinueUserActivityMethodDefined = [_appDelegate + respondsToSelector:@selector(application:continueUserActivity:restorationHandler:)]; + } + } + return self; +} + +- (void)followActionURL:(NSURL *)actionURL withCompletionBlock:(void (^)(BOOL success))completion { + // So this is the logic of the url following flow + // 1 If it's a http or https link + // 1.1 If delegate implements application:continueUserActivity:restorationHandler: and calling + // it returns YES: the flow stops here: we have finished the url-following action + // 1.2 In other cases: fall through to step 3 + // 2 If the URL scheme matches any element in appCustomURLSchemes + // 2.1 Triggers application:openURL:options: or + // application:openURL:sourceApplication:annotation: + // depending on their availability. + // 3 Use UIApplication openURL: or openURL:options:completionHandler: to have iOS system to deal + // with the url following. + // + // The rationale for doing step 1 and 2 instead of simply doing step 3 for all cases are: + // I) calling UIApplication openURL with the universal link targeted for current app would + // not cause the link being treated as a universal link. See apple doc at + // https://developer.apple.com/library/content/documentation/General/Conceptual/AppSearch/UniversalLinks.html + // So step 1 is trying to handle this gracefully + // II) If there are other apps on the same device declaring the same custom url scheme as for + // the current app, doing step 3 directly have the risk of triggering another app for + // handling the custom scheme url: See the note about "If more than one third-party" from + // https://developer.apple.com/library/content/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/Inter-AppCommunication/Inter-AppCommunication.html + // So step 2 is to optimize user experience by short-circuiting the engagement with iOS + // system + + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240007", @"Following action url %@", actionURL); + + if ([self.class isHttpOrHttpsScheme:actionURL]) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240001", @"Try to treat it as a universal link."); + if ([self followURLWithContinueUserActivity:actionURL]) { + completion(YES); + return; // following the url has been fully handled by App Delegate's + // continueUserActivity method + } + } else if ([self isCustomSchemeForCurrentApp:actionURL]) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240002", @"Custom URL scheme matches."); + if ([self followURLWithAppDelegateOpenURLActivity:actionURL]) { + completion(YES); + return; // following the url has been fully handled by App Delegate's openURL method + } + } + + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240003", @"Open the url via iOS."); + [self followURLViaIOS:actionURL withCompletionBlock:completion]; +} + +// Try to handle the url as a custom scheme url link by triggering +// application:openURL:options: on App's delegate object directly. +// @returns YES if that delegate method is defined and returns YES. +- (BOOL)followURLWithAppDelegateOpenURLActivity:(NSURL *)url { + if (self.isNewAppDelegateOpenURLDefined) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM210008", + @"iOS 9+ version of App Delegate's application:openURL:options: method detected"); +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunguarded-availability" + return [self.appDelegate application:self.mainApplication openURL:url options:@{}]; +#pragma clang pop + } + + // if we come here, we can try to trigger the older version of openURL method on the app's + // delegate + if (self.isOldAppDelegateOpenURLDefined) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240009", + @"iOS 9 below version of App Delegate's openURL method detected"); + NSString *appBundleIdentifier = [[NSBundle mainBundle] bundleIdentifier]; + BOOL handled = [self.appDelegate application:self.mainApplication + openURL:url + sourceApplication:appBundleIdentifier + annotation:@{}]; + return handled; + } + + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240010", + @"No approriate openURL method defined for App Delegate"); + return NO; +} + +// Try to handle the url as a universal link by triggering +// application:continueUserActivity:restorationHandler: on App's delegate object directly. +// @returns YES if that delegate method is defined and seeing a YES being returned from +// trigging it +- (BOOL)followURLWithContinueUserActivity:(NSURL *)url { + if (self.isContinueUserActivityMethodDefined) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240004", + @"App delegate responds to application:continueUserActivity:restorationHandler:." + "Simulating action url opening from a web browser."); + NSUserActivity *userActivity = + [[NSUserActivity alloc] initWithActivityType:NSUserActivityTypeBrowsingWeb]; + userActivity.webpageURL = url; + BOOL handled = [self.appDelegate application:self.mainApplication + continueUserActivity:userActivity + restorationHandler:^(NSArray *restorableObjects) { + // mimic system behavior of triggering restoreUserActivityState: + // method on each element of restorableObjects + for (id nextRestoreObject in restorableObjects) { + if ([nextRestoreObject isKindOfClass:[UIResponder class]]) { + UIResponder *responder = (UIResponder *)nextRestoreObject; + [responder restoreUserActivityState:userActivity]; + } + } + }]; + if (handled) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240005", + @"App handling acton URL returns YES, no more further action taken"); + } else { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240004", @"App handling acton URL returns NO."); + } + return handled; + } else { + return NO; + } +} + +- (void)followURLViaIOS:(NSURL *)url withCompletionBlock:(void (^)(BOOL success))completion { + if ([self.mainApplication respondsToSelector:@selector(openURL:options:completionHandler:)]) { + NSDictionary *options = @{}; + [self.mainApplication + openURL:url + options:options + completionHandler:^(BOOL success) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240006", @"openURL result is %d", success); + completion(success); + }]; + } else { + // fallback to the older version of openURL + BOOL success = [self.mainApplication openURL:url]; + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240007", @"openURL result is %d", success); + completion(success); + } +} + +- (BOOL)isCustomSchemeForCurrentApp:(NSURL *)url { + NSString *schemeInLowerCase = [url.scheme lowercaseString]; + return [self.appCustomURLSchemesSet containsObject:schemeInLowerCase]; +} + ++ (BOOL)isHttpOrHttpsScheme:(NSURL *)url { + NSString *schemeInLowerCase = [url.scheme lowercaseString]; + return + [schemeInLowerCase isEqualToString:@"https"] || [schemeInLowerCase isEqualToString:@"http"]; +} +@end diff --git a/Firebase/InAppMessaging/Runtime/FIRIAMRuntimeManager.h b/Firebase/InAppMessaging/Runtime/FIRIAMRuntimeManager.h new file mode 100644 index 00000000000..f106fbcc093 --- /dev/null +++ b/Firebase/InAppMessaging/Runtime/FIRIAMRuntimeManager.h @@ -0,0 +1,56 @@ +/* + * 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 + +#import "FIRIAMActivityLogger.h" +#import "FIRIAMBookKeeper.h" +#import "FIRIAMDisplayExecutor.h" +#import "FIRIAMMessageClientCache.h" +#import "FIRIAMSDKSettings.h" +#import "FIRIAMServerMsgFetchStorage.h" + +NS_ASSUME_NONNULL_BEGIN +// A class for managing the objects/dependencies for supporting different fiam flows at runtime +@interface FIRIAMRuntimeManager : NSObject +@property(nonatomic, nonnull) FIRIAMSDKSettings *currentSetting; +@property(nonatomic, nonnull) FIRIAMActivityLogger *activityLogger; +@property(nonatomic, nonnull) FIRIAMBookKeeperViaUserDefaults *bookKeeper; +@property(nonatomic, nonnull) FIRIAMMessageClientCache *messageCache; +@property(nonatomic, nonnull) FIRIAMServerMsgFetchStorage *fetchResultStorage; +@property(nonatomic, nonnull) FIRIAMDisplayExecutor *displayExecutor; + +// Initialize fiam SDKs and start various flows with specified settings. +- (void)startRuntimeWithSDKSettings:(FIRIAMSDKSettings *)settings; + +// Pause runtime flows/functions to disable SDK functions at runtime +- (void)pause; + +// Resume runtime flows/functions. +- (void)resume; + +// allows app to programmatically turn on/off auto data collection for fiam, which also implies +// running/stopping fiam functionalities +@property(nonatomic) BOOL automaticDataCollectionEnabled; + +// Get the global singleton instance ++ (FIRIAMRuntimeManager *)getSDKRuntimeInstance; + +// a method used to suppress or allow message being displayed based on the parameter +// @param shouldSuppress if true, no new message is rendered by the sdk. +- (void)setShouldSuppressMessageDisplay:(BOOL)shouldSuppress; +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Runtime/FIRIAMRuntimeManager.m b/Firebase/InAppMessaging/Runtime/FIRIAMRuntimeManager.m new file mode 100644 index 00000000000..f8ccf87b8f8 --- /dev/null +++ b/Firebase/InAppMessaging/Runtime/FIRIAMRuntimeManager.m @@ -0,0 +1,431 @@ +/* + * 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 + +#import "FIRCore+InAppMessaging.h" +#import "FIRIAMActivityLogger.h" +#import "FIRIAMAnalyticsEventLoggerImpl.h" +#import "FIRIAMBookKeeper.h" +#import "FIRIAMClearcutHttpRequestSender.h" +#import "FIRIAMClearcutLogStorage.h" +#import "FIRIAMClearcutLogger.h" +#import "FIRIAMClearcutUploader.h" +#import "FIRIAMClientInfoFetcher.h" +#import "FIRIAMDisplayCheckOnAnalyticEventsFlow.h" +#import "FIRIAMDisplayCheckOnAppForegroundFlow.h" +#import "FIRIAMDisplayCheckOnFetchDoneNotificationFlow.h" +#import "FIRIAMDisplayExecutor.h" +#import "FIRIAMFetchOnAppForegroundFlow.h" +#import "FIRIAMFetchResponseParser.h" +#import "FIRIAMMessageClientCache.h" +#import "FIRIAMMsgFetcherUsingRestful.h" +#import "FIRIAMRuntimeManager.h" +#import "FIRIAMSDKModeManager.h" +#import "FIRInAppMessaging.h" + +@interface FIRInAppMessaging () +@property(nonatomic, readwrite, strong) id _Nullable analytics; +@end + +// A enum indicating 3 different possiblities of a setting about auto data collection. +typedef NS_ENUM(NSInteger, FIRIAMAutoDataCollectionSetting) { + // This indicates that the config is not explicitly set. + FIRIAMAutoDataCollectionSettingNone = 0, + + // This indicates that the setting explicitly enables the auto data collection. + FIRIAMAutoDataCollectionSettingEnabled = 1, + + // This indicates that the setting explicitly disables the auto data collection. + FIRIAMAutoDataCollectionSettingDisabled = 2, +}; + +@interface FIRIAMRuntimeManager () +@property(nonatomic, nonnull) FIRIAMMsgFetcherUsingRestful *restfulFetcher; +@property(nonatomic, nonnull) FIRIAMDisplayCheckOnAppForegroundFlow *displayOnAppForegroundFlow; +@property(nonatomic, nonnull) FIRIAMDisplayCheckOnFetchDoneNotificationFlow *displayOnFetchDoneFlow; +@property(nonatomic, nonnull) + FIRIAMDisplayCheckOnAnalyticEventsFlow *displayOnFIRAnalyticEventsFlow; + +@property(nonatomic, nonnull) FIRIAMFetchOnAppForegroundFlow *fetchOnAppForegroundFlow; +@property(nonatomic, nonnull) FIRIAMClientInfoFetcher *clientInfoFetcher; +@property(nonatomic, nonnull) FIRIAMFetchResponseParser *responseParser; +@end + +static NSString *const _userDefaultsKeyForFIAMProgammaticAutoDataCollectionSetting = + @"firebase-iam-sdk-auto-data-collection"; + +@implementation FIRIAMRuntimeManager { + // since we allow the SDK feature to be disabled/enabled at runtime, we need a field to track + // its state on this + BOOL _running; +} ++ (FIRIAMRuntimeManager *)getSDKRuntimeInstance { + static FIRIAMRuntimeManager *managerInstance = nil; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + managerInstance = [[FIRIAMRuntimeManager alloc] init]; + }); + + return managerInstance; +} + +// For protocol FIRIAMTestingModeListener. +- (void)testingModeSwitchedOn { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180015", + @"Dynamically switch to the display flow for testing mode instance."); + + [self.displayOnAppForegroundFlow stop]; + [self.displayOnFetchDoneFlow start]; +} + +- (FIRIAMAutoDataCollectionSetting)FIAMProgrammaticAutoDataCollectionSetting { + id settingEntry = [[NSUserDefaults standardUserDefaults] + objectForKey:_userDefaultsKeyForFIAMProgammaticAutoDataCollectionSetting]; + + if (![settingEntry isKindOfClass:[NSNumber class]]) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180014", + @"No auto data collection enable setting entry detected." + "So no FIAM programmatic setting from the app."); + return FIRIAMAutoDataCollectionSettingNone; + } else { + if ([(NSNumber *)settingEntry boolValue]) { + return FIRIAMAutoDataCollectionSettingEnabled; + } else { + return FIRIAMAutoDataCollectionSettingDisabled; + } + } +} + +// the key for the plist entry to suppress auto start +static NSString *const kFirebaseInAppMessagingAutoDataCollectionKey = + @"FirebaseInAppMessagingAutomaticDataCollectionEnabled"; + +- (FIRIAMAutoDataCollectionSetting)FIAMPlistAutoDataCollectionSetting { + id fiamAutoDataCollectionPlistEntry = [[NSBundle mainBundle] + objectForInfoDictionaryKey:kFirebaseInAppMessagingAutoDataCollectionKey]; + + if ([fiamAutoDataCollectionPlistEntry isKindOfClass:[NSNumber class]]) { + BOOL fiamDataCollectionEnabledPlistSetting = + [(NSNumber *)fiamAutoDataCollectionPlistEntry boolValue]; + + if (fiamDataCollectionEnabledPlistSetting) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180011", + @"Auto data collection is explicitly enabled in FIAM plist entry."); + return FIRIAMAutoDataCollectionSettingEnabled; + } else { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180012", + @"Auto data collection is explicitly disabled in FIAM plist entry."); + return FIRIAMAutoDataCollectionSettingDisabled; + } + } else { + return FIRIAMAutoDataCollectionSettingNone; + } +} + +// Whether data collection is enabled by FIAM programmatic flag. +- (BOOL)automaticDataCollectionEnabled { + return + [self FIAMProgrammaticAutoDataCollectionSetting] != FIRIAMAutoDataCollectionSettingDisabled; +} + +// Sets FIAM's programmatic flag for auto data collection. +- (void)setAutomaticDataCollectionEnabled:(BOOL)automaticDataCollectionEnabled { + if (automaticDataCollectionEnabled) { + [self resume]; + } else { + [self pause]; + } +} + +- (BOOL)shouldRunSDKFlowsOnStartup { + // This can be controlled at 3 different levels in decsending priority. If a higher-priority + // setting exists, the lower level settings are ignored. + // 1. Setting made by the app by setting FIAM SDK's automaticDataCollectionEnabled flag. + // 2. FIAM specific data collection setting in plist file. + // 3. Global Firebase auto data collecting setting (carried over by currentSetting property). + + FIRIAMAutoDataCollectionSetting programmaticSetting = + [self FIAMProgrammaticAutoDataCollectionSetting]; + + if (programmaticSetting == FIRIAMAutoDataCollectionSettingEnabled) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180010", + @"FIAM auto data-collection is explicitly enabled, start SDK flows."); + return true; + } else if (programmaticSetting == FIRIAMAutoDataCollectionSettingDisabled) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180013", + @"FIAM auto data-collection is explicitly disabled, do not start SDK flows."); + return false; + } else { + // No explicit setting from fiam's programmatic setting. Checking next level down. + FIRIAMAutoDataCollectionSetting fiamPlistDataCollectionSetting = + [self FIAMPlistAutoDataCollectionSetting]; + + if (fiamPlistDataCollectionSetting == FIRIAMAutoDataCollectionSettingNone) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180018", + @"No programmatic or plist setting at FIAM level. Fallback to global Firebase " + "level setting."); + return self.currentSetting.isFirebaseAutoDataCollectionEnabled; + } else { + return fiamPlistDataCollectionSetting == FIRIAMAutoDataCollectionSettingEnabled; + } + } +} + +- (void)resume { + // persist the setting + [[NSUserDefaults standardUserDefaults] + setObject:@(YES) + forKey:_userDefaultsKeyForFIAMProgammaticAutoDataCollectionSetting]; + + @synchronized(self) { + if (!_running) { + [self.fetchOnAppForegroundFlow start]; + [self.displayOnAppForegroundFlow start]; + [self.displayOnFIRAnalyticEventsFlow start]; + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180019", + @"Start Firebase In-App Messaging flows from inactive."); + _running = YES; + } else { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM180004", + @"Runtime is already active, resume is just a no-op"); + } + } +} + +- (void)pause { + // persist the setting + [[NSUserDefaults standardUserDefaults] + setObject:@(NO) + forKey:_userDefaultsKeyForFIAMProgammaticAutoDataCollectionSetting]; + + @synchronized(self) { + if (_running) { + [self.fetchOnAppForegroundFlow stop]; + [self.displayOnAppForegroundFlow stop]; + [self.displayOnFIRAnalyticEventsFlow stop]; + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180006", + @"Shutdown Firebase In-App Messaging flows."); + _running = NO; + } else { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM180005", + @"No runtime active yet, pause is just a no-op"); + } + } +} + +- (void)setShouldSuppressMessageDisplay:(BOOL)shouldSuppress { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180003", @"Message display suppress set to %@", + @(shouldSuppress)); + self.displayExecutor.suppressMessageDisplay = shouldSuppress; +} + +- (void)startRuntimeWithSDKSettings:(FIRIAMSDKSettings *)settings { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul), ^{ + [self internalStartRuntimeWithSDKSettings:settings]; + }); +} + +- (void)internalStartRuntimeWithSDKSettings:(FIRIAMSDKSettings *)settings { + if (_running) { + // Runtime has been started previously. Stop all the flows first. + [self.fetchOnAppForegroundFlow stop]; + [self.displayOnAppForegroundFlow stop]; + [self.displayOnFIRAnalyticEventsFlow stop]; + } + + self.currentSetting = settings; + + FIRIAMTimerWithNSDate *timeFetcher = [[FIRIAMTimerWithNSDate alloc] init]; + NSTimeInterval start = [timeFetcher currentTimestampInSeconds]; + + self.activityLogger = + [[FIRIAMActivityLogger alloc] initWithMaxCountBeforeReduce:settings.loggerMaxCountBeforeReduce + withSizeAfterReduce:settings.loggerSizeAfterReduce + verboseMode:settings.loggerInVerboseMode + loadFromCache:YES]; + + self.responseParser = [[FIRIAMFetchResponseParser alloc] initWithTimeFetcher:timeFetcher]; + + self.bookKeeper = [[FIRIAMBookKeeperViaUserDefaults alloc] + initWithUserDefaults:[NSUserDefaults standardUserDefaults]]; + + self.messageCache = [[FIRIAMMessageClientCache alloc] initWithBookkeeper:self.bookKeeper + usingResponseParser:self.responseParser]; + self.fetchResultStorage = [[FIRIAMServerMsgFetchStorage alloc] init]; + self.clientInfoFetcher = [[FIRIAMClientInfoFetcher alloc] init]; + + self.restfulFetcher = + [[FIRIAMMsgFetcherUsingRestful alloc] initWithHost:settings.apiServerHost + HTTPProtocol:settings.apiHttpProtocol + project:settings.firebaseProjectNumber + firebaseApp:settings.firebaseAppId + APIKey:settings.apiKey + fetchStorage:self.fetchResultStorage + instanceIDFetcher:self.clientInfoFetcher + usingURLSession:nil + responseParser:self.responseParser]; + + // start fetch on app foreground flow + FIRIAMFetchSetting *fetchSetting = [[FIRIAMFetchSetting alloc] init]; + fetchSetting.fetchMinIntervalInMinutes = settings.fetchMinIntervalInMinutes; + + // start render on app foreground flow + FIRIAMDisplaySetting *appForegroundDisplaysetting = [[FIRIAMDisplaySetting alloc] init]; + appForegroundDisplaysetting.displayMinIntervalInMinutes = + settings.appFGRenderMinIntervalInMinutes; + + // clearcut log expires after 14 days: give up on attempting to deliver them any more + NSInteger ctLogExpiresInSeconds = 14 * 24 * 60 * 60; + + FIRIAMClearcutLogStorage *ctLogStorage = + [[FIRIAMClearcutLogStorage alloc] initWithExpireAfterInSeconds:ctLogExpiresInSeconds + withTimeFetcher:timeFetcher]; + + FIRIAMClearcutHttpRequestSender *clearcutRequestSender = [[FIRIAMClearcutHttpRequestSender alloc] + initWithClearcutHost:settings.clearcutServerHost + usingTimeFetcher:timeFetcher + withOSMajorVersion:[self.clientInfoFetcher getOSMajorVersion]]; + + FIRIAMClearcutUploader *ctUploader = + [[FIRIAMClearcutUploader alloc] initWithRequestSender:clearcutRequestSender + timeFetcher:timeFetcher + logStorage:ctLogStorage + usingStrategy:settings.clearcutStrategy + usingUserDefaults:nil]; + + FIRIAMClearcutLogger *clearcutLogger = + [[FIRIAMClearcutLogger alloc] initWithFBProjectNumber:settings.firebaseProjectNumber + fbAppId:settings.firebaseAppId + clientInfoFetcher:self.clientInfoFetcher + usingTimeFetcher:timeFetcher + usingUploader:ctUploader]; + + FIRIAMAnalyticsEventLoggerImpl *analyticsEventLogger = [[FIRIAMAnalyticsEventLoggerImpl alloc] + initWithClearcutLogger:clearcutLogger + usingTimeFetcher:timeFetcher + usingUserDefaults:nil + analytics:[FIRInAppMessaging inAppMessaging].analytics]; + + FIRIAMSDKModeManager *sdkModeManager = + [[FIRIAMSDKModeManager alloc] initWithUserDefaults:NSUserDefaults.standardUserDefaults + testingModeListener:self]; + + self.fetchOnAppForegroundFlow = + [[FIRIAMFetchOnAppForegroundFlow alloc] initWithSetting:fetchSetting + messageCache:self.messageCache + messageFetcher:self.restfulFetcher + timeFetcher:timeFetcher + bookKeeper:self.bookKeeper + activityLogger:self.activityLogger + analyticsEventLogger:analyticsEventLogger + FIRIAMSDKModeManager:sdkModeManager]; + + FIRIAMActionURLFollower *actionFollower = [FIRIAMActionURLFollower actionURLFollower]; + + self.displayExecutor = [[FIRIAMDisplayExecutor alloc] initWithSetting:appForegroundDisplaysetting + messageCache:self.messageCache + timeFetcher:timeFetcher + bookKeeper:self.bookKeeper + actionURLFollower:actionFollower + activityLogger:self.activityLogger + analyticsEventLogger:analyticsEventLogger]; + + // Setting the display component. It's needed in case headless SDK is initialized after + // the display component is already set on FIRInAppMessaging. + self.displayExecutor.messageDisplayComponent = + FIRInAppMessaging.inAppMessaging.messageDisplayComponent; + + // Both display flows are created on startup. But they would only be turned on (started) based on + // the sdk mode for the current instance + self.displayOnFetchDoneFlow = [[FIRIAMDisplayCheckOnFetchDoneNotificationFlow alloc] + initWithDisplayFlow:self.displayExecutor]; + self.displayOnAppForegroundFlow = + [[FIRIAMDisplayCheckOnAppForegroundFlow alloc] initWithDisplayFlow:self.displayExecutor]; + + self.displayOnFIRAnalyticEventsFlow = + [[FIRIAMDisplayCheckOnAnalyticEventsFlow alloc] initWithDisplayFlow:self.displayExecutor]; + + self.messageCache.analycisEventDislayCheckFlow = self.displayOnFIRAnalyticEventsFlow; + [self.messageCache + loadMessageDataFromServerFetchStorage:self.fetchResultStorage + withCompletion:^(BOOL success) { + // start flows regardless whether we can load messages from fetch + // storage successfully + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180001", + @"Message loading from fetch storage was done."); + + if ([self shouldRunSDKFlowsOnStartup]) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180008", + @"Start SDK runtime components."); + + [self.clientInfoFetcher + fetchFirebaseIIDDataWithProjectNumber: + self.currentSetting.firebaseProjectNumber + withCompletion:^( + NSString *_Nullable iid, + NSString *_Nullable token, + NSError *_Nullable error) { + // Always dump the instance id into + // log on startup to help developers + // to find it for their app instance. + FIRLogDebug(kFIRLoggerInAppMessaging, + @"I-IAM180017", + @"Starting " + @"InAppMessaging runtime " + @"with " + "Instance ID %@", + iid); + }]; + + [self.fetchOnAppForegroundFlow start]; + [self.displayOnFIRAnalyticEventsFlow start]; + + self->_running = YES; + + if (sdkModeManager.currentMode == FIRIAMSDKModeTesting) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180007", + @"InAppMessaging testing mode enabled. App " + "foreground messages will be displayed following " + "fetch"); + [self.displayOnFetchDoneFlow start]; + } else { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180020", + @"Start regular display flow for non-testing " + "instance mode"); + [self.displayOnAppForegroundFlow start]; + + // Simulate app going into foreground on startup + [self.displayExecutor checkAndDisplayNextAppForegroundMessage]; + } + + // One-time triggering of checks for both fetch flow + // upon SDK/app startup. + [self.fetchOnAppForegroundFlow checkAndFetch]; + } else { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180009", + @"No FIAM SDK startup due to settings."); + } + }]; + + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180002", + @"Firebase In-App Messaging SDK version %@ finished startup in %lf seconds " + "with these settings: %@", + [self.clientInfoFetcher getIAMSDKVersion], + (double)([timeFetcher currentTimestampInSeconds] - start), settings); +} +@end diff --git a/Firebase/InAppMessaging/Runtime/FIRIAMSDKModeManager.h b/Firebase/InAppMessaging/Runtime/FIRIAMSDKModeManager.h new file mode 100644 index 00000000000..65534186fec --- /dev/null +++ b/Firebase/InAppMessaging/Runtime/FIRIAMSDKModeManager.h @@ -0,0 +1,73 @@ +/* + * Copyright 2018 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 + +NS_ASSUME_NONNULL_BEGIN + +extern NSInteger const kFIRIAMMaxFetchInNewlyInstalledMode; + +/** + * At runtime a FIAM SDK client can function in one of the following modes: + * 1 Regular. This SDK client instance will conform to regular fetch minimal interval time policy. + * 2 Newly installed. This is a mode a newly installed SDK stays in until the first + * kFIRIAMMaxFetchInNewlyInstalledMode fetches have finished. In this mode, there is no + * minimal time interval between fetches: a fetch would be triggered as long as the app goes + * into foreground state. + * 3 Testing Instance. This app instance is targeted for test on device feature for fiam. When + * it's in this mode, no minimal time interval between fetches is applied. SDK turns itself + * into this mode on seeing test-on-client messages are returned in fetch responses. + */ + +typedef NS_ENUM(NSInteger, FIRIAMSDKMode) { + FIRIAMSDKModeRegular, + FIRIAMSDKModeTesting, + FIRIAMSDKModeNewlyInstalled +}; + +// turn the sdk mode enum integer value into a descriptive string +NSString *FIRIAMDescriptonStringForSDKMode(FIRIAMSDKMode mode); + +extern NSString *const kFIRIAMUserDefaultKeyForSDKMode; +extern NSString *const kFIRIAMUserDefaultKeyForServerFetchCount; +extern NSInteger const kFIRIAMMaxFetchInNewlyInstalledMode; + +@protocol FIRIAMTestingModeListener +// Triggered when the current app switches into testing mode from a using testing mode +- (void)testingModeSwitchedOn; +@end + +// A class for tracking and updating the SDK mode. The tracked mode related info is persisted +// so that it can be restored beyond app restarts +@interface FIRIAMSDKModeManager : NSObject + +- (instancetype)init NS_UNAVAILABLE; + +// having NSUserDefaults as passed-in to help with unit testing +- (instancetype)initWithUserDefaults:(NSUserDefaults *)userDefaults + testingModeListener:(id)testingModeListener; + +// returns the current SDK mode +- (FIRIAMSDKMode)currentMode; + +// turn the current SDK into 'Testing Instance' mode. +- (void)becomeTestingInstance; +// inform the manager that one more fetch is done. This is to allow +// the manager to potentially graduate from the newly installed mode. +- (void)registerOneMoreFetch; + +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Runtime/FIRIAMSDKModeManager.m b/Firebase/InAppMessaging/Runtime/FIRIAMSDKModeManager.m new file mode 100644 index 00000000000..b59c8f784e9 --- /dev/null +++ b/Firebase/InAppMessaging/Runtime/FIRIAMSDKModeManager.m @@ -0,0 +1,113 @@ +/* + * Copyright 2018 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 + +#import "FIRCore+InAppMessaging.h" +#import "FIRIAMSDKModeManager.h" + +NSString *FIRIAMDescriptonStringForSDKMode(FIRIAMSDKMode mode) { + switch (mode) { + case FIRIAMSDKModeTesting: + return @"Testing Instance"; + case FIRIAMSDKModeRegular: + return @"Regular"; + case FIRIAMSDKModeNewlyInstalled: + return @"Newly Installed"; + default: + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM290003", @"Unknown sdk mode value %d", + (int)mode); + return @"Unknown"; + } +} + +@interface FIRIAMSDKModeManager () +@property(nonatomic, nonnull, readonly) NSUserDefaults *userDefaults; +// Make it weak so that we don't depend on its existence to avoid circular reference. +@property(nonatomic, readonly, weak) id testingModeListener; +@end + +NSString *const kFIRIAMUserDefaultKeyForSDKMode = @"firebase-iam-sdk-mode"; +NSString *const kFIRIAMUserDefaultKeyForServerFetchCount = @"firebase-iam-server-fetch-count"; +NSInteger const kFIRIAMMaxFetchInNewlyInstalledMode = 5; + +@implementation FIRIAMSDKModeManager { + FIRIAMSDKMode _sdkMode; + NSInteger _fetchCount; +} + +- (instancetype)initWithUserDefaults:(NSUserDefaults *)userDefaults + testingModeListener:(id)testingModeListener { + if (self = [super init]) { + _userDefaults = userDefaults; + _testingModeListener = testingModeListener; + + id modeEntry = [_userDefaults objectForKey:kFIRIAMUserDefaultKeyForSDKMode]; + if (modeEntry == nil) { + // no entry yet, it's a newly installed sdk instance + _sdkMode = FIRIAMSDKModeNewlyInstalled; + + // initialize the mode and fetch count in the persistent storage + [_userDefaults setObject:[NSNumber numberWithInt:_sdkMode] + forKey:kFIRIAMUserDefaultKeyForSDKMode]; + [_userDefaults setInteger:0 forKey:kFIRIAMUserDefaultKeyForServerFetchCount]; + } else { + _sdkMode = [(NSNumber *)modeEntry integerValue]; + _fetchCount = [_userDefaults integerForKey:kFIRIAMUserDefaultKeyForServerFetchCount]; + } + + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM290001", + @"SDK is in mode of %@ and has seen %d fetches.", + FIRIAMDescriptonStringForSDKMode(_sdkMode), (int)_fetchCount); + } + return self; +} + +// inform the manager that one more fetch is done. This is to allow +// the manager to potentially graduate from the newly installed mode. +- (void)registerOneMoreFetch { + // we only care about the fetch count when sdk is in newly installed mode (so that it may + // graduate from that after certain number of fetches). + if (_sdkMode == FIRIAMSDKModeNewlyInstalled) { + if (++_fetchCount >= kFIRIAMMaxFetchInNewlyInstalledMode) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM290002", + @"Coming out of newly installed mode since there have been %d fetches", + (int)_fetchCount); + + _sdkMode = FIRIAMSDKModeRegular; + [_userDefaults setObject:[NSNumber numberWithInt:_sdkMode] + forKey:kFIRIAMUserDefaultKeyForSDKMode]; + } else { + [_userDefaults setInteger:_fetchCount forKey:kFIRIAMUserDefaultKeyForServerFetchCount]; + } + } +} + +- (void)becomeTestingInstance { + _sdkMode = FIRIAMSDKModeTesting; + [_userDefaults setObject:[NSNumber numberWithInt:_sdkMode] + forKey:kFIRIAMUserDefaultKeyForSDKMode]; + + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM290004", + @"Test mode enabled, notifying test mode listener."); + [self.testingModeListener testingModeSwitchedOn]; +} + +// returns the current SDK mode +- (FIRIAMSDKMode)currentMode { + return _sdkMode; +} +@end diff --git a/Firebase/InAppMessaging/Runtime/FIRIAMSDKRuntimeErrorCodes.h b/Firebase/InAppMessaging/Runtime/FIRIAMSDKRuntimeErrorCodes.h new file mode 100644 index 00000000000..d7f3e2c8df8 --- /dev/null +++ b/Firebase/InAppMessaging/Runtime/FIRIAMSDKRuntimeErrorCodes.h @@ -0,0 +1,25 @@ +/* + * 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 + +typedef NS_ENUM(NSInteger, FIRIAMSDKRuntimeError) { + // fail to crawl the image url + FIRIAMSDKRuntimeErrorImageNotFetchable = 0, + + // crawling image url sees non-image type data being returned + FIRIAMSDKRuntimeErrorNonImageMimetypeFromImageURL = 1 +}; diff --git a/Firebase/InAppMessaging/Runtime/FIRIAMSDKSettings.h b/Firebase/InAppMessaging/Runtime/FIRIAMSDKSettings.h new file mode 100644 index 00000000000..63aecea9f8a --- /dev/null +++ b/Firebase/InAppMessaging/Runtime/FIRIAMSDKSettings.h @@ -0,0 +1,53 @@ +/* + * 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 + +@class FIRIAMClearcutStrategy; + +NS_ASSUME_NONNULL_BEGIN +@interface FIRIAMSDKSettings : NSObject +// settings related to communicating with in-app messaging server +@property(nonatomic, copy) NSString *firebaseProjectNumber; +@property(nonatomic, copy) NSString *firebaseAppId; +@property(nonatomic, copy) NSString *apiKey; +@property(nonatomic, copy) NSString *apiServerHost; +@property(nonatomic, copy) NSString *apiHttpProtocol; // http or https. It should be always + // https on production. Allow http to + // faciliate testing in non-prod environment +@property(nonatomic) NSTimeInterval fetchMinIntervalInMinutes; + +// settings related to activity logger +@property(nonatomic) NSInteger loggerMaxCountBeforeReduce; +@property(nonatomic) NSInteger loggerSizeAfterReduce; +@property(nonatomic) BOOL loggerInVerboseMode; + +// settings for controlling rendering frequency for messages rendered from app foreground triggers +@property(nonatomic) NSTimeInterval appFGRenderMinIntervalInMinutes; + +// host name for clearcut servers +@property(nonatomic, copy) NSString *clearcutServerHost; +// clearcut strategy +@property(nonatomic, strong) FIRIAMClearcutStrategy *clearcutStrategy; + +// The global flag at whole Firebase level for automatic data collection. On FIAM SDK startup, +// it would be retreived from FIRApp's corresponding setting. +@property(nonatomic, getter=isFirebaseAutoDataCollectionEnabled) + BOOL firebaseAutoDataCollectionEnabled; + +- (NSString *)description; +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Runtime/FIRIAMSDKSettings.m b/Firebase/InAppMessaging/Runtime/FIRIAMSDKSettings.m new file mode 100644 index 00000000000..aa24f4f93c8 --- /dev/null +++ b/Firebase/InAppMessaging/Runtime/FIRIAMSDKSettings.m @@ -0,0 +1,35 @@ +/* + * 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 "FIRIAMSDKSettings.h" + +@implementation FIRIAMSDKSettings + +- (NSString *)description { + return + [NSString stringWithFormat:@"APIServer:%@;ProjectNumber:%@; API_Key:%@;Clearcut Server:%@; " + "Fetch Minimal Interval:%lu seconds; Activity Logger Max:%lu; " + "Foreground Display Trigger Minimal Interval:%lu seconds;\n" + "Clearcut strategy:%@;Global Firebase auto data collection %@\n", + self.apiServerHost, self.firebaseProjectNumber, self.apiKey, + self.clearcutServerHost, + (unsigned long)(self.fetchMinIntervalInMinutes * 60), + (unsigned long)self.loggerMaxCountBeforeReduce, + (unsigned long)(self.appFGRenderMinIntervalInMinutes * 60), + self.clearcutStrategy, + self.firebaseAutoDataCollectionEnabled ? @"enabled" : @"disabled"]; +} +@end diff --git a/Firebase/InAppMessaging/Runtime/FIRInAppMessaging+Bootstrap.h b/Firebase/InAppMessaging/Runtime/FIRInAppMessaging+Bootstrap.h new file mode 100644 index 00000000000..e2c51390ec1 --- /dev/null +++ b/Firebase/InAppMessaging/Runtime/FIRInAppMessaging+Bootstrap.h @@ -0,0 +1,33 @@ +/* + * 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 "FIRIAMSDKSettings.h" +#import "FIRInAppMessaging.h" + +/** + * This category extends FIRInAppMessaging with the configurations from FIRApp + */ +@interface FIRInAppMessaging (Bootstrap) + ++ (NSString *)getFiamServerHost; ++ (void)setFiamServerHostWithName:(NSString *)serverHost; + ++ (NSString *)getServer; + ++ (void)bootstrapIAMWithSettings:(FIRIAMSDKSettings *)settings; + ++ (void)bootstrapIAMFromFIRApp:(FIRApp *)app; +@end diff --git a/Firebase/InAppMessaging/Runtime/FIRInAppMessaging+Bootstrap.m b/Firebase/InAppMessaging/Runtime/FIRInAppMessaging+Bootstrap.m new file mode 100644 index 00000000000..1f3134adb44 --- /dev/null +++ b/Firebase/InAppMessaging/Runtime/FIRInAppMessaging+Bootstrap.m @@ -0,0 +1,137 @@ +/* + * 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 "FIRInAppMessaging+Bootstrap.h" + +#import +#import +#import +#import +#import + +#import "FIRCore+InAppMessaging.h" +#import "FIRIAMClearcutUploader.h" +#import "FIRIAMRuntimeManager.h" +#import "FIRIAMSDKSettings.h" +#import "FIROptionsInternal.h" +#import "NSString+FIRInterlaceStrings.h" + +@implementation FIRInAppMessaging (Bootstrap) + +static FIRIAMSDKSettings *_sdkSetting = nil; + +static NSString *_fiamServerHostName = @"firebaseinappmessaging.googleapis.com"; + ++ (NSString *)getFiamServerHost { + return _fiamServerHostName; +} + ++ (void)setFiamServerHostWithName:(NSString *)serverHost { + _fiamServerHostName = serverHost; +} + ++ (NSString *)getServer { + // Override to change to test server. + NSString *serverHostNameFirstComponent = @"pa.ogepscm"; + NSString *serverHostNameSecondComponent = @"lygolai.o"; + return [NSString fir_interlaceString:serverHostNameFirstComponent + withString:serverHostNameSecondComponent]; +} + ++ (void)bootstrapIAMFromFIRApp:(FIRApp *)app { + FIROptions *options = app.options; + NSError *error; + + if (!options.GCMSenderID.length) { + error = + [NSError errorWithDomain:kFirebaseInAppMessagingErrorDomain + code:0 + userInfo:@{ + NSLocalizedDescriptionKey : @"Google Sender ID must not be nil or empty." + }]; + + [self exitAppWithFatalError:error]; + } + + if (!options.APIKey.length) { + error = [NSError + errorWithDomain:kFirebaseInAppMessagingErrorDomain + code:0 + userInfo:@{NSLocalizedDescriptionKey : @"API key must not be nil or empty."}]; + + [self exitAppWithFatalError:error]; + } + + if (!options.googleAppID.length) { + error = + [NSError errorWithDomain:kFirebaseInAppMessagingErrorDomain + code:0 + userInfo:@{NSLocalizedDescriptionKey : @"Google App ID must not be nil."}]; + [self exitAppWithFatalError:error]; + } + + // following are the default sdk settings to be used by hosting app + _sdkSetting = [[FIRIAMSDKSettings alloc] init]; + _sdkSetting.apiServerHost = [FIRInAppMessaging getFiamServerHost]; + _sdkSetting.clearcutServerHost = [FIRInAppMessaging getServer]; + _sdkSetting.apiHttpProtocol = @"https"; + _sdkSetting.firebaseAppId = options.googleAppID; + _sdkSetting.firebaseProjectNumber = options.GCMSenderID; + _sdkSetting.apiKey = options.APIKey; + _sdkSetting.fetchMinIntervalInMinutes = 24 * 60; // fetch at most once every 24 hours + _sdkSetting.loggerMaxCountBeforeReduce = 100; + _sdkSetting.loggerSizeAfterReduce = 50; + _sdkSetting.appFGRenderMinIntervalInMinutes = 24 * 60; // render at most one message from + // app-foreground trigger every 24 hours + _sdkSetting.loggerInVerboseMode = NO; + + // TODO: once Firebase Core supports sending notifications at global Firebase level setting + // change, FIAM SDK would listen to it and respond to it. Until then, FIAM SDK only checks + // the setting once upon App/SDK startup. + _sdkSetting.firebaseAutoDataCollectionEnabled = app.isDataCollectionDefaultEnabled; + + if ([GULAppEnvironmentUtil isSimulator]) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM170004", + @"Running in simulator. Do realtime clearcut uploading."); + _sdkSetting.clearcutStrategy = + [[FIRIAMClearcutStrategy alloc] initWithMinWaitTimeInMills:0 + maxWaitTimeInMills:0 + failureBackoffTimeInMills:60 * 60 * 1000 // 60 mins + batchSendSize:50]; + } else { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM170005", + @"Not running in simulator. Use regular clearcut uploading strategy."); + _sdkSetting.clearcutStrategy = + [[FIRIAMClearcutStrategy alloc] initWithMinWaitTimeInMills:5 * 60 * 1000 // 5 mins + maxWaitTimeInMills:12 * 60 * 60 * 1000 // 12 hours + failureBackoffTimeInMills:60 * 60 * 1000 // 60 mins + batchSendSize:50]; + } + + [[FIRIAMRuntimeManager getSDKRuntimeInstance] startRuntimeWithSDKSettings:_sdkSetting]; +} + ++ (void)bootstrapIAMWithSettings:(FIRIAMSDKSettings *)settings { + _sdkSetting = settings; + [[FIRIAMRuntimeManager getSDKRuntimeInstance] startRuntimeWithSDKSettings:_sdkSetting]; +} + ++ (void)exitAppWithFatalError:(NSError *)error { + [NSException raise:kFirebaseInAppMessagingErrorDomain + format:@"Error happened %@", error.localizedDescription]; +} + +@end diff --git a/Firebase/InAppMessaging/Util/FIRIAMElapsedTimeTracker.h b/Firebase/InAppMessaging/Util/FIRIAMElapsedTimeTracker.h new file mode 100644 index 00000000000..02a48bf0b95 --- /dev/null +++ b/Firebase/InAppMessaging/Util/FIRIAMElapsedTimeTracker.h @@ -0,0 +1,29 @@ +/* + * 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 +#import "FIRIAMTimeFetcher.h" + +NS_ASSUME_NONNULL_BEGIN +// A class via which we can track elapsed time with the capability to pause and resume +// the tracking +@interface FIRIAMElapsedTimeTracker : NSObject +- (NSTimeInterval)trackedTimeSoFar; +- (void)pause; +- (void)resume; +- (instancetype)initWithTimeFetcher:(id)timeFetcher; +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Util/FIRIAMElapsedTimeTracker.m b/Firebase/InAppMessaging/Util/FIRIAMElapsedTimeTracker.m new file mode 100644 index 00000000000..79ec0dfdbbd --- /dev/null +++ b/Firebase/InAppMessaging/Util/FIRIAMElapsedTimeTracker.m @@ -0,0 +1,56 @@ +/* + * 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 "FIRIAMElapsedTimeTracker.h" +@interface FIRIAMElapsedTimeTracker () +@property(nonatomic) NSTimeInterval totalTrackedTimeSoFar; +@property(nonatomic) NSTimeInterval lastTrackingStartPoint; +@property(nonatomic, nonnull) id timeFetcher; +@property(nonatomic) BOOL tracking; +@end + +@implementation FIRIAMElapsedTimeTracker + +- (NSTimeInterval)trackedTimeSoFar { + if (_tracking) { + return self.totalTrackedTimeSoFar + [self.timeFetcher currentTimestampInSeconds] - + self.lastTrackingStartPoint; + } else { + return self.totalTrackedTimeSoFar; + } +} + +- (void)pause { + self.tracking = NO; + self.totalTrackedTimeSoFar += + [self.timeFetcher currentTimestampInSeconds] - self.lastTrackingStartPoint; +} + +- (void)resume { + self.tracking = YES; + self.lastTrackingStartPoint = [self.timeFetcher currentTimestampInSeconds]; +} + +- (instancetype)initWithTimeFetcher:(id)timeFetcher { + if (self = [super init]) { + _tracking = YES; + _timeFetcher = timeFetcher; + _totalTrackedTimeSoFar = 0; + _lastTrackingStartPoint = [timeFetcher currentTimestampInSeconds]; + } + return self; +} +@end diff --git a/Firebase/InAppMessaging/Util/FIRIAMTimeFetcher.h b/Firebase/InAppMessaging/Util/FIRIAMTimeFetcher.h new file mode 100644 index 00000000000..eacab44762e --- /dev/null +++ b/Firebase/InAppMessaging/Util/FIRIAMTimeFetcher.h @@ -0,0 +1,28 @@ +/* + * 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 + +NS_ASSUME_NONNULL_BEGIN +// A protocol wrapping around function of getting timestamp. Created to help +// unit testing in which we need to control the elapsed time. +@protocol FIRIAMTimeFetcher +- (NSTimeInterval)currentTimestampInSeconds; +@end + +@interface FIRIAMTimerWithNSDate : NSObject +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Util/FIRIAMTimeFetcher.m b/Firebase/InAppMessaging/Util/FIRIAMTimeFetcher.m new file mode 100644 index 00000000000..d32f5b24c2e --- /dev/null +++ b/Firebase/InAppMessaging/Util/FIRIAMTimeFetcher.m @@ -0,0 +1,23 @@ +/* + * 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 "FIRIAMTimeFetcher.h" + +@implementation FIRIAMTimerWithNSDate +- (NSTimeInterval)currentTimestampInSeconds { + return [[NSDate date] timeIntervalSince1970]; +} +@end diff --git a/Firebase/InAppMessaging/Util/NSString+FIRInterlaceStrings.h b/Firebase/InAppMessaging/Util/NSString+FIRInterlaceStrings.h new file mode 100644 index 00000000000..70025110a08 --- /dev/null +++ b/Firebase/InAppMessaging/Util/NSString+FIRInterlaceStrings.h @@ -0,0 +1,30 @@ +/* + * Copyright 2019 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 + +NS_ASSUME_NONNULL_BEGIN + +// Extension on NSString that combines two strings. +@interface NSString (FIRInterlaceStrings) + +// Returns a combined string created from iterating over both strings alternately, +// beginning with stringOne's first character. ++ (NSString *)fir_interlaceString:(NSString *)stringOne withString:(NSString *)stringTwo; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Util/NSString+FIRInterlaceStrings.m b/Firebase/InAppMessaging/Util/NSString+FIRInterlaceStrings.m new file mode 100644 index 00000000000..ddd1aa1e93c --- /dev/null +++ b/Firebase/InAppMessaging/Util/NSString+FIRInterlaceStrings.m @@ -0,0 +1,42 @@ +/* + * Copyright 2019 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 "NSString+FIRInterlaceStrings.h" + +@implementation NSString (InterlaceStrings) + ++ (NSString *)fir_interlaceString:(NSString *)stringOne withString:(NSString *)stringTwo { + NSMutableString *interlacedString = [NSMutableString string]; + + NSUInteger count = MAX(stringOne.length, stringTwo.length); + + for (NSUInteger i = 0; i < count; i++) { + if (i < stringOne.length) { + NSString *firstComponentChar = + [NSString stringWithFormat:@"%c", [stringOne characterAtIndex:i]]; + [interlacedString appendString:firstComponentChar]; + } + if (i < stringTwo.length) { + NSString *secondComponentChar = + [NSString stringWithFormat:@"%c", [stringTwo characterAtIndex:i]]; + [interlacedString appendString:secondComponentChar]; + } + } + + return interlacedString; +} + +@end diff --git a/Firebase/InAppMessaging/Util/UIColor+FIRIAMHexString.h b/Firebase/InAppMessaging/Util/UIColor+FIRIAMHexString.h new file mode 100644 index 00000000000..e7d1feebe54 --- /dev/null +++ b/Firebase/InAppMessaging/Util/UIColor+FIRIAMHexString.h @@ -0,0 +1,31 @@ +/* + * 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 +#import + +// Extension on UIColor to support conversion from a color hex string in the format +// of #XXXXXX +@interface UIColor (HexString) + +// Constructing UIColor object from a string with '#XXXXXX' format where 'XXXXXX' is +// the 6-digit hex value string of the rgb color. +// +// @param hexString hex string for the color. +// @return a UIColor parsed out of the hex string. Nil returned if the hexString is nil or does +// not conform the desired format. ++ (nullable UIColor *)firiam_colorWithHexString:(nullable NSString *)hexString; +@end diff --git a/Firebase/InAppMessaging/Util/UIColor+FIRIAMHexString.m b/Firebase/InAppMessaging/Util/UIColor+FIRIAMHexString.m new file mode 100644 index 00000000000..7768d646c93 --- /dev/null +++ b/Firebase/InAppMessaging/Util/UIColor+FIRIAMHexString.m @@ -0,0 +1,39 @@ +/* + * 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 "UIColor+FIRIAMHexString.h" + +@implementation UIColor (HexString) ++ (UIColor *)firiam_colorWithHexString:(nullable NSString *)hexString { + if (hexString.length < 7) { + return nil; + } + + unsigned rgbValue = 0; + NSScanner *scanner = [NSScanner scannerWithString:hexString]; + [scanner setScanLocation:1]; // bypass '#' character + + if (![scanner scanHexInt:&rgbValue]) { + // no valid heximal value is detected + return nil; + } + + return [UIColor colorWithRed:((rgbValue & 0xFF0000) >> 16) / 255.0 + green:((rgbValue & 0xFF00) >> 8) / 255.0 + blue:(rgbValue & 0xFF) / 255.0 + alpha:1.0]; +} +@end diff --git a/Firebase/InAppMessaging/firebase_28dp.png b/Firebase/InAppMessaging/firebase_28dp.png new file mode 100644 index 0000000000000000000000000000000000000000..51a87f47575265b41b68bdb7691e9b64e33069b9 GIT binary patch literal 1428 zcmV;F1#9|=P)fz^Idj(bFxt z2^^`RzXZ=y?gJSntX$U|+F-Bf0e~ zz`*N%7&~Lk4cz!y2lT$0z>##n00^XV1X%A(!QFaBNWU!rV|Dv~ir_s??ZDl98h;`h z+g0*!Mx9M0`vW*wPVd0+ISjy912B5Zv<>Ha(6}SDuKiFjtR#+vJT_cOr#7zL4_yY} z({IcEB?TBuKoWQJDIuhQwx)>Fqxf?Pu2V0ZKXP(UWdutfIWZ<}akvzK=auxhu0ukG zD;dsPYvjmFR>X;6%mElVAzMbUc%BB|34njz^=l1u@?|22>oNl8DS&8y4HLa_7`Tok zT&IqI9)>F@S=`-9fLRBCuL@x4L!*Ykx~AQ5B(WPtSpVupSo{7E{+@&*>BuK9W63F| z;AYDJ=E@1sso^b}o!Sk@br>M>4FW&YL-48*B0m{`Yfr+FbmGf)=q*YXC+6SxnzRJ6 zayW9m#b^VpUo`N*)k_APqEVQlh~tw7;3)`zx7s9&n|Tdjp}s}K1~vQTa2!r1a#g?y z(Lv}6K6YJ?Q%u5<;6!5#uBmc39$3d}2kdz{EskXK=TscYS~DWQ!{_J(j%z=N=jK6d z5}Q&EM>1clO_So<=HPFt)8NRp7rwF$cOjoo@C!YJf7AijhQKXce6nhH+)OEdj#j{i zbU4zIo2>>(9LZ{PK7_v2Llj~BW9b8lwi@EQh%=r9uq5t6Q;Kd?&Cbz z7_53UoB&k6Nujr3C$1wgJt^GU*5n=wM>2oXuX4dSO+tgF-Z;nGgy~66j#6N9eC+Bs zDQ;AqJ9Y)QP;Xq*r=J|+qC*PcNao%kMdMHpI9%DZ9T#uI>+X^+ZJ{VKx#TRKSMLpG zV5}`ETb>RwJ)Js*B84MasHfB{7&dv!!%y#`Mw1gWf0|JdSQtp&&n2d(!Q{mFs(tDK zyN^D=#eP9pvkFUr98S$9#htg4-vc6!YLm3AK9pezs%s|#N4e= z{-zt;&qJ&=!;|A<#s7O;*(L>Vjf3kt!cQ-wlOs5ixi?ACm=wGRt#L-L=Vg9!$ysbv z?(K^>Q%0>`)_5t9!;##3N4X}Av|8w_nVc9Un=~|ZhK)~GWDPDpWr4McW6Cw@eNTyT zg(;!Kw@ZPW&K!>W%L?n`W>|I?(ir7GZMyHVn*xlTjj*MfHCLa$F)9|5{ayUlogTCj+0 zd&Wh6vuqVek?FvGIS|?q9JF8&+0x=Rf6rinC 'Apache', :file => 'LICENSE' } + s.authors = 'Google, Inc.' + + s.source = { + :git => 'https://github.com/firebase/firebase-ios-sdk.git', + :tag => s.version.to_s + } + s.social_media_url = 'https://twitter.com/Firebase' + s.ios.deployment_target = '8.0' + + s.cocoapods_version = '>= 1.4.0' + s.static_framework = true + s.prefix_header_file = false + + base_dir = "Firebase/InAppMessaging/" + s.source_files = base_dir + '**/*.[mh]' + s.public_header_files = base_dir + 'Public/*.h' + + s.pod_target_xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' => + '$(inherited) ' + + 'FIRInAppMessaging_LIB_VERSION=' + String(s.version) + } + + s.dependency 'FirebaseCore' + s.ios.dependency 'FirebaseAnalytics' + s.ios.dependency 'FirebaseAnalyticsInterop' + s.dependency 'FirebaseInstanceID' +end diff --git a/InAppMessaging/Example/App/InAppMessaging-Example-iOS/AppDelegate.h b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/AppDelegate.h new file mode 100644 index 00000000000..fbd9afa1834 --- /dev/null +++ b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/AppDelegate.h @@ -0,0 +1,36 @@ +/* + * 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 + +#import "FIRIAMBookKeeper.h" +#import "FIRIAMMsgFetcherUsingRestful.h" + +NS_ASSUME_NONNULL_BEGIN +@interface AppDelegate : UIResponder +@property(strong, nonatomic) UIWindow *window; +@property(strong, nonatomic) FIRIAMActivityLogger *activityLogger; +@property(nonatomic, nullable) FIRIAMBookKeeperViaUserDefaults *bookKeeper; + +@property(nonatomic, nullable) FIRIAMMsgFetcherUsingRestful *restfulFetcher; +@property(nonatomic, copy) NSString *backendServer; +@property(nonatomic, copy) NSString *projectId; +@property(nonatomic, copy) NSString *apiKey; + +@property(nonatomic) NSInteger displayIntervalInSeconds; +@property(nonatomic) NSInteger fetchIntervalInSeconds; +@end +NS_ASSUME_NONNULL_END diff --git a/InAppMessaging/Example/App/InAppMessaging-Example-iOS/AppDelegate.m b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/AppDelegate.m new file mode 100644 index 00000000000..477f3000369 --- /dev/null +++ b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/AppDelegate.m @@ -0,0 +1,118 @@ +/* + * 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 "AppDelegate.h" +#import "FIRIAMClearcutUploader.h" +#import "FIRIAMRuntimeManager.h" +#import "FIRInAppMessaging+Bootstrap.h" +#import "NSString+FIRInterlaceStrings.h" + +#import +#import + +@interface FIRInAppMessaging (Testing) ++ (void)disableAutoBootstrapWithFIRApp; +@end + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + // Override point for customization after application launch. + NSLog(@"application started"); + + [FIRInAppMessaging disableAutoBootstrapWithFIRApp]; + [FIROptions defaultOptions].deepLinkURLScheme = @"fiam-testing"; + [FIRApp configure]; + + FIRIAMSDKSettings *sdkSetting = [[FIRIAMSDKSettings alloc] init]; + + sdkSetting.apiServerHost = @"firebaseinappmessaging.googleapis.com"; + + NSString *serverHostNameFirstComponent = @"pa.ogepscm"; + NSString *serverHostNameSecondComponent = @"lygolai.o"; + + sdkSetting.clearcutServerHost = [NSString fir_interlaceString:serverHostNameFirstComponent + withString:serverHostNameSecondComponent]; + sdkSetting.apiHttpProtocol = @"https"; + sdkSetting.fetchMinIntervalInMinutes = 0.1; // ok to refetch every 6 seconds + sdkSetting.loggerMaxCountBeforeReduce = 800; + sdkSetting.loggerSizeAfterReduce = 600; + sdkSetting.appFGRenderMinIntervalInMinutes = 0.1; + sdkSetting.loggerInVerboseMode = YES; + sdkSetting.conversionTrackingExpiresInSeconds = 180; + sdkSetting.firebaseAutoDataCollectionEnabled = NO; + + sdkSetting.clearcutStrategy = + [[FIRIAMClearcutStrategy alloc] initWithMinWaitTimeInMills:5 * 1000 // 5 seconds + maxWaitTimeInMills:30 * 1000 // 30 seconds + failureBackoffTimeInMills:60 * 1000 // 60 seconds + batchSendSize:50]; + + [FIRInAppMessaging bootstrapIAMWithSettings:sdkSetting]; + return YES; +} + +- (BOOL)application:(UIApplication *)application + continueUserActivity:(NSUserActivity *)userActivity + restorationHandler:(void (^)(NSArray *))restorationHandler { + NSLog(@"handle page url %@", userActivity.webpageURL); + BOOL handled = [[FIRDynamicLinks dynamicLinks] + handleUniversalLink:userActivity.webpageURL + completion:^(FIRDynamicLink *_Nullable dynamicLink, NSError *_Nullable error) { + if (dynamicLink) { + NSLog(@"dynamic link recogized with url as %@", dynamicLink.url.absoluteString); + [self showDeepLink:dynamicLink.url.absoluteString forUrlType:@"universal link"]; + } else { + NSLog(@"error happened %@", error); + } + }]; + return handled; +} + +- (void)showDeepLink:(NSString *)url forUrlType:(NSString *)urlType { + NSString *message = [NSString stringWithFormat:@"App wants to open a %@ : %@", urlType, url]; + UIAlertController *alert = + [UIAlertController alertControllerWithTitle:@"Deep link recognized" + message:message + preferredStyle:UIAlertControllerStyleAlert]; + + UIAlertAction *defaultAction = [UIAlertAction actionWithTitle:@"OK" + style:UIAlertActionStyleDefault + handler:^(UIAlertAction *action){ + }]; + + [alert addAction:defaultAction]; + [UIApplication.sharedApplication.keyWindow.rootViewController presentViewController:alert + animated:YES + completion:nil]; +} + +- (BOOL)application:(UIApplication *)app + openURL:(NSURL *)url + options:(NSDictionary *)options { + return [self application:app openURL:url sourceApplication:@"source app" annotation:@{}]; +} + +- (BOOL)application:(UIApplication *)application + openURL:(NSURL *)url + sourceApplication:(NSString *)sourceApplication + annotation:(id)annotation { + NSLog(@"handle link with custom scheme: %@", url.absoluteString); + [self showDeepLink:url.absoluteString forUrlType:@"custom scheme url"]; + return YES; +} +@end diff --git a/InAppMessaging/Example/App/InAppMessaging-Example-iOS/Assets.xcassets/AppIcon.appiconset/Contents.json b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000000..1d060ed2882 --- /dev/null +++ b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,93 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "83.5x83.5", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/InAppMessaging/Example/App/InAppMessaging-Example-iOS/AutoDisplayFlowViewController.h b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/AutoDisplayFlowViewController.h new file mode 100644 index 00000000000..78cc22bc3c2 --- /dev/null +++ b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/AutoDisplayFlowViewController.h @@ -0,0 +1,20 @@ +/* + * 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 + +@interface AutoDisplayFlowViewController : UIViewController + +@end diff --git a/InAppMessaging/Example/App/InAppMessaging-Example-iOS/AutoDisplayFlowViewController.m b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/AutoDisplayFlowViewController.m new file mode 100644 index 00000000000..6ed900259c2 --- /dev/null +++ b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/AutoDisplayFlowViewController.m @@ -0,0 +1,170 @@ +/* + * 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 "AppDelegate.h" + +#import "AutoDisplayFlowViewController.h" +#import "AutoDisplayMesagesTableVC.h" + +#import "FIRIAMDisplayCheckOnAppForegroundFlow.h" +#import "FIRIAMMessageClientCache.h" +#import "FIRIAMMessageContentDataWithImageURL.h" +#import "FIRIAMMessageDefinition.h" + +#import "FIRIAMActivityLogger.h" +#import "FIRIAMDisplayCheckOnAnalyticEventsFlow.h" +#import "FIRIAMFetchOnAppForegroundFlow.h" +#import "FIRIAMMessageClientCache.h" +#import "FIRIAMMsgFetcherUsingRestful.h" + +#import "FIRIAMRuntimeManager.h" +#import "FIRInAppMessaging.h" + +#import + +@interface AutoDisplayFlowViewController () +@property(weak, nonatomic) IBOutlet UISwitch *autoDisplayFlowSwitch; + +@property(nonatomic, weak) AutoDisplayMesagesTableVC *messageTableVC; +@property(weak, nonatomic) IBOutlet UITextField *autoDisplayIntervalText; +@property(weak, nonatomic) IBOutlet UITextField *autoFetchIntervalText; +@property(weak, nonatomic) IBOutlet UITextField *eventNameText; +@property(nonatomic) FIRIAMRuntimeManager *sdkRuntime; +@property(weak, nonatomic) IBOutlet UIButton *disableEnableSDKBtn; +@property(weak, nonatomic) IBOutlet UIButton *changeDataCollectionBtn; +@end + +@implementation AutoDisplayFlowViewController +- (IBAction)clearClientStorage:(id)sender { + [self.sdkRuntime.fetchResultStorage + saveResponseDictionary:@{} + withCompletion:^(BOOL success) { + [self.sdkRuntime.messageCache + loadMessageDataFromServerFetchStorage:self.sdkRuntime.fetchResultStorage + withCompletion:^(BOOL success) { + NSLog(@"load from storage result is %d", success); + }]; + }]; +} +- (IBAction)disableEnableClicked:(id)sender { + FIRInAppMessaging *sdk = [FIRInAppMessaging inAppMessaging]; + sdk.messageDisplaySuppressed = !sdk.messageDisplaySuppressed; + [self setupDisableEnableButtonLabel]; +} + +- (void)setupDisableEnableButtonLabel { + FIRInAppMessaging *sdk = [FIRInAppMessaging inAppMessaging]; + NSString *title = sdk.messageDisplaySuppressed ? @"allow rendering" : @"disallow rendering"; + [self.disableEnableSDKBtn setTitle:title forState:UIControlStateNormal]; +} + +- (IBAction)triggerAnalyticEventTapped:(id)sender { + NSLog(@"triggering an analytics event: %@", self.eventNameText.text); + + [FIRAnalytics logEventWithName:self.eventNameText.text parameters:@{}]; +} + +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { + UITouch *touch = [touches anyObject]; + if (![touch.view isMemberOfClass:[UITextField class]]) { + [touch.view endEditing:YES]; + } +} +- (IBAction)changeAutoDataCollection:(id)sender { + FIRInAppMessaging *sdk = [FIRInAppMessaging inAppMessaging]; + sdk.automaticDataCollectionEnabled = !sdk.automaticDataCollectionEnabled; + [self setupChangeAutoDataCollectionButtonLabel]; +} + +- (void)setupChangeAutoDataCollectionButtonLabel { + FIRInAppMessaging *sdk = [FIRInAppMessaging inAppMessaging]; + NSString *title = sdk.automaticDataCollectionEnabled ? @"disable data-col" : @"enable data-col"; + [self.changeDataCollectionBtn setTitle:title forState:UIControlStateNormal]; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + + double delayInSeconds = 2.0; + dispatch_time_t setupTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC); + dispatch_after(setupTime, dispatch_get_main_queue(), ^(void) { + // code to be executed on the main queue after delay + self.sdkRuntime = [FIRIAMRuntimeManager getSDKRuntimeInstance]; + self.messageTableVC.messageCache = self.sdkRuntime.messageCache; + [self.sdkRuntime.messageCache setDataObserver:self.messageTableVC]; + [self.messageTableVC.tableView reloadData]; + [self setupDisableEnableButtonLabel]; + [self setupChangeAutoDataCollectionButtonLabel]; + }); + + NSLog(@"done with set data observer"); + + self.autoFetchIntervalText.text = [[NSNumber + numberWithDouble:self.sdkRuntime.currentSetting.fetchMinIntervalInMinutes * 60] stringValue]; + self.autoDisplayIntervalText.text = + [[NSNumber numberWithDouble:self.sdkRuntime.currentSetting.appFGRenderMinIntervalInMinutes * + 60] stringValue]; +} + +- (IBAction)dumpImpressionsToConsole:(id)sender { + NSArray *impressions = [self.sdkRuntime.bookKeeper getImpressions]; + NSLog(@"impressions are %@", [impressions componentsJoinedByString:@","]); +} +- (IBAction)clearImpressionRecord:(id)sender { + [self.sdkRuntime.bookKeeper cleanupImpressions]; +} + +- (IBAction)changeAutoFetchDisplaySettings:(id)sender { + FIRIAMSDKSettings *setting = self.sdkRuntime.currentSetting; + + // set fetch interval + double intervalValue = self.autoFetchIntervalText.text.doubleValue / 60; + if (intervalValue < 0.0001) { + intervalValue = 1; + self.autoFetchIntervalText.text = [[NSNumber numberWithDouble:intervalValue * 60] stringValue]; + } + setting.fetchMinIntervalInMinutes = intervalValue; + + // set app foreground display interval + double displayIntervalValue = self.autoDisplayIntervalText.text.doubleValue / 60; + + if (displayIntervalValue < 0.0001) { + displayIntervalValue = 1; + self.autoDisplayIntervalText.text = + [[NSNumber numberWithDouble:displayIntervalValue * 60] stringValue]; + } + setting.appFGRenderMinIntervalInMinutes = displayIntervalValue; + + [self.sdkRuntime startRuntimeWithSDKSettings:setting]; +} + +- (void)didReceiveMemoryWarning { + [super didReceiveMemoryWarning]; + // Dispose of any resources that can be recreated. +} + +#pragma mark - Navigation +// In a storyboard-based application, you will often want to do a little preparation before +// navigation +- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { + // Get the new view controller using [segue destinationViewController]. + // Pass the selected object to the new view controller. + + if ([segue.identifier isEqualToString:@"message-table-segue"]) { + self.messageTableVC = (AutoDisplayMesagesTableVC *)[segue destinationViewController]; + } +} +@end diff --git a/InAppMessaging/Example/App/InAppMessaging-Example-iOS/AutoDisplayMesagesTableVC.h b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/AutoDisplayMesagesTableVC.h new file mode 100644 index 00000000000..df529553d60 --- /dev/null +++ b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/AutoDisplayMesagesTableVC.h @@ -0,0 +1,23 @@ +/* + * 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 + +#import "FIRIAMMessageClientCache.h" + +@interface AutoDisplayMesagesTableVC : UITableViewController +@property(nonatomic) FIRIAMMessageClientCache *messageCache; +@end diff --git a/InAppMessaging/Example/App/InAppMessaging-Example-iOS/AutoDisplayMesagesTableVC.m b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/AutoDisplayMesagesTableVC.m new file mode 100644 index 00000000000..27e5c667a9a --- /dev/null +++ b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/AutoDisplayMesagesTableVC.m @@ -0,0 +1,137 @@ +/* + * 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 "AutoDisplayMesagesTableVC.h" +#import "FIRIAMDisplayTriggerDefinition.h" +#import "FIRIAMMessageContentData.h" + +@interface AutoDisplayMesagesTableVC () +@end + +@implementation AutoDisplayMesagesTableVC + +- (void)dataChanged { + dispatch_async(dispatch_get_main_queue(), ^{ + [self.tableView reloadData]; + }); +} + +- (void)viewDidLoad { + [super viewDidLoad]; + + // Uncomment the following line to preserve selection between presentations. + // self.clearsSelectionOnViewWillAppear = NO; + + // Uncomment the following line to display an Edit button in the navigation bar for this view + // controller. self.navigationItem.rightBarButtonItem = self.editButtonItem; +} + +- (void)didReceiveMemoryWarning { + [super didReceiveMemoryWarning]; + // Dispose of any resources that can be recreated. +} + +#pragma mark - Table view data source +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { + return 1; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + NSArray *messages = self.messageCache.allRegularMessages; + if (messages) { + return messages.count; + } else { + return 0; + } +} + +static NSString *CellIdentifier = @"CellIdentifier"; + +- (NSString *)viewModeDisplayString:(FIRIAMRenderingMode)viewMode { + switch (viewMode) { + case FIRIAMRenderAsBannerView: + return @"Banner"; + case FIRIAMRenderAsModalView: + return @"Modal"; + case FIRIAMRenderAsImageOnlyView: + return @"Image"; + default: + return @"Unknown"; + } +} + +- (NSString *)triggerDisplayString:(NSArray *)triggers { + NSMutableString *s = [[NSMutableString alloc] init]; + for (FIRIAMDisplayTriggerDefinition *trigger in triggers) { + [s appendString:[self triggerDisplayStringForOneTrigger:trigger]]; + [s appendString:@","]; + } + return [s copy]; +} + +- (NSString *)triggerDisplayStringForOneTrigger: + (FIRIAMDisplayTriggerDefinition *)triggerDefinition { + switch (triggerDefinition.triggerType) { + case FIRIAMRenderTriggerOnAppForeground: + return @"app_foreground"; + case FIRIAMRenderTriggerOnFirebaseAnalyticsEvent: + return triggerDefinition.firebaseEventName; + default: + return @"Unknown"; + } +} +- (UITableViewCell *)tableView:(UITableView *)tableView + cellForRowAtIndexPath:(NSIndexPath *)indexPath { + NSArray *messageDefs = self.messageCache.allRegularMessages; + + NSInteger rowIndex = [indexPath row]; + if (messageDefs.count > rowIndex) { + UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; + + if (cell == nil) { + cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle + reuseIdentifier:CellIdentifier]; + } + + UILabel *titleLabel = (UILabel *)[cell.contentView viewWithTag:10]; + UILabel *modeLabel = (UILabel *)[cell.contentView viewWithTag:20]; + UIImageView *imageView = (UIImageView *)[cell.contentView viewWithTag:30]; + UILabel *triggerLabel = (UILabel *)[cell.contentView viewWithTag:40]; + + titleLabel.text = messageDefs[rowIndex].renderData.contentData.titleText; + modeLabel.text = [self + viewModeDisplayString:messageDefs[rowIndex].renderData.renderingEffectSettings.viewMode]; + + triggerLabel.text = [self triggerDisplayString:messageDefs[rowIndex].renderTriggers]; + + [messageDefs[rowIndex].renderData.contentData + loadImageDataWithBlock:^(NSData *_Nullable imageData, NSError *error) { + if (error) { + NSLog(@"error in loading image: %@", error.localizedDescription); + } else { + UIImage *image = [UIImage imageWithData:imageData]; + dispatch_async(dispatch_get_main_queue(), ^{ + [imageView setImage:image]; + }); + } + }]; + return cell; + } else { + return nil; + } +} + +@end diff --git a/InAppMessaging/Example/App/InAppMessaging-Example-iOS/Base.lproj/LaunchScreen.storyboard b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000000..fdf3f97d1b6 --- /dev/null +++ b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/InAppMessaging/Example/App/InAppMessaging-Example-iOS/Base.lproj/Main.storyboard b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/Base.lproj/Main.storyboard new file mode 100644 index 00000000000..0aa7f4cb7ca --- /dev/null +++ b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/Base.lproj/Main.storyboard @@ -0,0 +1,395 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/InAppMessaging/Example/App/InAppMessaging-Example-iOS/GoogleService-Info.plist b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/GoogleService-Info.plist new file mode 100644 index 00000000000..3f7547fb48d --- /dev/null +++ b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/GoogleService-Info.plist @@ -0,0 +1,28 @@ + + + + + API_KEY + correct_api_key + TRACKING_ID + correct_tracking_id + CLIENT_ID + correct_client_id + REVERSED_CLIENT_ID + correct_reversed_client_id + GOOGLE_APP_ID + 1:123:ios:123abc + GCM_SENDER_ID + correct_gcm_sender_id + PLIST_VERSION + 1 + BUNDLE_ID + com.google.FirebaseSDKTests + PROJECT_ID + abc-xyz-123 + DATABASE_URL + https://abc-xyz-123.firebaseio.com + STORAGE_BUCKET + project-id-123.storage.firebase.com + + diff --git a/InAppMessaging/Example/App/InAppMessaging-Example-iOS/Info.plist b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/Info.plist new file mode 100644 index 00000000000..f49d10287cd --- /dev/null +++ b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/Info.plist @@ -0,0 +1,67 @@ + + + + + FirebaseInAppMessagingAutomaticDataCollectionEnabled + + FirebaseAutomaticDataCollectionEnabled + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0.3 + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + mytesting + CFBundleURLSchemes + + fiam-testing + + + + CFBundleVersion + 1.0.3-1 + LSRequiresIPhoneOS + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/InAppMessaging/Example/App/InAppMessaging-Example-iOS/LogDumpViewController.h b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/LogDumpViewController.h new file mode 100644 index 00000000000..270c6cfb520 --- /dev/null +++ b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/LogDumpViewController.h @@ -0,0 +1,21 @@ +/* + * 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 + +@interface LogDumpViewController : UIViewController + +@end diff --git a/InAppMessaging/Example/App/InAppMessaging-Example-iOS/LogDumpViewController.m b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/LogDumpViewController.m new file mode 100644 index 00000000000..c050cf40763 --- /dev/null +++ b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/LogDumpViewController.m @@ -0,0 +1,73 @@ +/* + * 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 "LogDumpViewController.h" +#import "AppDelegate.h" +#import "FIRIAMRuntimeManager.h" + +@interface LogDumpViewController () +@property(weak, nonatomic) IBOutlet UITextView *logTextView; +@end + +@implementation LogDumpViewController +- (IBAction)dumpImpressList:(id)sender { + NSArray *impressions = [[FIRIAMRuntimeManager getSDKRuntimeInstance].bookKeeper getImpressions]; + NSString *text = [NSString stringWithFormat:@"Message Impression History are :\n%@", + [impressions componentsJoinedByString:@"\n"]]; + self.logTextView.text = text; +} + +- (IBAction)dumActivityLogs:(id)sender { + NSArray *records = + [[FIRIAMRuntimeManager getSDKRuntimeInstance].activityLogger readRecords]; + + NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; + dateFormatter.dateStyle = NSDateFormatterShortStyle; + dateFormatter.timeStyle = NSDateFormatterMediumStyle; + + static NSString *appBuildVersion = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + appBuildVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]; + }); + + NSMutableString *dumpContent = [[NSString + stringWithFormat:@"App Build Version -- %@\n\n" + "SDK Settings -- %@\n\n" + "Activity Logs: %lu records\n\n", + appBuildVersion, [FIRIAMRuntimeManager getSDKRuntimeInstance].currentSetting, + (unsigned long)records.count] mutableCopy]; + + for (FIRIAMActivityRecord *next in records) { + NSString *nextRecordLog = [NSString + stringWithFormat:@"%@, %@, %@, %@\n", [dateFormatter stringFromDate:next.timestamp], + [next displayStringForActivityType], next.success ? @"Success" : @"Failed", + next.detail]; + [dumpContent appendString:nextRecordLog]; + } + self.logTextView.text = dumpContent; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + // Do any additional setup after loading the view. +} + +- (void)didReceiveMemoryWarning { + [super didReceiveMemoryWarning]; + // Dispose of any resources that can be recreated. +} +@end diff --git a/InAppMessaging/Example/App/InAppMessaging-Example-iOS/Podfile b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/Podfile new file mode 100644 index 00000000000..f4396a22c41 --- /dev/null +++ b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/Podfile @@ -0,0 +1,34 @@ + +use_frameworks! + + +target 'InAppMessaging_Example_iOS' do + platform :ios, '8.0' + + pod 'FirebaseCommunity/InAppMessaging', :path => '../..' + # Lock to the 1.0.9 version of InstanceID since 1.0.10 added a dependency + # to FirebaseCore + # pod 'FirebaseInstanceID', '1.0.9' + + #target 'InAppMessaging_Tests_iOS' do + # inherit! :search_paths + # pod 'FirebaseCommunity/InAppMessaging', :path => '../..' + # pod 'OCMock' + #end +end + + +#target 'InAppMessaging_Example_Swift_iOS' do +# platform :ios, '8.0' + +# pod 'FirebaseCommunity/InAppMessaging', :path => '../' + # Lock to the 1.0.9 version of InstanceID since 1.0.10 added a dependency + # to FirebaseCore +# pod 'FirebaseInstanceID', '1.0.9' + + #target 'Messaging_Tests_iOS' do + # inherit! :search_paths + # pod 'OCMock' + #end +#end + diff --git a/InAppMessaging/Example/App/InAppMessaging-Example-iOS/main.m b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/main.m new file mode 100644 index 00000000000..3b3b323ef02 --- /dev/null +++ b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/main.m @@ -0,0 +1,24 @@ +/* + * 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 +#import "AppDelegate.h" + +int main(int argc, char* argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/GoogleService-Info.plist b/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/GoogleService-Info.plist new file mode 100644 index 00000000000..cb4d6f4ac0f --- /dev/null +++ b/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/GoogleService-Info.plist @@ -0,0 +1,28 @@ + + + + + API_KEY + correct_api_key + TRACKING_ID + correct_tracking_id + CLIENT_ID + correct_client_id + REVERSED_CLIENT_ID + correct_reversed_client_id + GOOGLE_APP_ID + 1:123:ios:123abc + GCM_SENDER_ID + correct_gcm_sender_id + PLIST_VERSION + 1 + BUNDLE_ID + com.google.FirebaseSDKTests + PROJECT_ID + abc-xyz-123 + DATABASE_URL + https://abc-xyz-123.firebaseio.com + STORAGE_BUCKET + project-id-123.storage.firebase.com + + diff --git a/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/Podfile b/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/Podfile new file mode 100644 index 00000000000..6e716cb976c --- /dev/null +++ b/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/Podfile @@ -0,0 +1,18 @@ +# Uncomment the next line to define a global platform for your project +platform :ios, '8.4' + +# uncomment the follow two lines if you are trying to test internal releases +#source 'sso://cpdc-internal/spec.git' +#source 'https://github.com/CocoaPods/Specs.git' + +use_frameworks! + +target 'fiam-external-ios-testing-app' do + # Uncomment the next line if you're using Swift or would like to use dynamic frameworks + # use_frameworks! + + # Pods for fiam-external-ios-testing-app + pod 'Firebase/Core' + pod 'Firebase/InAppMessagingDisplay' + pod 'Firebase/DynamicLinks' +end diff --git a/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app.xcodeproj/project.pbxproj b/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app.xcodeproj/project.pbxproj new file mode 100644 index 00000000000..3f40036081f --- /dev/null +++ b/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app.xcodeproj/project.pbxproj @@ -0,0 +1,423 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 48; + objects = { + +/* Begin PBXBuildFile section */ + 7D31D8493943DCD77743922A /* Pods_fiam_external_ios_testing_app.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 12F51E141FEC71BA1CE57DC4 /* Pods_fiam_external_ios_testing_app.framework */; }; + AD7649C71FE1B0A800378AE0 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = AD7649C61FE1B0A800378AE0 /* AppDelegate.m */; }; + AD7649CA1FE1B0A800378AE0 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = AD7649C91FE1B0A800378AE0 /* ViewController.m */; }; + AD7649CD1FE1B0A800378AE0 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AD7649CB1FE1B0A800378AE0 /* Main.storyboard */; }; + AD7649CF1FE1B0A800378AE0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AD7649CE1FE1B0A800378AE0 /* Assets.xcassets */; }; + AD7649D21FE1B0A800378AE0 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AD7649D01FE1B0A800378AE0 /* LaunchScreen.storyboard */; }; + AD7649D51FE1B0A800378AE0 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = AD7649D41FE1B0A800378AE0 /* main.m */; }; + AD7649DC1FE1B57A00378AE0 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = AD7649DB1FE1B57A00378AE0 /* GoogleService-Info.plist */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 12F51E141FEC71BA1CE57DC4 /* Pods_fiam_external_ios_testing_app.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_fiam_external_ios_testing_app.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 61D649CA05E938083C88FC6D /* Pods-fiam-external-ios-testing-app.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-fiam-external-ios-testing-app.debug.xcconfig"; path = "Pods/Target Support Files/Pods-fiam-external-ios-testing-app/Pods-fiam-external-ios-testing-app.debug.xcconfig"; sourceTree = ""; }; + AD7649C21FE1B0A800378AE0 /* fiam-external-ios-testing-app.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "fiam-external-ios-testing-app.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + AD7649C51FE1B0A800378AE0 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + AD7649C61FE1B0A800378AE0 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + AD7649C81FE1B0A800378AE0 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; + AD7649C91FE1B0A800378AE0 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; + AD7649CC1FE1B0A800378AE0 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + AD7649CE1FE1B0A800378AE0 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + AD7649D11FE1B0A800378AE0 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + AD7649D31FE1B0A800378AE0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + AD7649D41FE1B0A800378AE0 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + AD7649DB1FE1B57A00378AE0 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = SOURCE_ROOT; }; + EE0B5FD5B23F372E4894C799 /* Pods-fiam-external-ios-testing-app.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-fiam-external-ios-testing-app.release.xcconfig"; path = "Pods/Target Support Files/Pods-fiam-external-ios-testing-app/Pods-fiam-external-ios-testing-app.release.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + AD7649BF1FE1B0A800378AE0 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 7D31D8493943DCD77743922A /* Pods_fiam_external_ios_testing_app.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 2F924E232047E700385C2AFA /* Frameworks */ = { + isa = PBXGroup; + children = ( + 12F51E141FEC71BA1CE57DC4 /* Pods_fiam_external_ios_testing_app.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 4DC924B9E0562D822E3E68F3 /* Pods */ = { + isa = PBXGroup; + children = ( + 61D649CA05E938083C88FC6D /* Pods-fiam-external-ios-testing-app.debug.xcconfig */, + EE0B5FD5B23F372E4894C799 /* Pods-fiam-external-ios-testing-app.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + AD7649B91FE1B0A800378AE0 = { + isa = PBXGroup; + children = ( + AD7649C41FE1B0A800378AE0 /* fiam-external-ios-testing-app */, + AD7649C31FE1B0A800378AE0 /* Products */, + 4DC924B9E0562D822E3E68F3 /* Pods */, + 2F924E232047E700385C2AFA /* Frameworks */, + ); + sourceTree = ""; + }; + AD7649C31FE1B0A800378AE0 /* Products */ = { + isa = PBXGroup; + children = ( + AD7649C21FE1B0A800378AE0 /* fiam-external-ios-testing-app.app */, + ); + name = Products; + sourceTree = ""; + }; + AD7649C41FE1B0A800378AE0 /* fiam-external-ios-testing-app */ = { + isa = PBXGroup; + children = ( + AD7649DB1FE1B57A00378AE0 /* GoogleService-Info.plist */, + AD7649C51FE1B0A800378AE0 /* AppDelegate.h */, + AD7649C61FE1B0A800378AE0 /* AppDelegate.m */, + AD7649C81FE1B0A800378AE0 /* ViewController.h */, + AD7649C91FE1B0A800378AE0 /* ViewController.m */, + AD7649CB1FE1B0A800378AE0 /* Main.storyboard */, + AD7649CE1FE1B0A800378AE0 /* Assets.xcassets */, + AD7649D01FE1B0A800378AE0 /* LaunchScreen.storyboard */, + AD7649D31FE1B0A800378AE0 /* Info.plist */, + AD7649D41FE1B0A800378AE0 /* main.m */, + ); + path = "fiam-external-ios-testing-app"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + AD7649C11FE1B0A800378AE0 /* fiam-external-ios-testing-app */ = { + isa = PBXNativeTarget; + buildConfigurationList = AD7649D81FE1B0A800378AE0 /* Build configuration list for PBXNativeTarget "fiam-external-ios-testing-app" */; + buildPhases = ( + 89F39EB5CA1632B8B86E938F /* [CP] Check Pods Manifest.lock */, + AD7649BE1FE1B0A800378AE0 /* Sources */, + AD7649BF1FE1B0A800378AE0 /* Frameworks */, + AD7649C01FE1B0A800378AE0 /* Resources */, + AF2C898A9A08823BD10E1650 /* [CP] Embed Pods Frameworks */, + 638CE7E9369C7DEB067D82E7 /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "fiam-external-ios-testing-app"; + productName = "fiam-external-ios-testing-app"; + productReference = AD7649C21FE1B0A800378AE0 /* fiam-external-ios-testing-app.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + AD7649BA1FE1B0A800378AE0 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0910; + ORGANIZATIONNAME = "Yong Mao"; + TargetAttributes = { + AD7649C11FE1B0A800378AE0 = { + CreatedOnToolsVersion = 9.1; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = AD7649BD1FE1B0A800378AE0 /* Build configuration list for PBXProject "fiam-external-ios-testing-app" */; + compatibilityVersion = "Xcode 8.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = AD7649B91FE1B0A800378AE0; + productRefGroup = AD7649C31FE1B0A800378AE0 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + AD7649C11FE1B0A800378AE0 /* fiam-external-ios-testing-app */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + AD7649C01FE1B0A800378AE0 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + AD7649D21FE1B0A800378AE0 /* LaunchScreen.storyboard in Resources */, + AD7649DC1FE1B57A00378AE0 /* GoogleService-Info.plist in Resources */, + AD7649CF1FE1B0A800378AE0 /* Assets.xcassets in Resources */, + AD7649CD1FE1B0A800378AE0 /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 638CE7E9369C7DEB067D82E7 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${SRCROOT}/Pods/Target Support Files/Pods-fiam-external-ios-testing-app/Pods-fiam-external-ios-testing-app-resources.sh", + "${PODS_CONFIGURATION_BUILD_DIR}/FirebaseInAppMessagingDisplay/InAppMessagingDisplayResources.bundle", + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/InAppMessagingDisplayResources.bundle", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-fiam-external-ios-testing-app/Pods-fiam-external-ios-testing-app-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 89F39EB5CA1632B8B86E938F /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-fiam-external-ios-testing-app-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + AF2C898A9A08823BD10E1650 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${SRCROOT}/Pods/Target Support Files/Pods-fiam-external-ios-testing-app/Pods-fiam-external-ios-testing-app-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/GoogleUtilities/GoogleUtilities.framework", + "${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleUtilities.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-fiam-external-ios-testing-app/Pods-fiam-external-ios-testing-app-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + AD7649BE1FE1B0A800378AE0 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + AD7649CA1FE1B0A800378AE0 /* ViewController.m in Sources */, + AD7649D51FE1B0A800378AE0 /* main.m in Sources */, + AD7649C71FE1B0A800378AE0 /* AppDelegate.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + AD7649CB1FE1B0A800378AE0 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + AD7649CC1FE1B0A800378AE0 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + AD7649D01FE1B0A800378AE0 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + AD7649D11FE1B0A800378AE0 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + AD7649D61FE1B0A800378AE0 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.1; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + AD7649D71FE1B0A800378AE0 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.1; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + AD7649D91FE1B0A800378AE0 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61D649CA05E938083C88FC6D /* Pods-fiam-external-ios-testing-app.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = EQHXZ8M8AV; + INFOPLIST_FILE = "fiam-external-ios-testing-app/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 8.4; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.google.fiam-external-ios-testing"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + AD7649DA1FE1B0A800378AE0 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = EE0B5FD5B23F372E4894C799 /* Pods-fiam-external-ios-testing-app.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = EQHXZ8M8AV; + INFOPLIST_FILE = "fiam-external-ios-testing-app/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 8.4; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.google.fiam-external-ios-testing"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + AD7649BD1FE1B0A800378AE0 /* Build configuration list for PBXProject "fiam-external-ios-testing-app" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AD7649D61FE1B0A800378AE0 /* Debug */, + AD7649D71FE1B0A800378AE0 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + AD7649D81FE1B0A800378AE0 /* Build configuration list for PBXNativeTarget "fiam-external-ios-testing-app" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AD7649D91FE1B0A800378AE0 /* Debug */, + AD7649DA1FE1B0A800378AE0 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = AD7649BA1FE1B0A800378AE0 /* Project object */; +} diff --git a/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/AppDelegate.h b/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/AppDelegate.h new file mode 100644 index 00000000000..013891c90b6 --- /dev/null +++ b/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/AppDelegate.h @@ -0,0 +1,21 @@ +// 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 + +@interface AppDelegate : UIResponder + +@property(strong, nonatomic) UIWindow *window; + +@end diff --git a/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/AppDelegate.m b/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/AppDelegate.m new file mode 100644 index 00000000000..f645a71b700 --- /dev/null +++ b/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/AppDelegate.m @@ -0,0 +1,70 @@ +// 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 "AppDelegate.h" + +@import Firebase; + +@interface AppDelegate () + +@end + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + // Override point for customization after application launch. + // uncomment the following line for disabling the auto startup + // of the sdk + // [FIRInAppMessaging inAppMessaging].automaticDataCollectionEnabled = @NO; + + [FIROptions defaultOptions].deepLinkURLScheme = @"com.google.InAppMessagingExampleiOS"; + [FIRApp configure]; + return YES; +} + +- (BOOL)application:(UIApplication *)app + openURL:(NSURL *)url + options:(NSDictionary *)options { + NSLog(@"called here 1"); + return [self application:app + openURL:url + sourceApplication:options[UIApplicationOpenURLOptionsSourceApplicationKey] + annotation:options[UIApplicationOpenURLOptionsAnnotationKey]]; +} + +- (BOOL)application:(UIApplication *)application + openURL:(NSURL *)url + sourceApplication:(NSString *)sourceApplication + annotation:(id)annotation { + FIRDynamicLink *dynamicLink = [[FIRDynamicLinks dynamicLinks] dynamicLinkFromCustomSchemeURL:url]; + + NSLog(@"called here with %@", dynamicLink); + if (dynamicLink) { + if (dynamicLink.url) { + // Handle the deep link. For example, show the deep-linked content, + // apply a promotional offer to the user's account or show customized onboarding view. + // ... + + } else { + // Dynamic link has empty deep link. This situation will happens if + // Firebase Dynamic Links iOS SDK tried to retrieve pending dynamic link, + // but pending link is not available for this device/App combination. + // At this point you may display default onboarding view. + } + return YES; + } + return NO; +} +@end diff --git a/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/Assets.xcassets/AppIcon.appiconset/Contents.json b/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000000..1d060ed2882 --- /dev/null +++ b/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,93 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "83.5x83.5", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/Base.lproj/LaunchScreen.storyboard b/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000000..acde84d5a56 --- /dev/null +++ b/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/Base.lproj/Main.storyboard b/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/Base.lproj/Main.storyboard new file mode 100644 index 00000000000..67cb7ed4c58 --- /dev/null +++ b/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/Base.lproj/Main.storyboard @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/Info.plist b/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/Info.plist new file mode 100644 index 00000000000..15f461a4e6a --- /dev/null +++ b/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/Info.plist @@ -0,0 +1,60 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + fiam-external-ios-testing + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + my-url + CFBundleURLSchemes + + com.google.InAppMessagingExampleiOS + + + + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/ViewController.h b/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/ViewController.h new file mode 100644 index 00000000000..b6115b80707 --- /dev/null +++ b/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/ViewController.h @@ -0,0 +1,19 @@ +// 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 + +@interface ViewController : UIViewController + +@end diff --git a/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/ViewController.m b/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/ViewController.m new file mode 100644 index 00000000000..44335d8292b --- /dev/null +++ b/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/ViewController.m @@ -0,0 +1,39 @@ +// 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 "ViewController.h" + +#import + +@interface ViewController () +@property(weak, nonatomic) IBOutlet UITextField *urlText; + +@end + +@implementation ViewController +- (IBAction)triggerEvent:(id)sender { + [FIRAnalytics logEventWithName:self.urlText.text parameters:@{}]; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + // Do any additional setup after loading the view, typically from a nib. +} + +- (void)didReceiveMemoryWarning { + [super didReceiveMemoryWarning]; + // Dispose of any resources that can be recreated. +} + +@end diff --git a/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/main.m b/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/main.m new file mode 100644 index 00000000000..3b3b323ef02 --- /dev/null +++ b/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/main.m @@ -0,0 +1,24 @@ +/* + * 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 +#import "AppDelegate.h" + +int main(int argc, char* argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/InAppMessaging/Example/FIRIAMSDKModeManagerTests.m b/InAppMessaging/Example/FIRIAMSDKModeManagerTests.m new file mode 100644 index 00000000000..eed680b3136 --- /dev/null +++ b/InAppMessaging/Example/FIRIAMSDKModeManagerTests.m @@ -0,0 +1,137 @@ +/* + * 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 +#import + +#import "FIRIAMSDKModeManager.h" + +@interface FIRIAMSDKModeManagerTests : XCTestCase +@property(nonatomic) NSUserDefaults *mockUserDefaults; +@property(nonatomic) id mockTestingModeListener; +@end + +@implementation FIRIAMSDKModeManagerTests + +- (void)setUp { + [super setUp]; + self.mockUserDefaults = OCMClassMock(NSUserDefaults.class); + self.mockTestingModeListener = OCMStrictProtocolMock(@protocol(FIRIAMTestingModeListener)); +} + +- (void)tearDown { + // Put teardown code here. This method is called after the invocation of each test method in the + // class. + [super tearDown]; +} + +- (void)testFirstRunFromInstall_ok { + // mode entry not existing from a fresh install + OCMStub([self.mockUserDefaults objectForKey:[OCMArg isEqual:kFIRIAMUserDefaultKeyForSDKMode]]) + .andReturn(nil); + FIRIAMSDKModeManager *sdkManager = + [[FIRIAMSDKModeManager alloc] initWithUserDefaults:self.mockUserDefaults + testingModeListener:self.mockTestingModeListener]; + + XCTAssertEqual(FIRIAMSDKModeNewlyInstalled, [sdkManager currentMode]); + + // verify that we setting the mode into use defaults + OCMVerify([self.mockUserDefaults + setObject:[OCMArg isEqual:[NSNumber numberWithInt:FIRIAMSDKModeNewlyInstalled]] + forKey:[OCMArg isEqual:kFIRIAMUserDefaultKeyForSDKMode]]); + + // verify that we are initializing fetch count as 0 by writing into user defaults + OCMVerify([self.mockUserDefaults + setInteger:0 + forKey:[OCMArg isEqual:kFIRIAMUserDefaultKeyForServerFetchCount]]); +} + +- (void)testGoingIntoRegularFromNewlyInstalledMode { + // mode entry not existing from a fresh install + OCMStub([self.mockUserDefaults objectForKey:[OCMArg isEqual:kFIRIAMUserDefaultKeyForSDKMode]]) + .andReturn(nil); + FIRIAMSDKModeManager *sdkManager = + [[FIRIAMSDKModeManager alloc] initWithUserDefaults:self.mockUserDefaults + testingModeListener:self.mockTestingModeListener]; + XCTAssertEqual(FIRIAMSDKModeNewlyInstalled, [sdkManager currentMode]); + + // now we register up to kFIRIAMMaxFetchInNewlyInstalledMode - 1 fetches and it still stay + // in Newly Installed mode + for (int i = 0; i < kFIRIAMMaxFetchInNewlyInstalledMode - 1; i++) { + [sdkManager registerOneMoreFetch]; + } + XCTAssertEqual(FIRIAMSDKModeNewlyInstalled, [sdkManager currentMode]); + + // now one more fetch would turn it into regular mode + [sdkManager registerOneMoreFetch]; + XCTAssertEqual(FIRIAMSDKModeRegular, [sdkManager currentMode]); +} + +- (void)testIncrementCountForFetchRegistrationFromNewlyInstalledMode { + // put sdk into regular mode + OCMStub([self.mockUserDefaults objectForKey:[OCMArg isEqual:kFIRIAMUserDefaultKeyForSDKMode]]) + .andReturn([NSNumber numberWithInt:FIRIAMSDKModeNewlyInstalled]); + + int currentFetchCount = 3; + OCMStub([self.mockUserDefaults + integerForKey:[OCMArg isEqual:kFIRIAMUserDefaultKeyForServerFetchCount]]) + .andReturn(currentFetchCount); + FIRIAMSDKModeManager *sdkManager = + [[FIRIAMSDKModeManager alloc] initWithUserDefaults:self.mockUserDefaults + testingModeListener:self.mockTestingModeListener]; + + // now we do new fetch registeration + [sdkManager registerOneMoreFetch]; + + // verify that we are writing currentFetchCount+1 into user defaults + OCMVerify([self.mockUserDefaults + setInteger:currentFetchCount + 1 + forKey:[OCMArg isEqual:kFIRIAMUserDefaultKeyForServerFetchCount]]); +} + +- (void)testNoUpdateForFetchRegistrationFromRegularMode { + // put sdk into regular mode + OCMStub([self.mockUserDefaults objectForKey:[OCMArg isEqual:kFIRIAMUserDefaultKeyForSDKMode]]) + .andReturn([NSNumber numberWithInt:FIRIAMSDKModeRegular]); + + int currentFetchCount = 3; + OCMStub([self.mockUserDefaults + integerForKey:[OCMArg isEqual:kFIRIAMUserDefaultKeyForServerFetchCount]]) + .andReturn(currentFetchCount); + FIRIAMSDKModeManager *sdkManager = + [[FIRIAMSDKModeManager alloc] initWithUserDefaults:self.mockUserDefaults + testingModeListener:self.mockTestingModeListener]; + + // now we do new fetch registeration, but no more fetch count or mode updates in user defaults + [sdkManager registerOneMoreFetch]; + XCTAssertEqual(FIRIAMSDKModeRegular, [sdkManager currentMode]); + OCMReject([self.mockUserDefaults setInteger:currentFetchCount + 1 forKey:[OCMArg any]]); +} + +- (void)testGoingIntoTestingDeviceMode { + // mode entry not existing from a fresh install + OCMStub([self.mockUserDefaults objectForKey:[OCMArg isEqual:kFIRIAMUserDefaultKeyForSDKMode]]) + .andReturn(nil); + OCMExpect([self.mockTestingModeListener testingModeSwitchedOn]); + FIRIAMSDKModeManager *sdkManager = + [[FIRIAMSDKModeManager alloc] initWithUserDefaults:self.mockUserDefaults + testingModeListener:self.mockTestingModeListener]; + + [sdkManager becomeTestingInstance]; + XCTAssertEqual(FIRIAMSDKModeTesting, [sdkManager currentMode]); + OCMVerify([self.mockTestingModeListener testingModeSwitchedOn]); +} +@end diff --git a/InAppMessaging/Example/InAppMessaging-Example-iOS.xcodeproj/project.pbxproj b/InAppMessaging/Example/InAppMessaging-Example-iOS.xcodeproj/project.pbxproj new file mode 100644 index 00000000000..a2d174fd00f --- /dev/null +++ b/InAppMessaging/Example/InAppMessaging-Example-iOS.xcodeproj/project.pbxproj @@ -0,0 +1,828 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 25C6745121EEC868005A4C23 /* NSString+InterlaceStringsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 25C6745021EEC868005A4C23 /* NSString+InterlaceStringsTests.m */; }; + 5BDD2462D9E03C4678945D2B /* Pods_InAppMessaging_Example_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 16E2417C62DE07F79F99BC3A /* Pods_InAppMessaging_Example_iOS.framework */; }; + AD0E370C1F7C63BC00BBF23C /* FIRIAMMsgFetcherUsingRestfulTests.m in Sources */ = {isa = PBXBuildFile; fileRef = AD0E370B1F7C63BC00BBF23C /* FIRIAMMsgFetcherUsingRestfulTests.m */; }; + AD0E37101F7DB3D500BBF23C /* FIRIAMFetchResponseParserTests.m in Sources */ = {isa = PBXBuildFile; fileRef = AD0E370F1F7DB3D500BBF23C /* FIRIAMFetchResponseParserTests.m */; }; + AD241B0F1F4F582A009A3C22 /* FIRIAMElapsedTimeTrackerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = AD241B0E1F4F582A009A3C22 /* FIRIAMElapsedTimeTrackerTests.m */; }; + AD3868531F95300E00E36BC5 /* FIRIAMMessageClientCacheTests.m in Sources */ = {isa = PBXBuildFile; fileRef = AD3868521F95300E00E36BC5 /* FIRIAMMessageClientCacheTests.m */; }; + AD39268920AB55B700FA3FDF /* JsonDataWithInvalidMessagesFromFetch.txt in Resources */ = {isa = PBXBuildFile; fileRef = AD39268820AB55B700FA3FDF /* JsonDataWithInvalidMessagesFromFetch.txt */; }; + AD39268A20AB56B900FA3FDF /* JsonDataWithInvalidMessagesFromFetch.txt in Resources */ = {isa = PBXBuildFile; fileRef = AD39268820AB55B700FA3FDF /* JsonDataWithInvalidMessagesFromFetch.txt */; }; + AD3EE82A1F4CED0B006829AE /* FIRIAMDisplayExecutorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = AD3EE8291F4CED0B006829AE /* FIRIAMDisplayExecutorTests.m */; }; + AD5D25CB1FA91C9900F1B0EB /* FIRIAMActivityLoggerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = AD5D25CA1FA91C9900F1B0EB /* FIRIAMActivityLoggerTests.m */; }; + AD6A6CB11F56093700A6DFA1 /* FIRIAMBookKeeperViaUserDefaultsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = AD6A6CAE1F5606CD00A6DFA1 /* FIRIAMBookKeeperViaUserDefaultsTests.m */; }; + AD764A341FE4856400378AE0 /* AutoDisplayFlowViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = AD764A2E1FE4856300378AE0 /* AutoDisplayFlowViewController.m */; }; + AD764A361FE4856400378AE0 /* LogDumpViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = AD764A301FE4856300378AE0 /* LogDumpViewController.m */; }; + AD764A371FE4856400378AE0 /* AutoDisplayMesagesTableVC.m in Sources */ = {isa = PBXBuildFile; fileRef = AD764A331FE4856300378AE0 /* AutoDisplayMesagesTableVC.m */; }; + AD811A321F13F88800BF632A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = AD811A311F13F88800BF632A /* main.m */; }; + AD811A351F13F88800BF632A /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = AD811A341F13F88800BF632A /* AppDelegate.m */; }; + AD811A3D1F13F88800BF632A /* Shared.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AD811A3C1F13F88800BF632A /* Shared.xcassets */; }; + AD81223D1F14100700BF632A /* FIRIAMMessageContentDataWithImageURLTests.m in Sources */ = {isa = PBXBuildFile; fileRef = AD8122391F140FFC00BF632A /* FIRIAMMessageContentDataWithImageURLTests.m */; }; + AD8122431F14148100BF632A /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AD81221B1F14064800BF632A /* LaunchScreen.storyboard */; }; + AD8122441F14148100BF632A /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AD8122181F14052100BF632A /* Main.storyboard */; }; + AD95D8EE1FFEFCA700780607 /* FIRIAMActionUrlFollowerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = AD95D8ED1FFEFCA700780607 /* FIRIAMActionUrlFollowerTests.m */; }; + ADA10F98202CC1B0000F4425 /* AdSupport.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ADA10F97202CC1B0000F4425 /* AdSupport.framework */; }; + ADA10F9A202CCBFC000F4425 /* FIRIAMAnalyticsEventLoggerImplTests.m in Sources */ = {isa = PBXBuildFile; fileRef = ADA10F99202CCBFC000F4425 /* FIRIAMAnalyticsEventLoggerImplTests.m */; }; + ADA72B9320282F3B0087E131 /* FIRIAMClearcutUploaderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = ADA72B9220282F3B0087E131 /* FIRIAMClearcutUploaderTests.m */; }; + ADB47ADF20A107C6002D52E9 /* TestJsonDataFromFetch.txt in Resources */ = {isa = PBXBuildFile; fileRef = ADB47ADE20A107C6002D52E9 /* TestJsonDataFromFetch.txt */; }; + ADB47AE020A107F3002D52E9 /* TestJsonDataFromFetch.txt in Resources */ = {isa = PBXBuildFile; fileRef = ADB47ADE20A107C6002D52E9 /* TestJsonDataFromFetch.txt */; }; + ADB47AE220A110F4002D52E9 /* TestJsonDataWithTestMessageFromFetch.txt in Resources */ = {isa = PBXBuildFile; fileRef = ADB47AE120A110F4002D52E9 /* TestJsonDataWithTestMessageFromFetch.txt */; }; + ADB47AE320A111D5002D52E9 /* TestJsonDataWithTestMessageFromFetch.txt in Resources */ = {isa = PBXBuildFile; fileRef = ADB47AE120A110F4002D52E9 /* TestJsonDataWithTestMessageFromFetch.txt */; }; + ADBC527A1F4CD18300A4BEF9 /* FIRIAMFetchFlowTests.m in Sources */ = {isa = PBXBuildFile; fileRef = ADBC52791F4CD18300A4BEF9 /* FIRIAMFetchFlowTests.m */; }; + ADC4298A1F8BEAFD00027599 /* UIColor+FIRIAMHexStringTests.m in Sources */ = {isa = PBXBuildFile; fileRef = ADC429891F8BEAFD00027599 /* UIColor+FIRIAMHexStringTests.m */; }; + ADC4298F1F8D3DD500027599 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = ADC4298E1F8D3DD500027599 /* GoogleService-Info.plist */; }; + ADCC091020782E2D009BFC2F /* FIRIAMSDKModeManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = ADCC090F20782E2D009BFC2F /* FIRIAMSDKModeManagerTests.m */; }; + ADD981962006D1C500944751 /* FIRIAMClearcutLoggerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = ADD981952006D1C500944751 /* FIRIAMClearcutLoggerTests.m */; }; + ADE5BB60200988A1001A1395 /* FIRIAMClearcutRetryLocalStorageTests.m in Sources */ = {isa = PBXBuildFile; fileRef = ADE5BB5F200988A1001A1395 /* FIRIAMClearcutRetryLocalStorageTests.m */; }; + FA7A6267360499E94F504669 /* Pods_InAppMessaging_Tests_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B403D5D166E193EAAB4C3B25 /* Pods_InAppMessaging_Tests_iOS.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + AD8122311F140F9700BF632A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = AD811A251F13F88800BF632A /* Project object */; + proxyType = 1; + remoteGlobalIDString = AD811A2C1F13F88800BF632A; + remoteInfo = InAppMessaging_Example_iOS; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 0AC166835CF567A8E912BD91 /* Pods-InAppMessaging_Example_iOS_Swift.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-InAppMessaging_Example_iOS_Swift.release.xcconfig"; path = "Pods/Target Support Files/Pods-InAppMessaging_Example_iOS_Swift/Pods-InAppMessaging_Example_iOS_Swift.release.xcconfig"; sourceTree = ""; }; + 16E2417C62DE07F79F99BC3A /* Pods_InAppMessaging_Example_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_InAppMessaging_Example_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 25C6745021EEC868005A4C23 /* NSString+InterlaceStringsTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = "NSString+InterlaceStringsTests.m"; path = "Tests/NSString+InterlaceStringsTests.m"; sourceTree = ""; }; + 4A218F594D01B3E92842DF08 /* Pods-InAppMessaging_Tests_iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-InAppMessaging_Tests_iOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-InAppMessaging_Tests_iOS/Pods-InAppMessaging_Tests_iOS.release.xcconfig"; sourceTree = ""; }; + 92F3AF0CAD88D57986578C52 /* Pods-InAppMessaging_Tests_iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-InAppMessaging_Tests_iOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-InAppMessaging_Tests_iOS/Pods-InAppMessaging_Tests_iOS.debug.xcconfig"; sourceTree = ""; }; + 9E53A331D50419C13BE9189E /* Pods_InAppMessaging_Example_iOS_Swift.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_InAppMessaging_Example_iOS_Swift.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + AD00D6E61F26655B00DB4967 /* InAppMessaging_Example_iOS_SwiftUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppMessaging_Example_iOS_SwiftUITests.swift; sourceTree = ""; }; + AD00D6E81F26655B00DB4967 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + AD0E370B1F7C63BC00BBF23C /* FIRIAMMsgFetcherUsingRestfulTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = FIRIAMMsgFetcherUsingRestfulTests.m; path = Tests/FIRIAMMsgFetcherUsingRestfulTests.m; sourceTree = ""; }; + AD0E370F1F7DB3D500BBF23C /* FIRIAMFetchResponseParserTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = FIRIAMFetchResponseParserTests.m; path = Tests/FIRIAMFetchResponseParserTests.m; sourceTree = ""; }; + AD1469821FEC7DD5002051BF /* InAppMessaging_Example_iOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = InAppMessaging_Example_iOS.entitlements; sourceTree = ""; }; + AD241B0E1F4F582A009A3C22 /* FIRIAMElapsedTimeTrackerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = FIRIAMElapsedTimeTrackerTests.m; path = Tests/FIRIAMElapsedTimeTrackerTests.m; sourceTree = ""; }; + AD3868521F95300E00E36BC5 /* FIRIAMMessageClientCacheTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = FIRIAMMessageClientCacheTests.m; path = Tests/FIRIAMMessageClientCacheTests.m; sourceTree = ""; }; + AD39268820AB55B700FA3FDF /* JsonDataWithInvalidMessagesFromFetch.txt */ = {isa = PBXFileReference; lastKnownFileType = text; name = JsonDataWithInvalidMessagesFromFetch.txt; path = Tests/JsonDataWithInvalidMessagesFromFetch.txt; sourceTree = ""; }; + AD3EE8291F4CED0B006829AE /* FIRIAMDisplayExecutorTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = FIRIAMDisplayExecutorTests.m; path = Tests/FIRIAMDisplayExecutorTests.m; sourceTree = ""; }; + AD40FB071F38CCB700AB8C14 /* SnapshotHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SnapshotHelper.swift; path = ../App/InAppMessaging_Example_iOS_Swift/SnapshotHelper.swift; sourceTree = ""; }; + AD5D25CA1FA91C9900F1B0EB /* FIRIAMActivityLoggerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = FIRIAMActivityLoggerTests.m; path = Tests/FIRIAMActivityLoggerTests.m; sourceTree = ""; }; + AD6A6CAE1F5606CD00A6DFA1 /* FIRIAMBookKeeperViaUserDefaultsTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRIAMBookKeeperViaUserDefaultsTests.m; path = Tests/FIRIAMBookKeeperViaUserDefaultsTests.m; sourceTree = ""; }; + AD764A2C1FE4856300378AE0 /* AutoDisplayMesagesTableVC.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AutoDisplayMesagesTableVC.h; path = "App/InAppMessaging-Example-iOS/AutoDisplayMesagesTableVC.h"; sourceTree = ""; }; + AD764A2D1FE4856300378AE0 /* AutoDisplayFlowViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AutoDisplayFlowViewController.h; path = "App/InAppMessaging-Example-iOS/AutoDisplayFlowViewController.h"; sourceTree = ""; }; + AD764A2E1FE4856300378AE0 /* AutoDisplayFlowViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AutoDisplayFlowViewController.m; path = "App/InAppMessaging-Example-iOS/AutoDisplayFlowViewController.m"; sourceTree = ""; }; + AD764A301FE4856300378AE0 /* LogDumpViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = LogDumpViewController.m; path = "App/InAppMessaging-Example-iOS/LogDumpViewController.m"; sourceTree = ""; }; + AD764A311FE4856300378AE0 /* LogDumpViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = LogDumpViewController.h; path = "App/InAppMessaging-Example-iOS/LogDumpViewController.h"; sourceTree = ""; }; + AD764A331FE4856300378AE0 /* AutoDisplayMesagesTableVC.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AutoDisplayMesagesTableVC.m; path = "App/InAppMessaging-Example-iOS/AutoDisplayMesagesTableVC.m"; sourceTree = ""; }; + AD811A2D1F13F88800BF632A /* InAppMessaging_Example_iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = InAppMessaging_Example_iOS.app; sourceTree = BUILT_PRODUCTS_DIR; }; + AD811A311F13F88800BF632A /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = main.m; path = "App/InAppMessaging-Example-iOS/main.m"; sourceTree = ""; }; + AD811A331F13F88800BF632A /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = "App/InAppMessaging-Example-iOS/AppDelegate.h"; sourceTree = ""; }; + AD811A341F13F88800BF632A /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.m; path = "App/InAppMessaging-Example-iOS/AppDelegate.m"; sourceTree = ""; }; + AD811A3C1F13F88800BF632A /* Shared.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Shared.xcassets; path = ../../Example/Shared/Shared.xcassets; sourceTree = ""; }; + AD811A411F13F88800BF632A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = "App/InAppMessaging-Example-iOS/Info.plist"; sourceTree = ""; }; + AD8122191F14052100BF632A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = "App/InAppMessaging-Example-iOS/Base.lproj/Main.storyboard"; sourceTree = ""; }; + AD81221C1F14064800BF632A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = "App/InAppMessaging-Example-iOS/Base.lproj/LaunchScreen.storyboard"; sourceTree = ""; }; + AD81222C1F140F9700BF632A /* InAppMessaging_Tests_iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = InAppMessaging_Tests_iOS.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + AD8122391F140FFC00BF632A /* FIRIAMMessageContentDataWithImageURLTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRIAMMessageContentDataWithImageURLTests.m; path = Tests/FIRIAMMessageContentDataWithImageURLTests.m; sourceTree = ""; }; + AD8122411F1412C500BF632A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Tests/Info.plist; sourceTree = ""; }; + AD95D8ED1FFEFCA700780607 /* FIRIAMActionUrlFollowerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = FIRIAMActionUrlFollowerTests.m; path = Tests/FIRIAMActionUrlFollowerTests.m; sourceTree = ""; }; + ADA10F97202CC1B0000F4425 /* AdSupport.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AdSupport.framework; path = System/Library/Frameworks/AdSupport.framework; sourceTree = SDKROOT; }; + ADA10F99202CCBFC000F4425 /* FIRIAMAnalyticsEventLoggerImplTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = FIRIAMAnalyticsEventLoggerImplTests.m; path = Tests/FIRIAMAnalyticsEventLoggerImplTests.m; sourceTree = ""; }; + ADA72B9220282F3B0087E131 /* FIRIAMClearcutUploaderTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = FIRIAMClearcutUploaderTests.m; path = Tests/FIRIAMClearcutUploaderTests.m; sourceTree = ""; }; + ADB47ADE20A107C6002D52E9 /* TestJsonDataFromFetch.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = TestJsonDataFromFetch.txt; sourceTree = ""; }; + ADB47AE120A110F4002D52E9 /* TestJsonDataWithTestMessageFromFetch.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = TestJsonDataWithTestMessageFromFetch.txt; sourceTree = ""; }; + ADBC52791F4CD18300A4BEF9 /* FIRIAMFetchFlowTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRIAMFetchFlowTests.m; path = Tests/FIRIAMFetchFlowTests.m; sourceTree = ""; }; + ADC429891F8BEAFD00027599 /* UIColor+FIRIAMHexStringTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = "UIColor+FIRIAMHexStringTests.m"; path = "Tests/UIColor+FIRIAMHexStringTests.m"; sourceTree = ""; }; + ADC4298E1F8D3DD500027599 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "App/InAppMessaging-Example-iOS/GoogleService-Info.plist"; sourceTree = ""; }; + ADCC090F20782E2D009BFC2F /* FIRIAMSDKModeManagerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRIAMSDKModeManagerTests.m; sourceTree = ""; }; + ADD981952006D1C500944751 /* FIRIAMClearcutLoggerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = FIRIAMClearcutLoggerTests.m; path = Tests/FIRIAMClearcutLoggerTests.m; sourceTree = ""; }; + ADE5BB5F200988A1001A1395 /* FIRIAMClearcutRetryLocalStorageTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = FIRIAMClearcutRetryLocalStorageTests.m; path = Tests/FIRIAMClearcutRetryLocalStorageTests.m; sourceTree = ""; }; + B08DD02B5CAEC2B9FF562FBC /* Pods_fiam_sample_consuming_app.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_fiam_sample_consuming_app.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + B403D5D166E193EAAB4C3B25 /* Pods_InAppMessaging_Tests_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_InAppMessaging_Tests_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + BF4A969BA440546630E4C931 /* Pods-InAppMessaging_Example_iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-InAppMessaging_Example_iOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-InAppMessaging_Example_iOS/Pods-InAppMessaging_Example_iOS.debug.xcconfig"; sourceTree = ""; }; + E96B40A85DA4991275EC4934 /* Pods-InAppMessaging_Example_iOS_Swift.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-InAppMessaging_Example_iOS_Swift.debug.xcconfig"; path = "Pods/Target Support Files/Pods-InAppMessaging_Example_iOS_Swift/Pods-InAppMessaging_Example_iOS_Swift.debug.xcconfig"; sourceTree = ""; }; + F0C2B7614A637C08CBDB8FF1 /* Pods-InAppMessaging_Example_iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-InAppMessaging_Example_iOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-InAppMessaging_Example_iOS/Pods-InAppMessaging_Example_iOS.release.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + AD811A2A1F13F88800BF632A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ADA10F98202CC1B0000F4425 /* AdSupport.framework in Frameworks */, + 5BDD2462D9E03C4678945D2B /* Pods_InAppMessaging_Example_iOS.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + AD8122291F140F9700BF632A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + FA7A6267360499E94F504669 /* Pods_InAppMessaging_Tests_iOS.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 9F765C4CE4252F7162E20DB1 /* Pods */ = { + isa = PBXGroup; + children = ( + BF4A969BA440546630E4C931 /* Pods-InAppMessaging_Example_iOS.debug.xcconfig */, + F0C2B7614A637C08CBDB8FF1 /* Pods-InAppMessaging_Example_iOS.release.xcconfig */, + E96B40A85DA4991275EC4934 /* Pods-InAppMessaging_Example_iOS_Swift.debug.xcconfig */, + 0AC166835CF567A8E912BD91 /* Pods-InAppMessaging_Example_iOS_Swift.release.xcconfig */, + 92F3AF0CAD88D57986578C52 /* Pods-InAppMessaging_Tests_iOS.debug.xcconfig */, + 4A218F594D01B3E92842DF08 /* Pods-InAppMessaging_Tests_iOS.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + AD00D6E51F26655B00DB4967 /* InAppMessaging_Example_iOS_SwiftUITests */ = { + isa = PBXGroup; + children = ( + AD40FB071F38CCB700AB8C14 /* SnapshotHelper.swift */, + AD00D6E61F26655B00DB4967 /* InAppMessaging_Example_iOS_SwiftUITests.swift */, + AD00D6E81F26655B00DB4967 /* Info.plist */, + ); + path = InAppMessaging_Example_iOS_SwiftUITests; + sourceTree = ""; + }; + AD531EF91F3A5D9D00E899A5 /* UI-Tests */ = { + isa = PBXGroup; + children = ( + AD00D6E51F26655B00DB4967 /* InAppMessaging_Example_iOS_SwiftUITests */, + ); + name = "UI-Tests"; + sourceTree = ""; + }; + AD531EFA1F3A5DB400E899A5 /* Unit Tests */ = { + isa = PBXGroup; + children = ( + ADE5BB5E20098880001A1395 /* Analytics */, + AD95D8EC1FFEFC6000780607 /* Runtime */, + AD6A6CAB1F56030F00A6DFA1 /* Util */, + ADBC52781F4CD12C00A4BEF9 /* Flows */, + AD8122411F1412C500BF632A /* Info.plist */, + AD8122371F140FEA00BF632A /* Data */, + AD8122361F140FE500BF632A /* UI */, + ); + name = "Unit Tests"; + sourceTree = ""; + }; + AD6A6CAB1F56030F00A6DFA1 /* Util */ = { + isa = PBXGroup; + children = ( + AD241B0E1F4F582A009A3C22 /* FIRIAMElapsedTimeTrackerTests.m */, + ADC429891F8BEAFD00027599 /* UIColor+FIRIAMHexStringTests.m */, + 25C6745021EEC868005A4C23 /* NSString+InterlaceStringsTests.m */, + ); + name = Util; + sourceTree = ""; + }; + AD811A241F13F88800BF632A = { + isa = PBXGroup; + children = ( + AD1469821FEC7DD5002051BF /* InAppMessaging_Example_iOS.entitlements */, + AD81221F1F140DE800BF632A /* Tests */, + AD811A471F13FB3A00BF632A /* App */, + AD811A2E1F13F88800BF632A /* Products */, + 9F765C4CE4252F7162E20DB1 /* Pods */, + AE2B80A4702458F0BCEF595B /* Frameworks */, + ); + sourceTree = ""; + }; + AD811A2E1F13F88800BF632A /* Products */ = { + isa = PBXGroup; + children = ( + AD811A2D1F13F88800BF632A /* InAppMessaging_Example_iOS.app */, + AD81222C1F140F9700BF632A /* InAppMessaging_Tests_iOS.xctest */, + ); + name = Products; + sourceTree = ""; + }; + AD811A471F13FB3A00BF632A /* App */ = { + isa = PBXGroup; + children = ( + AD8121DF1F13FC2D00BF632A /* iOS */, + ); + name = App; + sourceTree = ""; + }; + AD8121DF1F13FC2D00BF632A /* iOS */ = { + isa = PBXGroup; + children = ( + AD8121E01F13FC3800BF632A /* objc */, + ); + name = iOS; + sourceTree = ""; + }; + AD8121E01F13FC3800BF632A /* objc */ = { + isa = PBXGroup; + children = ( + AD764A2D1FE4856300378AE0 /* AutoDisplayFlowViewController.h */, + AD764A2E1FE4856300378AE0 /* AutoDisplayFlowViewController.m */, + AD764A2C1FE4856300378AE0 /* AutoDisplayMesagesTableVC.h */, + AD764A331FE4856300378AE0 /* AutoDisplayMesagesTableVC.m */, + AD764A311FE4856300378AE0 /* LogDumpViewController.h */, + AD764A301FE4856300378AE0 /* LogDumpViewController.m */, + ADC4298E1F8D3DD500027599 /* GoogleService-Info.plist */, + AD81221B1F14064800BF632A /* LaunchScreen.storyboard */, + AD8122181F14052100BF632A /* Main.storyboard */, + AD811A331F13F88800BF632A /* AppDelegate.h */, + AD811A341F13F88800BF632A /* AppDelegate.m */, + AD811A3C1F13F88800BF632A /* Shared.xcassets */, + AD811A411F13F88800BF632A /* Info.plist */, + AD811A311F13F88800BF632A /* main.m */, + ); + name = objc; + sourceTree = ""; + }; + AD81221F1F140DE800BF632A /* Tests */ = { + isa = PBXGroup; + children = ( + AD531EFA1F3A5DB400E899A5 /* Unit Tests */, + AD531EF91F3A5D9D00E899A5 /* UI-Tests */, + ); + name = Tests; + sourceTree = ""; + }; + AD8122361F140FE500BF632A /* UI */ = { + isa = PBXGroup; + children = ( + ); + name = UI; + sourceTree = ""; + }; + AD8122371F140FEA00BF632A /* Data */ = { + isa = PBXGroup; + children = ( + AD8122391F140FFC00BF632A /* FIRIAMMessageContentDataWithImageURLTests.m */, + AD0E370F1F7DB3D500BBF23C /* FIRIAMFetchResponseParserTests.m */, + ADB47ADE20A107C6002D52E9 /* TestJsonDataFromFetch.txt */, + ADB47AE120A110F4002D52E9 /* TestJsonDataWithTestMessageFromFetch.txt */, + AD39268820AB55B700FA3FDF /* JsonDataWithInvalidMessagesFromFetch.txt */, + ); + name = Data; + sourceTree = ""; + }; + AD95D8EC1FFEFC6000780607 /* Runtime */ = { + isa = PBXGroup; + children = ( + AD95D8ED1FFEFCA700780607 /* FIRIAMActionUrlFollowerTests.m */, + ); + name = Runtime; + sourceTree = ""; + }; + ADBC52781F4CD12C00A4BEF9 /* Flows */ = { + isa = PBXGroup; + children = ( + AD6A6CAE1F5606CD00A6DFA1 /* FIRIAMBookKeeperViaUserDefaultsTests.m */, + ADBC52791F4CD18300A4BEF9 /* FIRIAMFetchFlowTests.m */, + AD3EE8291F4CED0B006829AE /* FIRIAMDisplayExecutorTests.m */, + AD0E370B1F7C63BC00BBF23C /* FIRIAMMsgFetcherUsingRestfulTests.m */, + AD3868521F95300E00E36BC5 /* FIRIAMMessageClientCacheTests.m */, + AD5D25CA1FA91C9900F1B0EB /* FIRIAMActivityLoggerTests.m */, + ADCC090F20782E2D009BFC2F /* FIRIAMSDKModeManagerTests.m */, + ); + name = Flows; + sourceTree = ""; + }; + ADE5BB5E20098880001A1395 /* Analytics */ = { + isa = PBXGroup; + children = ( + ADD981952006D1C500944751 /* FIRIAMClearcutLoggerTests.m */, + ADE5BB5F200988A1001A1395 /* FIRIAMClearcutRetryLocalStorageTests.m */, + ADA10F99202CCBFC000F4425 /* FIRIAMAnalyticsEventLoggerImplTests.m */, + ADA72B9220282F3B0087E131 /* FIRIAMClearcutUploaderTests.m */, + ); + name = Analytics; + sourceTree = ""; + }; + AE2B80A4702458F0BCEF595B /* Frameworks */ = { + isa = PBXGroup; + children = ( + ADA10F97202CC1B0000F4425 /* AdSupport.framework */, + 16E2417C62DE07F79F99BC3A /* Pods_InAppMessaging_Example_iOS.framework */, + 9E53A331D50419C13BE9189E /* Pods_InAppMessaging_Example_iOS_Swift.framework */, + B403D5D166E193EAAB4C3B25 /* Pods_InAppMessaging_Tests_iOS.framework */, + B08DD02B5CAEC2B9FF562FBC /* Pods_fiam_sample_consuming_app.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + AD811A2C1F13F88800BF632A /* InAppMessaging_Example_iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = AD811A441F13F88800BF632A /* Build configuration list for PBXNativeTarget "InAppMessaging_Example_iOS" */; + buildPhases = ( + B3566055E1E8BBB4417DE800 /* [CP] Check Pods Manifest.lock */, + AD811A291F13F88800BF632A /* Sources */, + AD811A2A1F13F88800BF632A /* Frameworks */, + AD811A2B1F13F88800BF632A /* Resources */, + EB73238A9A185D6764A4067A /* [CP] Embed Pods Frameworks */, + E8F0203E0D9D89F08A0D0D30 /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = InAppMessaging_Example_iOS; + productName = "InAppMessaging-Example-iOS"; + productReference = AD811A2D1F13F88800BF632A /* InAppMessaging_Example_iOS.app */; + productType = "com.apple.product-type.application"; + }; + AD81222B1F140F9700BF632A /* InAppMessaging_Tests_iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = AD8122351F140F9700BF632A /* Build configuration list for PBXNativeTarget "InAppMessaging_Tests_iOS" */; + buildPhases = ( + 51B09CAB6AC5DD3E8DDA85E3 /* [CP] Check Pods Manifest.lock */, + AD8122281F140F9700BF632A /* Sources */, + AD8122291F140F9700BF632A /* Frameworks */, + AD81222A1F140F9700BF632A /* Resources */, + 8A096C95C7D03347A14348EE /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + AD8122321F140F9700BF632A /* PBXTargetDependency */, + ); + name = InAppMessaging_Tests_iOS; + productName = InAppMessaging_Tests_iOS; + productReference = AD81222C1F140F9700BF632A /* InAppMessaging_Tests_iOS.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + AD811A251F13F88800BF632A /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0830; + LastUpgradeCheck = 0900; + ORGANIZATIONNAME = "Yong Mao"; + TargetAttributes = { + AD811A2C1F13F88800BF632A = { + CreatedOnToolsVersion = 8.3.3; + DevelopmentTeam = EQHXZ8M8AV; + LastSwiftMigration = 0910; + ProvisioningStyle = Manual; + SystemCapabilities = { + com.apple.SafariKeychain = { + enabled = 1; + }; + }; + }; + AD81222B1F140F9700BF632A = { + CreatedOnToolsVersion = 8.3.3; + DevelopmentTeam = L8VKXC2S77; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = AD811A281F13F88800BF632A /* Build configuration list for PBXProject "InAppMessaging-Example-iOS" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = AD811A241F13F88800BF632A; + productRefGroup = AD811A2E1F13F88800BF632A /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + AD811A2C1F13F88800BF632A /* InAppMessaging_Example_iOS */, + AD81222B1F140F9700BF632A /* InAppMessaging_Tests_iOS */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + AD811A2B1F13F88800BF632A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + AD8122431F14148100BF632A /* LaunchScreen.storyboard in Resources */, + AD8122441F14148100BF632A /* Main.storyboard in Resources */, + ADC4298F1F8D3DD500027599 /* GoogleService-Info.plist in Resources */, + AD39268920AB55B700FA3FDF /* JsonDataWithInvalidMessagesFromFetch.txt in Resources */, + ADB47ADF20A107C6002D52E9 /* TestJsonDataFromFetch.txt in Resources */, + AD811A3D1F13F88800BF632A /* Shared.xcassets in Resources */, + ADB47AE220A110F4002D52E9 /* TestJsonDataWithTestMessageFromFetch.txt in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + AD81222A1F140F9700BF632A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + AD39268A20AB56B900FA3FDF /* JsonDataWithInvalidMessagesFromFetch.txt in Resources */, + ADB47AE320A111D5002D52E9 /* TestJsonDataWithTestMessageFromFetch.txt in Resources */, + ADB47AE020A107F3002D52E9 /* TestJsonDataFromFetch.txt in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 51B09CAB6AC5DD3E8DDA85E3 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-InAppMessaging_Tests_iOS-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 8A096C95C7D03347A14348EE /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${SRCROOT}/Pods/Target Support Files/Pods-InAppMessaging_Tests_iOS/Pods-InAppMessaging_Tests_iOS-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/GoogleUtilities/GoogleUtilities.framework", + "${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework", + "${BUILT_PRODUCTS_DIR}/OCMock/OCMock.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleUtilities.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OCMock.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-InAppMessaging_Tests_iOS/Pods-InAppMessaging_Tests_iOS-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + B3566055E1E8BBB4417DE800 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-InAppMessaging_Example_iOS-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + }; + E8F0203E0D9D89F08A0D0D30 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 12; + files = ( + ); + inputPaths = ( + "${SRCROOT}/Pods/Target Support Files/Pods-InAppMessaging_Example_iOS/Pods-InAppMessaging_Example_iOS-resources.sh", + "${PODS_CONFIGURATION_BUILD_DIR}/FirebaseInAppMessagingDisplay/InAppMessagingDisplayResources.bundle", + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/InAppMessagingDisplayResources.bundle", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-InAppMessaging_Example_iOS/Pods-InAppMessaging_Example_iOS-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + EB73238A9A185D6764A4067A /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${SRCROOT}/Pods/Target Support Files/Pods-InAppMessaging_Example_iOS/Pods-InAppMessaging_Example_iOS-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/GoogleUtilities/GoogleUtilities.framework", + "${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleUtilities.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-InAppMessaging_Example_iOS/Pods-InAppMessaging_Example_iOS-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + AD811A291F13F88800BF632A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + AD764A341FE4856400378AE0 /* AutoDisplayFlowViewController.m in Sources */, + AD764A371FE4856400378AE0 /* AutoDisplayMesagesTableVC.m in Sources */, + AD811A351F13F88800BF632A /* AppDelegate.m in Sources */, + AD764A361FE4856400378AE0 /* LogDumpViewController.m in Sources */, + AD811A321F13F88800BF632A /* main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + AD8122281F140F9700BF632A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 25C6745121EEC868005A4C23 /* NSString+InterlaceStringsTests.m in Sources */, + ADC4298A1F8BEAFD00027599 /* UIColor+FIRIAMHexStringTests.m in Sources */, + AD3EE82A1F4CED0B006829AE /* FIRIAMDisplayExecutorTests.m in Sources */, + ADA72B9320282F3B0087E131 /* FIRIAMClearcutUploaderTests.m in Sources */, + ADCC091020782E2D009BFC2F /* FIRIAMSDKModeManagerTests.m in Sources */, + ADD981962006D1C500944751 /* FIRIAMClearcutLoggerTests.m in Sources */, + ADE5BB60200988A1001A1395 /* FIRIAMClearcutRetryLocalStorageTests.m in Sources */, + AD81223D1F14100700BF632A /* FIRIAMMessageContentDataWithImageURLTests.m in Sources */, + AD95D8EE1FFEFCA700780607 /* FIRIAMActionUrlFollowerTests.m in Sources */, + ADA10F9A202CCBFC000F4425 /* FIRIAMAnalyticsEventLoggerImplTests.m in Sources */, + ADBC527A1F4CD18300A4BEF9 /* FIRIAMFetchFlowTests.m in Sources */, + AD3868531F95300E00E36BC5 /* FIRIAMMessageClientCacheTests.m in Sources */, + AD0E370C1F7C63BC00BBF23C /* FIRIAMMsgFetcherUsingRestfulTests.m in Sources */, + AD6A6CB11F56093700A6DFA1 /* FIRIAMBookKeeperViaUserDefaultsTests.m in Sources */, + AD0E37101F7DB3D500BBF23C /* FIRIAMFetchResponseParserTests.m in Sources */, + AD241B0F1F4F582A009A3C22 /* FIRIAMElapsedTimeTrackerTests.m in Sources */, + AD5D25CB1FA91C9900F1B0EB /* FIRIAMActivityLoggerTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + AD8122321F140F9700BF632A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = AD811A2C1F13F88800BF632A /* InAppMessaging_Example_iOS */; + targetProxy = AD8122311F140F9700BF632A /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + AD8122181F14052100BF632A /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + AD8122191F14052100BF632A /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + AD81221B1F14064800BF632A /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + AD81221C1F14064800BF632A /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + AD811A421F13F88800BF632A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.3; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + AD811A431F13F88800BF632A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.3; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + AD811A451F13F88800BF632A /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = BF4A969BA440546630E4C931 /* Pods-InAppMessaging_Example_iOS.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = InAppMessaging_Example_iOS.entitlements; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_STYLE = Manual; + DEVELOPMENT_TEAM = EQHXZ8M8AV; + FRAMEWORK_SEARCH_PATHS = "$(inherited)"; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "\"${PODS_ROOT}/../../../Firebase/InAppMessaging/\"/**", + ); + INFOPLIST_FILE = "$(SRCROOT)/App/InAppMessaging-Example-iOS/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 8.4; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.google.experimental1.dev; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE = "3aa1288b-7c4f-4d59-8975-546306cf00ae"; + PROVISIONING_PROFILE_SPECIFIER = "Experimental App 1 Dev"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + AD811A461F13F88800BF632A /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F0C2B7614A637C08CBDB8FF1 /* Pods-InAppMessaging_Example_iOS.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = InAppMessaging_Example_iOS.entitlements; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_STYLE = Manual; + DEVELOPMENT_TEAM = EQHXZ8M8AV; + FRAMEWORK_SEARCH_PATHS = "$(inherited)"; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "\"${PODS_ROOT}/../../../Firebase/InAppMessaging/\"/**", + ); + INFOPLIST_FILE = "$(SRCROOT)/App/InAppMessaging-Example-iOS/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 8.4; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.google.experimental1.dev; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE = "3aa1288b-7c4f-4d59-8975-546306cf00ae"; + PROVISIONING_PROFILE_SPECIFIER = "Experimental App 1 Dev"; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; + AD8122331F140F9700BF632A /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 92F3AF0CAD88D57986578C52 /* Pods-InAppMessaging_Tests_iOS.debug.xcconfig */; + buildSettings = { + DEVELOPMENT_TEAM = L8VKXC2S77; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "\"${PODS_ROOT}/../../../Firebase/InAppMessaging/\"/**", + ); + INFOPLIST_FILE = Tests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.google.InAppMessaging-Tests-iOS"; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + AD8122341F140F9700BF632A /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 4A218F594D01B3E92842DF08 /* Pods-InAppMessaging_Tests_iOS.release.xcconfig */; + buildSettings = { + DEVELOPMENT_TEAM = L8VKXC2S77; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "\"${PODS_ROOT}/../../../Firebase/InAppMessaging/\"/**", + ); + INFOPLIST_FILE = Tests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.google.InAppMessaging-Tests-iOS"; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + AD811A281F13F88800BF632A /* Build configuration list for PBXProject "InAppMessaging-Example-iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AD811A421F13F88800BF632A /* Debug */, + AD811A431F13F88800BF632A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + AD811A441F13F88800BF632A /* Build configuration list for PBXNativeTarget "InAppMessaging_Example_iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AD811A451F13F88800BF632A /* Debug */, + AD811A461F13F88800BF632A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + AD8122351F140F9700BF632A /* Build configuration list for PBXNativeTarget "InAppMessaging_Tests_iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AD8122331F140F9700BF632A /* Debug */, + AD8122341F140F9700BF632A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = AD811A251F13F88800BF632A /* Project object */; +} diff --git a/InAppMessaging/Example/InAppMessaging-Example-iOS.xcodeproj/xcshareddata/xcschemes/InAppMessaging_Example_iOS.xcscheme b/InAppMessaging/Example/InAppMessaging-Example-iOS.xcodeproj/xcshareddata/xcschemes/InAppMessaging_Example_iOS.xcscheme new file mode 100644 index 00000000000..735c3f01be4 --- /dev/null +++ b/InAppMessaging/Example/InAppMessaging-Example-iOS.xcodeproj/xcshareddata/xcschemes/InAppMessaging_Example_iOS.xcscheme @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/InAppMessaging/Example/InAppMessaging-Example-iOS.xcodeproj/xcshareddata/xcschemes/InAppMessaging_Tests_iOS.xcscheme b/InAppMessaging/Example/InAppMessaging-Example-iOS.xcodeproj/xcshareddata/xcschemes/InAppMessaging_Tests_iOS.xcscheme new file mode 100644 index 00000000000..d80dabb17c5 --- /dev/null +++ b/InAppMessaging/Example/InAppMessaging-Example-iOS.xcodeproj/xcshareddata/xcschemes/InAppMessaging_Tests_iOS.xcscheme @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/InAppMessaging/Example/InAppMessaging_Example_iOS.entitlements b/InAppMessaging/Example/InAppMessaging_Example_iOS.entitlements new file mode 100644 index 00000000000..31304199ac4 --- /dev/null +++ b/InAppMessaging/Example/InAppMessaging_Example_iOS.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.developer.associated-domains + + applinks:da29k.app.goo.gl + + + diff --git a/InAppMessaging/Example/InAppMessaging_Example_iOS_SwiftUITests/InAppMessaging_Example_iOS_SwiftUITests.swift b/InAppMessaging/Example/InAppMessaging_Example_iOS_SwiftUITests/InAppMessaging_Example_iOS_SwiftUITests.swift new file mode 100644 index 00000000000..71c526137ee --- /dev/null +++ b/InAppMessaging/Example/InAppMessaging_Example_iOS_SwiftUITests/InAppMessaging_Example_iOS_SwiftUITests.swift @@ -0,0 +1,680 @@ +/* + * 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 XCTest + +class InAppMessaging_Example_iOS_SwiftUITests: XCTestCase { + override func setUp() { + super.setUp() + continueAfterFailure = false + let app = XCUIApplication() + setupSnapshot(app) + app.launch() + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + XCUIDevice.shared.orientation = .portrait + super.tearDown() + } + + func waitForElementToAppear(_ element: XCUIElement, _ timeoutInSeconds: TimeInterval = 5) { + let existsPredicate = NSPredicate(format: "exists == true") + expectation(for: existsPredicate, evaluatedWith: element, handler: nil) + waitForExpectations(timeout: timeoutInSeconds, handler: nil) + } + + func waitForElementToDisappear(_ element: XCUIElement, _ timeoutInSeconds: TimeInterval = 5) { + let existsPredicate = NSPredicate(format: "exists == false") + expectation(for: existsPredicate, evaluatedWith: element, handler: nil) + waitForExpectations(timeout: timeoutInSeconds, handler: nil) + } + + func childFrameWithinParentBound(parent: XCUIElement, child: XCUIElement) -> Bool { + return parent.frame.contains(child.frame) + } + + func isUIElementWithinUIWindow(_ uiElement: XCUIElement) -> Bool { + let app = XCUIApplication() + let window = app.windows.element(boundBy: 0) + return window.frame.contains(uiElement.frame) + } + + func isElementExistentAndHavingSize(_ uiElement: XCUIElement) -> Bool { + // on iOS 9.3 for a XCUIElement whose height or width <=0, uiElement.exists still returns true + // on iOS 10.3, for such an element uiElement.exists returns false + // this function is to handle the existence (in our semanatic visible) testing for both cases + return uiElement.exists && uiElement.frame.size.height > 0 && uiElement.frame.size.width > 0 + } + + func testNormalModalView() { + let app = XCUIApplication() + app.tabBars.buttons["Modal Messages"].tap() + + let messageCardView = app.otherElements["message-card-view"] + let closeButton = app.buttons["close-button"] + let imageView = app.images["modal-image-view"] + let actionButton = app.buttons["message-action-button"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + + app.buttons["Regular"].tap() + + waitForElementToAppear(closeButton) + + snapshot("in-app-regular-modal-view-\(orientation.rawValue)") + + XCTAssert(isElementExistentAndHavingSize(actionButton)) + XCTAssert(isElementExistentAndHavingSize(imageView)) + XCTAssert(isElementExistentAndHavingSize(messageCardView)) + XCTAssert(isElementExistentAndHavingSize(closeButton)) + + XCTAssert(isUIElementWithinUIWindow(messageCardView)) + XCTAssert(childFrameWithinParentBound(parent: messageCardView, child: actionButton)) + + app.buttons["close-button"].tap() + waitForElementToDisappear(messageCardView) + } + } + + func testModalViewWithWideImage() { + let app = XCUIApplication() + app.tabBars.buttons["Modal Messages"].tap() + + let messageCardView = app.otherElements["message-card-view"] + let closeButton = app.buttons["close-button"] + let imageView = app.images["modal-image-view"] + let actionButton = app.buttons["message-action-button"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + + app.buttons["Thin Image"].tap() + + waitForElementToAppear(closeButton) + + snapshot("in-app-regular-modal-view-with-wider-image-\(orientation.rawValue)") + + XCTAssert(isElementExistentAndHavingSize(actionButton)) + XCTAssert(isElementExistentAndHavingSize(imageView)) + XCTAssert(isElementExistentAndHavingSize(messageCardView)) + XCTAssert(isElementExistentAndHavingSize(closeButton)) + + XCTAssert(isUIElementWithinUIWindow(messageCardView)) + XCTAssert(childFrameWithinParentBound(parent: messageCardView, child: actionButton)) + + app.buttons["close-button"].tap() + waitForElementToDisappear(messageCardView) + } + } + + func testModalViewWithNarrowImage() { + let app = XCUIApplication() + app.tabBars.buttons["Modal Messages"].tap() + + let messageCardView = app.otherElements["message-card-view"] + let closeButton = app.buttons["close-button"] + let imageView = app.images["modal-image-view"] + let actionButton = app.buttons["message-action-button"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + + app.buttons["Wide Image"].tap() + + waitForElementToAppear(closeButton) + + snapshot("in-app-regular-modal-view-with-narrow-image-\(orientation.rawValue)") + + XCTAssert(isElementExistentAndHavingSize(actionButton)) + XCTAssert(isElementExistentAndHavingSize(imageView)) + XCTAssert(isElementExistentAndHavingSize(messageCardView)) + XCTAssert(isElementExistentAndHavingSize(closeButton)) + + XCTAssert(isUIElementWithinUIWindow(messageCardView)) + XCTAssert(childFrameWithinParentBound(parent: messageCardView, child: actionButton)) + + app.buttons["close-button"].tap() + waitForElementToDisappear(messageCardView) + } + } + + func testNormalBannerView() { + let app = XCUIApplication() + app.tabBars.buttons["Banner Messages"].tap() + + let titleElement = app.staticTexts["banner-message-title-view"] + let imageView = app.images["banner-image-view"] + let bodyElement = app.staticTexts["banner-body-label"] + let bannerUIView = app.otherElements["banner-mode-uiview"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + + app.buttons["Show Regular Banner View"].tap() + + waitForElementToAppear(bannerUIView) + + snapshot("in-app-regular-banner-view-\(orientation.rawValue)") + + XCTAssert(isElementExistentAndHavingSize(imageView)) + XCTAssert(isElementExistentAndHavingSize(titleElement)) + XCTAssert(isElementExistentAndHavingSize(bodyElement)) + + bannerUIView.swipeUp() + + waitForElementToDisappear(bannerUIView) + } + } + + func testBannerViewAutoDimiss() { + let app = XCUIApplication() + app.tabBars.buttons["Banner Messages"].tap() + + let titleElement = app.staticTexts["banner-message-title-view"] + let imageView = app.images["banner-image-view"] + let bodyElement = app.staticTexts["banner-body-label"] + let bannerUIView = app.otherElements["banner-mode-uiview"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + + app.buttons["Banner View With Short Auto Dismiss"].tap() + + waitForElementToAppear(bannerUIView) + + XCTAssert(isElementExistentAndHavingSize(imageView)) + XCTAssert(isElementExistentAndHavingSize(titleElement)) + XCTAssert(isElementExistentAndHavingSize(bodyElement)) + + // without user action, the banner is dismissed quickly in this test setup + waitForElementToDisappear(bannerUIView, 15) + } + } + + func testBannerViewWithoutImage() { + let app = XCUIApplication() + app.tabBars.buttons["Banner Messages"].tap() + + let titleElement = app.staticTexts["banner-message-title-view"] + let bodyElement = app.staticTexts["banner-body-label"] + let bannerUIView = app.otherElements["banner-mode-uiview"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + app.buttons["Without Image"].tap() + waitForElementToAppear(bannerUIView) + + snapshot("in-app-banner-view-without-image-\(orientation.rawValue)") + + XCTAssert(isElementExistentAndHavingSize(titleElement)) + XCTAssert(isElementExistentAndHavingSize(bodyElement)) + + bannerUIView.swipeUp() + waitForElementToDisappear(bannerUIView) + } + } + + func testBannerViewWithLongTitle() { + let app = XCUIApplication() + app.tabBars.buttons["Banner Messages"].tap() + + let titleElement = app.staticTexts["banner-message-title-view"] + let imageView = app.images["banner-image-view"] + let bodyElement = app.staticTexts["banner-body-label"] + let bannerUIView = app.otherElements["banner-mode-uiview"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + app.buttons["With Long Title"].tap() + waitForElementToAppear(bannerUIView) + + snapshot("in-app-banner-view-with-long-title-\(orientation.rawValue)") + + XCTAssert(isElementExistentAndHavingSize(imageView)) + XCTAssert(isElementExistentAndHavingSize(titleElement)) + XCTAssert(isElementExistentAndHavingSize(bodyElement)) + + bannerUIView.swipeUp() + waitForElementToDisappear(bannerUIView) + } + } + + func testBannerViewWithWideImage() { + let app = XCUIApplication() + app.tabBars.buttons["Banner Messages"].tap() + + let titleElement = app.staticTexts["banner-message-title-view"] + let imageView = app.images["banner-image-view"] + let bodyElement = app.staticTexts["banner-body-label"] + let bannerUIView = app.otherElements["banner-mode-uiview"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + app.buttons["With Wide Image"].tap() + waitForElementToAppear(bannerUIView) + + snapshot("in-app-banner-view-with-wide-image-\(orientation.rawValue)") + + XCTAssert(isElementExistentAndHavingSize(imageView)) + XCTAssert(isElementExistentAndHavingSize(titleElement)) + XCTAssert(isElementExistentAndHavingSize(bodyElement)) + + bannerUIView.swipeUp() + waitForElementToDisappear(bannerUIView) + } + } + + func testBannerViewWithThinImage() { + let app = XCUIApplication() + app.tabBars.buttons["Banner Messages"].tap() + + let titleElement = app.staticTexts["banner-message-title-view"] + let imageView = app.images["banner-image-view"] + let bodyElement = app.staticTexts["banner-body-label"] + let bannerUIView = app.otherElements["banner-mode-uiview"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + app.buttons["With Thin Image"].tap() + waitForElementToAppear(bannerUIView) + + snapshot("in-app-banner-view-with-thing-image-\(orientation.rawValue)") + + XCTAssert(isElementExistentAndHavingSize(imageView)) + XCTAssert(isElementExistentAndHavingSize(titleElement)) + XCTAssert(isElementExistentAndHavingSize(bodyElement)) + + bannerUIView.swipeUp() + waitForElementToDisappear(bannerUIView) + } + } + + func testBannerViewWithLargeBody() { + let app = XCUIApplication() + app.tabBars.buttons["Banner Messages"].tap() + + let titleElement = app.staticTexts["banner-message-title-view"] + let imageView = app.images["banner-image-view"] + let bodyElement = app.staticTexts["banner-body-label"] + let bannerUIView = app.otherElements["banner-mode-uiview"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + app.buttons["With Large Body Text"].tap() + waitForElementToAppear(bannerUIView) + + snapshot("in-app-banner-view-with-long-body-\(orientation.rawValue)") + + XCTAssert(isElementExistentAndHavingSize(imageView)) + XCTAssert(isElementExistentAndHavingSize(titleElement)) + XCTAssert(isElementExistentAndHavingSize(bodyElement)) + + bannerUIView.swipeUp() + waitForElementToDisappear(bannerUIView) + } + } + + func testImageOnlyView() { + let app = XCUIApplication() + app.tabBars.buttons["Image Only Messages"].tap() + + let imageView = app.images["image-view-in-image-only-view"] + let closeButton = app.buttons["close-button"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + app.buttons["Show Regular Image Only View"].tap() + + waitForElementToAppear(closeButton) + snapshot("in-app-regular-image-only-view-\(orientation.rawValue)") + + XCTAssert(isElementExistentAndHavingSize(imageView)) + XCTAssert(isUIElementWithinUIWindow(imageView)) + + app.buttons["close-button"].tap() + waitForElementToDisappear(imageView) + } + } + + func testImageOnlyViewWithLargeImageDimension() { + let app = XCUIApplication() + app.tabBars.buttons["Image Only Messages"].tap() + + let imageView = app.images["image-view-in-image-only-view"] + let closeButton = app.buttons["close-button"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + app.buttons["High Dimension Image"].tap() + + // wait time longer due to large image + waitForElementToAppear(closeButton, 10) + + snapshot("in-app-large-image-only-view-high-dimension-\(orientation.rawValue)") + XCTAssert(isElementExistentAndHavingSize(imageView)) + XCTAssert(isUIElementWithinUIWindow(imageView)) + + app.buttons["close-button"].tap() + waitForElementToDisappear(imageView) + } + } + + func testImageOnlyViewWithLowImageDimension() { + let app = XCUIApplication() + app.tabBars.buttons["Image Only Messages"].tap() + + let imageView = app.images["image-view-in-image-only-view"] + let closeButton = app.buttons["close-button"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + app.buttons["Low Dimension Image"].tap() + + // wait time longer due to large image + waitForElementToAppear(closeButton, 10) + + snapshot("in-app-large-image-only-view-low-dimension-\(orientation.rawValue)") + XCTAssert(isElementExistentAndHavingSize(imageView)) + XCTAssert(isUIElementWithinUIWindow(imageView)) + + app.buttons["close-button"].tap() + waitForElementToDisappear(imageView) + } + } + + func testModalViewWithoutImage() { + let app = XCUIApplication() + app.tabBars.buttons["Modal Messages"].tap() + + let messageCardView = app.otherElements["message-card-view"] + let closeButton = app.buttons["close-button"] + let actionButton = app.buttons["message-action-button"] + let imageView = app.images["modal-image-view"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + app.buttons["Without Image"].tap() + waitForElementToAppear(closeButton) + + snapshot("in-app-no-image-modal-view-\(orientation.rawValue)") + XCTAssert(isElementExistentAndHavingSize(actionButton)) + XCTAssert(isElementExistentAndHavingSize(closeButton)) + XCTAssertFalse(isElementExistentAndHavingSize(imageView)) + + XCTAssert(isElementExistentAndHavingSize(messageCardView)) + + XCTAssert(isUIElementWithinUIWindow(messageCardView)) + XCTAssert(childFrameWithinParentBound(parent: messageCardView, child: actionButton)) + + app.buttons["close-button"].tap() + waitForElementToDisappear(messageCardView) + } + } + + func testModalViewWithoutImageOrActionButton() { + let app = XCUIApplication() + app.tabBars.buttons["Modal Messages"].tap() + + let messageCardView = app.otherElements["message-card-view"] + let closeButton = app.buttons["close-button"] + let actionButton = app.buttons["message-action-button"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + app.buttons["Wthout Image or Action Button"].tap() + + waitForElementToAppear(closeButton) + + snapshot("in-app-no-image-no-button-modal-view-\(orientation.rawValue)") + XCTAssertFalse(isElementExistentAndHavingSize(actionButton)) + XCTAssert(isElementExistentAndHavingSize(closeButton)) + XCTAssert(isElementExistentAndHavingSize(messageCardView)) + + XCTAssert(isUIElementWithinUIWindow(messageCardView)) + + app.buttons["close-button"].tap() + waitForElementToDisappear(messageCardView) + XCUIDevice.shared.orientation = .portrait + } + } + + func testModalViewWithoutActionButton() { + let app = XCUIApplication() + app.tabBars.buttons["Modal Messages"].tap() + + let messageCardView = app.otherElements["message-card-view"] + let closeButton = app.buttons["close-button"] + let imageView = app.images["modal-image-view"] + let actionButton = app.buttons["message-action-button"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + app.buttons["Without Action Button"].tap() + waitForElementToAppear(closeButton) + + snapshot("in-app-no-action-button-moal-view-\(orientation.rawValue)") + XCTAssertFalse(isElementExistentAndHavingSize(actionButton)) + XCTAssert(isElementExistentAndHavingSize(closeButton)) + XCTAssert(isElementExistentAndHavingSize(messageCardView)) + XCTAssert(isElementExistentAndHavingSize(imageView)) + + XCTAssert(isUIElementWithinUIWindow(messageCardView)) + + app.buttons["close-button"].tap() + waitForElementToDisappear(messageCardView) + } + } + + func testModalViewWithLongMessageTitle() { + let app = XCUIApplication() + app.tabBars.buttons["Modal Messages"].tap() + + let messageCardView = app.otherElements["message-card-view"] + let closeButton = app.buttons["close-button"] + let imageView = app.images["modal-image-view"] + let bodyTextview = app.textViews["message-body-textview"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + + app.buttons["Large Title Text"].tap() + waitForElementToAppear(closeButton) + + snapshot("in-app-long-title-modal-view-\(orientation.rawValue)") + let actionButton = app.buttons["message-action-button"] + + XCTAssert(isElementExistentAndHavingSize(actionButton)) + XCTAssert(isElementExistentAndHavingSize(closeButton)) + XCTAssert(isElementExistentAndHavingSize(bodyTextview)) + XCTAssert(isElementExistentAndHavingSize(messageCardView)) + XCTAssert(isElementExistentAndHavingSize(imageView)) + + XCTAssert(isUIElementWithinUIWindow(messageCardView)) + XCTAssert(childFrameWithinParentBound(parent: messageCardView, child: actionButton)) + XCTAssert(childFrameWithinParentBound(parent: messageCardView, child: bodyTextview)) + XCTAssert(childFrameWithinParentBound(parent: messageCardView, child: imageView)) + + app.buttons["close-button"].tap() + + waitForElementToDisappear(messageCardView) + } + } + + func testModalViewWithLongMessageBody() { + let app = XCUIApplication() + app.tabBars.buttons["Modal Messages"].tap() + + let messageCardView = app.otherElements["message-card-view"] + let closeButton = app.buttons["close-button"] + let imageView = app.images["modal-image-view"] + let bodyTextview = app.textViews["message-body-textview"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + + app.buttons["Large Title Text"].tap() + waitForElementToAppear(closeButton) + + snapshot("in-app-long-body-modal-view-\(orientation.rawValue)") + let actionButton = app.buttons["message-action-button"] + + XCTAssert(isElementExistentAndHavingSize(actionButton)) + XCTAssert(isElementExistentAndHavingSize(closeButton)) + XCTAssert(isElementExistentAndHavingSize(bodyTextview)) + XCTAssert(isElementExistentAndHavingSize(messageCardView)) + XCTAssert(isElementExistentAndHavingSize(imageView)) + + XCTAssert(isUIElementWithinUIWindow(messageCardView)) + XCTAssert(childFrameWithinParentBound(parent: messageCardView, child: actionButton)) + XCTAssert(childFrameWithinParentBound(parent: messageCardView, child: bodyTextview)) + XCTAssert(childFrameWithinParentBound(parent: messageCardView, child: imageView)) + + app.buttons["close-button"].tap() + + waitForElementToDisappear(messageCardView) + } + } + + func testModalViewWithLongMessageTitleAndMessageBody() { + let app = XCUIApplication() + app.tabBars.buttons["Modal Messages"].tap() + + let messageCardView = app.otherElements["message-card-view"] + let closeButton = app.buttons["close-button"] + let imageView = app.images["modal-image-view"] + let bodyTextview = app.textViews["message-body-textview"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + + app.buttons["With Large Title and Body"].tap() + waitForElementToAppear(closeButton) + + snapshot("in-app-long-title-and-body-modal-view-\(orientation.rawValue)") + let actionButton = app.buttons["message-action-button"] + + XCTAssert(isElementExistentAndHavingSize(actionButton)) + XCTAssert(isElementExistentAndHavingSize(closeButton)) + XCTAssert(isElementExistentAndHavingSize(bodyTextview)) + XCTAssert(isElementExistentAndHavingSize(messageCardView)) + XCTAssert(isElementExistentAndHavingSize(imageView)) + + XCTAssert(isUIElementWithinUIWindow(messageCardView)) + XCTAssert(childFrameWithinParentBound(parent: messageCardView, child: actionButton)) + XCTAssert(childFrameWithinParentBound(parent: messageCardView, child: bodyTextview)) + XCTAssert(childFrameWithinParentBound(parent: messageCardView, child: imageView)) + + app.buttons["close-button"].tap() + + waitForElementToDisappear(messageCardView) + } + } + + func testModalViewWithLongMessageTitleAndMessageBodyWithoutImage() { + let app = XCUIApplication() + app.tabBars.buttons["Modal Messages"].tap() + + let messageCardView = app.otherElements["message-card-view"] + let closeButton = app.buttons["close-button"] + let imageView = app.images["modal-image-view"] + let bodyTextview = app.textViews["message-body-textview"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + + app.buttons["With Large Title and Body Without Image"].tap() + waitForElementToAppear(closeButton) + + snapshot("in-app-long-title-and-body-no-image-modal-view-\(orientation.rawValue)") + let actionButton = app.buttons["message-action-button"] + + XCTAssert(isElementExistentAndHavingSize(actionButton)) + XCTAssert(isElementExistentAndHavingSize(closeButton)) + XCTAssert(isElementExistentAndHavingSize(bodyTextview)) + XCTAssert(isElementExistentAndHavingSize(messageCardView)) + XCTAssert(!isElementExistentAndHavingSize(imageView)) + + XCTAssert(isUIElementWithinUIWindow(messageCardView)) + XCTAssert(childFrameWithinParentBound(parent: messageCardView, child: actionButton)) + XCTAssert(childFrameWithinParentBound(parent: messageCardView, child: bodyTextview)) + + app.buttons["close-button"].tap() + + waitForElementToDisappear(messageCardView) + } + } + + func testModalViewWithLongMessageTitleWithoutBodyWithoutImageWithoutButton() { + let app = XCUIApplication() + app.tabBars.buttons["Modal Messages"].tap() + + let messageCardView = app.otherElements["message-card-view"] + let closeButton = app.buttons["close-button"] + let imageView = app.images["modal-image-view"] + let bodyTextview = app.textViews["message-body-textview"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + + app.buttons["With Large Title, No Image, No Body and No Button"].tap() + waitForElementToAppear(closeButton) + + snapshot("in-app-long-title-no-image-body-button-modal-view-\(orientation.rawValue)") + let actionButton = app.buttons["message-action-button"] + + XCTAssert(!isElementExistentAndHavingSize(actionButton)) + XCTAssert(isElementExistentAndHavingSize(closeButton)) + XCTAssert(!isElementExistentAndHavingSize(bodyTextview)) + XCTAssert(isElementExistentAndHavingSize(messageCardView)) + XCTAssert(!isElementExistentAndHavingSize(imageView)) + + XCTAssert(isUIElementWithinUIWindow(messageCardView)) + + app.buttons["close-button"].tap() + + waitForElementToDisappear(messageCardView) + } + } +} diff --git a/InAppMessaging/Example/InAppMessaging_Example_iOS_SwiftUITests/Info.plist b/InAppMessaging/Example/InAppMessaging_Example_iOS_SwiftUITests/Info.plist new file mode 100644 index 00000000000..6c6c23c43ad --- /dev/null +++ b/InAppMessaging/Example/InAppMessaging_Example_iOS_SwiftUITests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/InAppMessaging/Example/Podfile b/InAppMessaging/Example/Podfile new file mode 100644 index 00000000000..13f55e31691 --- /dev/null +++ b/InAppMessaging/Example/Podfile @@ -0,0 +1,27 @@ + +use_frameworks! + +# Uncomment the next two lines for pre-release testing on public repo +source 'https://github.com/Firebase/SpecsStaging.git' +source 'https://github.com/CocoaPods/Specs.git' + +pod 'FirebaseCore', :path => '../..' + +target 'InAppMessaging_Example_iOS' do + platform :ios, '8.0' + + pod 'FirebaseInAppMessagingDisplay', :path => '../..' + pod 'FirebaseInAppMessaging', :path => '../..' + pod 'FirebaseAnalyticsInterop', :path => '../..' + pod 'FirebaseAnalytics' + pod 'FirebaseDynamicLinks', :path => '../..' +end + +target 'InAppMessaging_Tests_iOS' do + platform :ios, '8.0' + + pod 'FirebaseInAppMessaging', :path => '../..' + pod 'FirebaseInstanceID' + pod 'FirebaseAnalyticsInterop', :path => '../..' + pod 'OCMock' +end diff --git a/InAppMessaging/Example/Scanfile b/InAppMessaging/Example/Scanfile new file mode 100644 index 00000000000..43b96a958a3 --- /dev/null +++ b/InAppMessaging/Example/Scanfile @@ -0,0 +1,13 @@ +# For more information about this configuration visit +# https://github.com/fastlane/fastlane/tree/master/scan#scanfile + +# In general, you can use the options available +# fastlane scan --help + +# Remove the # in front of the line to enable the option + +workspace "InAppMessaging-Example-iOS.xcworkspace" +scheme "InAppMessaging_Example_iOS_Swift" +devices ["iPad Pro", "iPhone 6s", "iPhone 4s", "iPhone 7Plus"] +open_report true +clean true diff --git a/InAppMessaging/Example/Snapfile b/InAppMessaging/Example/Snapfile new file mode 100644 index 00000000000..9c3aed4cc48 --- /dev/null +++ b/InAppMessaging/Example/Snapfile @@ -0,0 +1,37 @@ +# Uncomment the lines below you want to change by removing the # in the beginning + +# A list of devices you want to take the screenshots from + devices([ +# "iPhone 4s", + "iPhone 6 Plus", + "iPhone 5s", + "iPhone 7", + "iPad Pro (12.9 inch)", +# "iPad Pro (9.7 inch)", +# "Apple TV 1080p" + ]) + + +languages([ + "en-US", +]) + +# The name of the scheme which contains the UI Tests +scheme "InAppMessaging_Example_iOS_Swift" + +# Where should the resulting screenshots be stored? +output_directory "./screenshots" + +clear_previous_screenshots true # remove the '#' to clear all previously generated screenshots before creating new ones + +# Choose which project/workspace to use +# project "./Project.xcodeproj" +# workspace "./Project.xcworkspace" + +workspace "InAppMessaging-Example-iOS.xcworkspace" + +# Arguments to pass to the app on launch. See https://github.com/fastlane/fastlane/tree/master/snapshot#launch-arguments +# launch_arguments(["-favColor red"]) + +# For more information about all available options run +# fastlane snapshot --help diff --git a/InAppMessaging/Example/TestJsonDataFromFetch.txt b/InAppMessaging/Example/TestJsonDataFromFetch.txt new file mode 100644 index 00000000000..ce305646e0f --- /dev/null +++ b/InAppMessaging/Example/TestJsonDataFromFetch.txt @@ -0,0 +1,169 @@ +{ + "messages": [ + { + "vanillaPayload": { + "campaignId": "13313766398414028800", + "campaignStartTimeMillis": "1523986039000", + "campaignEndTimeMillis": "1526986039000", + "campaignName": "first campaign" + }, + "content": { + "modal": { + "title": { + "text": "I heard you like In-App Messages", + "hexColor": "#000000" + }, + "body": { + "text": "This is message body", + "hexColor": "#000000" + }, + "imageUrl": "https://image.com/5GCaq8sWMgk", + "actionButton": { + "text": { + "text": "Learn More", + "hexColor": "#ffffff" + }, + "buttonHexColor": "#000000" + }, + "action": { + "actionUrl": "https://www.google.com" + }, + "backgroundHexColor": "#fffff8" + } + }, + "priority": { + "value": 1 + }, + "triggeringConditions": [ + { + "fiamTrigger": "ON_FOREGROUND" + }, + { + "event": { + "name": "jackpot" + } + } + ] + }, + { + "vanillaPayload": { + "campaignId": "9350598726327992320", + "campaignStartTimeMillis": "1523985333000", + "campaignEndTimeMillis": "9223372036854775807", + "campaignName": "Inception1" + }, + "content": { + "modal": { + "title": { + "text": "Test 2", + "hexColor": "#000000" + }, + "body": { + "hexColor": "#000000" + }, + "imageUrl": "https://image.com/5GCaq8sWMgk.jpg", + "actionButton": { + "text": { + "text": "Learn More", + "hexColor": "#ffffff" + }, + "buttonHexColor": "#000000" + }, + "action": { + "actionUrl": "https://www.google.com" + }, + "backgroundHexColor": "#ffffff" + } + }, + "priority": { + "value": 1 + }, + "triggeringConditions": [ + { + "fiamTrigger": "ON_FOREGROUND" + }, + { + "event": { + "name": "jackpot" + } + } + ] + }, + { + "vanillaPayload": { + "campaignId": "14819094573862617088", + "campaignStartTimeMillis": "1519934825000", + "campaignEndTimeMillis": "9223372036854775807", + "campaignName": "Top banner" + }, + "content": { + "banner": { + "title": { + "text": "Hey everybody!", + "hexColor": "#000000" + }, + "body": { + "text": "This is an in-app message! Now go to Screen 2!", + "hexColor": "#000000" + }, + "imageUrl": "https://image.com/5YYCaq8sWMgk.png", + "action": { + "actionUrl": "https://test-app.firebaseapp.com/Calculator/screen2" + }, + "backgroundHexColor": "#ffffff" + } + }, + "priority": { + "value": 1 + }, + "triggeringConditions": [ + { + "event": { + "name": "jackpot" + } + } + ] + }, + { + "vanillaPayload": { + "campaignId": "5595722537007841280", + "campaignStartTimeMillis": "1519934650000", + "campaignEndTimeMillis": "9223372036854775807", + "campaignName": "Ducks on foreground" + }, + "content": { + "modal": { + "title": { + "text": "Look, it's a duck!", + "hexColor": "#000000" + }, + "body": { + "text": "It's a very nice duck.", + "hexColor": "#000000" + }, + "imageUrl": "https://image.com/5YYCaq8sWMgkff.png", + "actionButton": { + "text": { + "text": "Go to Google.com", + "hexColor": "#ffffff" + }, + "buttonHexColor": "#000000" + }, + "action": { + "actionUrl": "https://www.google.com" + }, + "backgroundHexColor": "#ffffff" + } + }, + "priority": { + "value": 1 + }, + "triggeringConditions": [ + { + "fiamTrigger": "ON_FOREGROUND" + } + ] + } + ], + "expirationEpochTimestampMillis": "1537896430193" +} diff --git a/InAppMessaging/Example/TestJsonDataWithTestMessageFromFetch.txt b/InAppMessaging/Example/TestJsonDataWithTestMessageFromFetch.txt new file mode 100644 index 00000000000..637eb5cf68d --- /dev/null +++ b/InAppMessaging/Example/TestJsonDataWithTestMessageFromFetch.txt @@ -0,0 +1,71 @@ +{ + "messages": [ + { + "vanillaPayload": { + "campaignId": "2108810525516234752" + }, + "content": { + "modal": { + "title": { + "text": "FAST", + "hexColor": "#000000" + }, + "body": { + "hexColor": "#000000" + }, + "backgroundHexColor": "#ffffff" + } + }, + "triggeringConditions": [ + { + "fiamTrigger": "ON_FOREGROUND" + } + ], + "isTestCampaign": true + }, + { + "vanillaPayload": { + "campaignId": "13313766398414028800", + "campaignStartTimeMillis": "1523986039000", + "campaignEndTimeMillis": "9223372036854775807", + "campaignName": "copy of Inception1" + }, + "content": { + "modal": { + "title": { + "text": "I heard you like In-App Messages", + "hexColor": "#000000" + }, + "body": { + "hexColor": "#000000" + }, + "imageUrl": "https://google.com/an_image", + "actionButton": { + "text": { + "text": "Learn More", + "hexColor": "#ffffff" + }, + "buttonHexColor": "#000000" + }, + "action": { + "actionUrl": "https://www.google.com" + }, + "backgroundHexColor": "#ffffff" + } + }, + "priority": { + "value": 1 + }, + "triggeringConditions": [ + { + "fiamTrigger": "ON_FOREGROUND" + }, + { + "event": { + "name": "jackpot" + } + } + ] + } + ] +} diff --git a/InAppMessaging/Example/Tests/FIRIAMActionUrlFollowerTests.m b/InAppMessaging/Example/Tests/FIRIAMActionUrlFollowerTests.m new file mode 100644 index 00000000000..0f85028c68a --- /dev/null +++ b/InAppMessaging/Example/Tests/FIRIAMActionUrlFollowerTests.m @@ -0,0 +1,270 @@ +/* + * Copyright 2018 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 +#import + +#import "FIRIAMActionURLFollower.h" + +// since OCMock does support mocking respondsToSelector on mock object, we have to define +// different delegate classes with different coverages of certain delegate methods: +// FIRIAMActionURLFollower behavior depend on these method implementation coverages on the +// delegate + +// this delegate only implements application:continueUserActivity:restorationHandler +@interface Delegate1 : NSObject +- (BOOL)application:(UIApplication *)application + continueUserActivity:(NSUserActivity *)userActivity + restorationHandler:(void (^)(NSArray *))restorationHandler; +@end +@implementation Delegate1 +- (BOOL)application:(UIApplication *)application + continueUserActivity:(NSUserActivity *)userActivity + restorationHandler:(void (^)(NSArray *))restorationHandler { + return YES; +} +@end + +// this delegate only implements application:openURL:options which is suitable for custom url scheme +// link handling +@interface Delegate2 : NSObject +- (BOOL)application:(UIApplication *)app + openURL:(NSURL *)url + options:(NSDictionary *)options; +@end +@implementation Delegate2 +- (BOOL)application:(UIApplication *)app + openURL:(NSURL *)url + options:(NSDictionary *)options { + return YES; +} +@end + +@interface FIRIAMActionURLFollowerTests : XCTestCase +@property FIRIAMActionURLFollower *actionFollower; +@property UIApplication *mockApplication; +@property id mockAppDelegate; +@end + +@implementation FIRIAMActionURLFollowerTests + +- (void)setUp { + [super setUp]; + self.mockApplication = OCMClassMock([UIApplication class]); +} + +- (void)tearDown { + // Put teardown code here. This method is called after the invocation of each test method in the + // class. + [super tearDown]; +} + +- (void)testUniversalLinkHandlingReturnYES { + self.mockAppDelegate = OCMClassMock([Delegate1 class]); + OCMStub([self.mockApplication delegate]).andReturn(self.mockAppDelegate); + + // In this test case, app delegate's application:continueUserActivity:restorationHandler + // handles the url and returns YES + + NSURL *url = [NSURL URLWithString:@"http://test.com"]; + OCMExpect([self.mockAppDelegate application:[OCMArg isKindOfClass:[UIApplication class]] + continueUserActivity:[OCMArg checkWithBlock:^BOOL(id userActivity) { + // verifying the type and url field for the userActivity object + NSUserActivity *activity = (NSUserActivity *)userActivity; + return [activity.activityType + isEqualToString:NSUserActivityTypeBrowsingWeb] && + [activity.webpageURL isEqual:url]; + }] + restorationHandler:[OCMArg any]]) + .andReturn(YES); + + FIRIAMActionURLFollower *follower = + [[FIRIAMActionURLFollower alloc] initWithCustomURLSchemeArray:@[] + withApplication:self.mockApplication]; + + XCTestExpectation *expectation = + [self expectationWithDescription:@"Completion Callback Triggered"]; + [follower followActionURL:url + withCompletionBlock:^(BOOL success) { + XCTAssertTrue(success); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; + OCMVerifyAll((id)self.mockAppDelegate); +} + +- (void)setupOpenURLViaIOSForUIApplicationWithReturnValue:(BOOL)returnValue { + // it would fallback to either openURL:options:completionHandler: + // or openURL: on the UIApplication object to follow the url + if ([self.mockApplication respondsToSelector:@selector(openURL:options:completionHandler:)]) { + // id types is needed for calling invokeBlockWithArgs + id yesOrNo = returnValue ? @YES : @NO; + OCMStub([self.mockApplication openURL:[OCMArg any] + options:[OCMArg any] + completionHandler:([OCMArg invokeBlockWithArgs:yesOrNo, nil])]); + } else { + OCMStub([self.mockApplication openURL:[OCMArg any]]).andReturn(returnValue); + } +} + +- (void)testUniversalLinkHandlingReturnNo { + self.mockAppDelegate = OCMClassMock([Delegate1 class]); + OCMStub([self.mockApplication delegate]).andReturn(self.mockAppDelegate); + + // In this test case, app delegate's application:continueUserActivity:restorationHandler + // tries to handle the url but returns NO. We should fallback to the do iOS OpenURL for + // this case + NSURL *url = [NSURL URLWithString:@"http://test.com"]; + OCMExpect([self.mockAppDelegate application:[OCMArg isKindOfClass:[UIApplication class]] + continueUserActivity:[OCMArg any] + restorationHandler:[OCMArg any]]) + .andReturn(NO); + + [self setupOpenURLViaIOSForUIApplicationWithReturnValue:YES]; + + FIRIAMActionURLFollower *follower = + [[FIRIAMActionURLFollower alloc] initWithCustomURLSchemeArray:@[] + withApplication:self.mockApplication]; + + XCTestExpectation *expectation = + [self expectationWithDescription:@"Completion Callback Triggered"]; + [follower followActionURL:url + withCompletionBlock:^(BOOL success) { + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; + OCMVerifyAll((id)self.mockAppDelegate); +} + +- (void)testCustomSchemeHandlingReturnYES { + self.mockAppDelegate = OCMClassMock([Delegate2 class]); + OCMStub([self.mockApplication delegate]).andReturn(self.mockAppDelegate); + + // we support custom url scheme 'scheme1' and 'scheme2' in this setup + FIRIAMActionURLFollower *follower = + [[FIRIAMActionURLFollower alloc] initWithCustomURLSchemeArray:@[ @"scheme1", @"scheme2" ] + withApplication:self.mockApplication]; + + NSURL *customURL = [NSURL URLWithString:@"scheme1://test.com"]; + OCMExpect([self.mockAppDelegate application:[OCMArg isKindOfClass:[UIApplication class]] + openURL:[OCMArg checkWithBlock:^BOOL(id urlId) { + // verifying url received by the app delegate is expected + NSURL *url = (NSURL *)urlId; + return [url isEqual:customURL]; + }] + options:[OCMArg any]]) + .andReturn(YES); + + XCTestExpectation *expectation = + [self expectationWithDescription:@"Completion Callback Triggered"]; + [follower followActionURL:customURL + withCompletionBlock:^(BOOL success) { + XCTAssertTrue(success); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; + OCMVerifyAll((id)self.mockAppDelegate); +} + +- (void)testCustomSchemeHandlingReturnNO { + self.mockAppDelegate = OCMClassMock([Delegate2 class]); + OCMStub([self.mockApplication delegate]).andReturn(self.mockAppDelegate); + + // we support custom url scheme 'scheme1' and 'scheme2' in this setup + FIRIAMActionURLFollower *follower = + [[FIRIAMActionURLFollower alloc] initWithCustomURLSchemeArray:@[ @"scheme1", @"scheme2" ] + withApplication:self.mockApplication]; + + NSURL *customURL = [NSURL URLWithString:@"scheme1://test.com"]; + OCMExpect([self.mockAppDelegate application:[OCMArg isKindOfClass:[UIApplication class]] + openURL:[OCMArg checkWithBlock:^BOOL(id urlId) { + // verifying url received by the app delegate is expected + NSURL *url = (NSURL *)urlId; + return [url isEqual:customURL]; + }] + options:[OCMArg any]]) + .andReturn(NO); + + // it would fallback to Open URL with iOS System + [self setupOpenURLViaIOSForUIApplicationWithReturnValue:NO]; + XCTestExpectation *expectation = + [self expectationWithDescription:@"Completion Callback Triggered"]; + [follower followActionURL:customURL + withCompletionBlock:^(BOOL success) { + // since both custom scheme url open and fallback iOS url open returns NO, we expect + // to get a NO here + XCTAssertFalse(success); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; + OCMVerifyAll((id)self.mockAppDelegate); +} + +- (void)testCustomSchemeNotMatching { + self.mockAppDelegate = OCMClassMock([Delegate2 class]); + OCMStub([self.mockApplication delegate]).andReturn(self.mockAppDelegate); + + // we support custom url scheme 'scheme1' and 'scheme2' in this setup + FIRIAMActionURLFollower *follower = + [[FIRIAMActionURLFollower alloc] initWithCustomURLSchemeArray:@[ @"scheme1", @"scheme2" ] + withApplication:self.mockApplication]; + + NSURL *customURL = [NSURL URLWithString:@"unknown-scheme://test.com"]; + + // since custom scheme does not match, we should not expect app delegate's open URL method + // being triggered + OCMReject([self.mockAppDelegate application:[OCMArg any] + openURL:[OCMArg any] + options:[OCMArg any]]); + + XCTestExpectation *expectation = + [self expectationWithDescription:@"Completion Callback Triggered"]; + [self setupOpenURLViaIOSForUIApplicationWithReturnValue:YES]; + + [follower followActionURL:customURL + withCompletionBlock:^(BOOL success) { + XCTAssertTrue(success); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; + OCMVerifyAll((id)self.mockAppDelegate); +} + +- (void)testUniversalLinkWithoutContinueUserActivityDefined { + // Delegate2 does not define application:continueUserActivity:restorationHandler + self.mockAppDelegate = OCMClassMock([Delegate2 class]); + OCMStub([self.mockApplication delegate]).andReturn(self.mockAppDelegate); + + FIRIAMActionURLFollower *follower = + [[FIRIAMActionURLFollower alloc] initWithCustomURLSchemeArray:@[] + withApplication:self.mockApplication]; + + // so for this url, even if it's a http or https link, we should fall back to openURL with + // iOS system + NSURL *url = [NSURL URLWithString:@"http://test.com"]; + + XCTestExpectation *expectation = + [self expectationWithDescription:@"Completion Callback Triggered"]; + [self setupOpenURLViaIOSForUIApplicationWithReturnValue:YES]; + + [follower followActionURL:url + withCompletionBlock:^(BOOL success) { + XCTAssertTrue(success); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} +@end diff --git a/InAppMessaging/Example/Tests/FIRIAMActivityLoggerTests.m b/InAppMessaging/Example/Tests/FIRIAMActivityLoggerTests.m new file mode 100644 index 00000000000..a82da439662 --- /dev/null +++ b/InAppMessaging/Example/Tests/FIRIAMActivityLoggerTests.m @@ -0,0 +1,185 @@ +/* + * 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 + +#import "FIRIAMActivityLogger.h" +@interface FIRIAMActivityLogger () +- (void)loadFromCachePath:(NSString *)cacheFilePath; +- (BOOL)saveIntoCacheWithPath:(NSString *)cacheFilePath; +@end + +@interface FIRIAMActivityLoggerTests : XCTestCase + +@end + +@implementation FIRIAMActivityLoggerTests + +- (void)setUp { + [super setUp]; + // Put setup code here. This method is called before the invocation of each test method in the + // class. +} + +- (void)tearDown { + // Put teardown code here. This method is called after the invocation of each test method in the + // class. + [super tearDown]; +} + +- (void)testNormalFlow { + FIRIAMActivityLogger *logger = [[FIRIAMActivityLogger alloc] initWithMaxCountBeforeReduce:100 + withSizeAfterReduce:80 + verboseMode:YES + loadFromCache:NO]; + + FIRIAMActivityRecord *first = + [[FIRIAMActivityRecord alloc] initWithActivityType:FIRIAMActivityTypeRenderMessage + isSuccessful:NO + withDetail:@"log detail" + timestamp:nil]; + [logger addLogRecord:first]; + NSDate *now = [[NSDate alloc] init]; + FIRIAMActivityRecord *second = + [[FIRIAMActivityRecord alloc] initWithActivityType:FIRIAMActivityTypeCheckForFetch + isSuccessful:YES + withDetail:@"log detail2" + timestamp:now]; + [logger addLogRecord:second]; + + // now read them back + NSArray *records = [logger readRecords]; + XCTAssertEqual(2, [records count]); + + // notice that log records read out would be [second, first] in LIFO order + FIRIAMActivityRecord *firstFetched = records[0]; + XCTAssertEqualObjects(@"log detail2", firstFetched.detail); + XCTAssertEqual(YES, firstFetched.success); + XCTAssertEqual(FIRIAMActivityTypeCheckForFetch, firstFetched.activityType); + // second's timestamp should be equal to now since it's used to construct that log record + XCTAssertEqualWithAccuracy(now.timeIntervalSince1970, + firstFetched.timestamp.timeIntervalSince1970, 0.001); + + FIRIAMActivityRecord *secondFetched = records[1]; + XCTAssertEqualObjects(@"log detail", secondFetched.detail); + XCTAssertEqual(NO, secondFetched.success); + XCTAssertEqual(FIRIAMActivityTypeRenderMessage, secondFetched.activityType); + // 60 seconds is large enough buffer for the timestamp comparison + XCTAssertEqualWithAccuracy([[NSDate alloc] init].timeIntervalSince1970, + secondFetched.timestamp.timeIntervalSince1970, 60); +} + +- (void)testReduceAfterReachingMaxCount { + // expected behavior for logger regarding reducing is to come down to 1 after reaching size of 3 + FIRIAMActivityLogger *logger = [[FIRIAMActivityLogger alloc] initWithMaxCountBeforeReduce:3 + withSizeAfterReduce:1 + verboseMode:YES + loadFromCache:NO]; + + FIRIAMActivityRecord *first = + [[FIRIAMActivityRecord alloc] initWithActivityType:FIRIAMActivityTypeRenderMessage + isSuccessful:NO + withDetail:@"log detail" + timestamp:nil]; + [logger addLogRecord:first]; + FIRIAMActivityRecord *second = + [[FIRIAMActivityRecord alloc] initWithActivityType:FIRIAMActivityTypeCheckForFetch + isSuccessful:YES + withDetail:@"log detail2" + timestamp:nil]; + [logger addLogRecord:second]; + + FIRIAMActivityRecord *third = + [[FIRIAMActivityRecord alloc] initWithActivityType:FIRIAMActivityTypeCheckForFetch + isSuccessful:YES + withDetail:@"log detail3" + timestamp:nil]; + [logger addLogRecord:third]; + NSArray *records = [logger readRecords]; + XCTAssertEqual(1, [records count]); + + // and the remaining one would be the last one being inserted + XCTAssertEqualObjects(@"log detail3", records[0].detail); +} + +- (void)testNonVerboseMode { + // certain types of messages would get dropped + FIRIAMActivityLogger *logger = [[FIRIAMActivityLogger alloc] initWithMaxCountBeforeReduce:100 + withSizeAfterReduce:50 + verboseMode:NO + loadFromCache:NO]; + + // this one would be added + FIRIAMActivityRecord *next = + [[FIRIAMActivityRecord alloc] initWithActivityType:FIRIAMActivityTypeRenderMessage + isSuccessful:NO + withDetail:@"log detail" + timestamp:nil]; + [logger addLogRecord:next]; + + // this one would be dropped + next = [[FIRIAMActivityRecord alloc] initWithActivityType:FIRIAMActivityTypeCheckForOnOpenMessage + isSuccessful:NO + withDetail:@"log detail" + timestamp:nil]; + [logger addLogRecord:next]; + + // this one would be added + next = [[FIRIAMActivityRecord alloc] initWithActivityType:FIRIAMActivityTypeFetchMessage + isSuccessful:NO + withDetail:@"log detail" + timestamp:nil]; + [logger addLogRecord:next]; + NSArray *records = [logger readRecords]; + XCTAssertEqual(2, [records count]); +} + +- (void)testReadingAndWritingCache { + FIRIAMActivityLogger *logger = [[FIRIAMActivityLogger alloc] initWithMaxCountBeforeReduce:100 + withSizeAfterReduce:50 + verboseMode:YES + loadFromCache:NO]; + + FIRIAMActivityRecord *next = + [[FIRIAMActivityRecord alloc] initWithActivityType:FIRIAMActivityTypeRenderMessage + isSuccessful:NO + withDetail:@"log detail" + timestamp:nil]; + [logger addLogRecord:next]; + next = [[FIRIAMActivityRecord alloc] initWithActivityType:FIRIAMActivityTypeCheckForOnOpenMessage + isSuccessful:NO + withDetail:@"log detail2" + timestamp:nil]; + [logger addLogRecord:next]; + next = [[FIRIAMActivityRecord alloc] initWithActivityType:FIRIAMActivityTypeCheckForOnOpenMessage + isSuccessful:NO + withDetail:@"log detail3" + timestamp:nil]; + [logger addLogRecord:next]; + + NSString *cacheFilePath = [NSString stringWithFormat:@"%@/temp-cache", NSTemporaryDirectory()]; + [logger saveIntoCacheWithPath:cacheFilePath]; + + // read it back + FIRIAMActivityLogger *logger2 = [[FIRIAMActivityLogger alloc] initWithMaxCountBeforeReduce:100 + withSizeAfterReduce:50 + verboseMode:YES + loadFromCache:NO]; + [logger2 loadFromCachePath:cacheFilePath]; + + XCTAssertEqual(3, [[logger2 readRecords] count]); +} +@end diff --git a/InAppMessaging/Example/Tests/FIRIAMAnalyticsEventLoggerImplTests.m b/InAppMessaging/Example/Tests/FIRIAMAnalyticsEventLoggerImplTests.m new file mode 100644 index 00000000000..5816c652483 --- /dev/null +++ b/InAppMessaging/Example/Tests/FIRIAMAnalyticsEventLoggerImplTests.m @@ -0,0 +1,224 @@ +/* + * Copyright 2018 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 +#import + +#import "FIRIAMAnalyticsEventLoggerImpl.h" +#import "FIRIAMClearcutLogger.h" + +#import +#import +#import + +@interface FIRIAMAnalyticsEventLoggerImplTests : XCTestCase +@property(nonatomic) FIRIAMClearcutLogger *mockClearcutLogger; +@property(nonatomic) id mockTimeFetcher; +@property(nonatomic) id mockFirebaseAnalytics; +@property(nonatomic) NSUserDefaults *mockUserDefaults; + +@end + +static NSString *campaignID = @"campaign id"; +static NSString *campaignName = @"campaign name"; + +typedef void (^FIRAUserPropertiesCallback)(NSDictionary *userProperties); + +typedef void (^FakeAnalyticsLogEventHandler)(NSString *origin, + NSString *name, + NSDictionary *parameters); +typedef void (^FakeAnalyticsUserPropertyHandler)(NSString *origin, NSString *name, id value); +typedef void (^LastNotificationCallback)(NSString *); +typedef void (^FakeAnalyticsLastNotificationHandler)(NSString *origin, LastNotificationCallback); + +@interface FakeAnalytics : NSObject + +@property FakeAnalyticsLogEventHandler eventHandler; +@property FakeAnalyticsLogEventHandler userPropertyHandler; +@property FakeAnalyticsLastNotificationHandler lastNotificationHandler; + +- (instancetype)initWithEventHandler:(FakeAnalyticsLogEventHandler)eventHandler; +- (instancetype)initWithUserPropertyHandler:(FakeAnalyticsUserPropertyHandler)userPropertyHandler; +@end + +@implementation FakeAnalytics + +- (instancetype)initWithEventHandler:(FakeAnalyticsLogEventHandler)eventHandler { + self = [super init]; + if (self) { + _eventHandler = eventHandler; + } + return self; +} + +- (instancetype)initWithUserPropertyHandler:(FakeAnalyticsUserPropertyHandler)userPropertyHandler { + self = [super init]; + if (self) { + _userPropertyHandler = userPropertyHandler; + } + return self; +} + +- (void)logEventWithOrigin:(nonnull NSString *)origin + name:(nonnull NSString *)name + parameters:(nullable NSDictionary *)parameters { + if (_eventHandler) { + _eventHandler(origin, name, parameters); + } +} + +- (void)setUserPropertyWithOrigin:(nonnull NSString *)origin + name:(nonnull NSString *)name + value:(nonnull id)value { + if (_userPropertyHandler) { + _userPropertyHandler(origin, name, value); + } +} + +- (void)checkLastNotificationForOrigin:(nonnull NSString *)origin + queue:(nonnull dispatch_queue_t)queue + callback:(nonnull void (^)(NSString *_Nullable)) + currentLastNotificationProperty { + if (_lastNotificationHandler) { + _lastNotificationHandler(origin, currentLastNotificationProperty); + } +} + +// Stubs +- (void)clearConditionalUserProperty:(nonnull NSString *)userPropertyName + clearEventName:(nonnull NSString *)clearEventName + clearEventParameters:(nonnull NSDictionary *)clearEventParameters { +} + +- (nonnull NSArray *) + conditionalUserProperties:(nonnull NSString *)origin + propertyNamePrefix:(nonnull NSString *)propertyNamePrefix { + return @[]; +} + +- (NSInteger)maxUserProperties:(nonnull NSString *)origin { + return -1; +} + +- (void)setConditionalUserProperty:(nonnull FIRAConditionalUserProperty *)conditionalUserProperty { +} + +- (void)registerAnalyticsListener:(nonnull id)listener + withOrigin:(nonnull NSString *)origin { +} + +- (void)unregisterAnalyticsListenerWithOrigin:(nonnull NSString *)origin { +} +@end + +@implementation FIRIAMAnalyticsEventLoggerImplTests + +- (void)setUp { + [super setUp]; + self.mockClearcutLogger = OCMClassMock(FIRIAMClearcutLogger.class); + self.mockTimeFetcher = OCMProtocolMock(@protocol(FIRIAMTimeFetcher)); + self.mockUserDefaults = OCMClassMock(NSUserDefaults.class); +} + +- (void)tearDown { + [super tearDown]; +} + +- (void)testLogImpressionEvent { + XCTestExpectation *expectation1 = [self expectationWithDescription:@"Log to Analytics"]; + FakeAnalytics *analytics = [[FakeAnalytics alloc] + initWithEventHandler:^(NSString *origin, NSString *name, NSDictionary *parameters) { + XCTAssertEqualObjects(origin, @"fiam"); + XCTAssertEqualObjects(name, @"firebase_in_app_message_impression"); + XCTAssertEqual([parameters count], 3); + XCTAssertNotNil(parameters); + XCTAssertEqual(parameters[@"_nmid"], campaignID); + XCTAssertEqual(parameters[@"_nmn"], campaignName); + [expectation1 fulfill]; + }]; + FIRIAMAnalyticsEventLoggerImpl *logger = + [[FIRIAMAnalyticsEventLoggerImpl alloc] initWithClearcutLogger:self.mockClearcutLogger + usingTimeFetcher:self.mockTimeFetcher + usingUserDefaults:nil + analytics:analytics]; + + NSTimeInterval currentMoment = 10000; + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(currentMoment); + + OCMExpect([self.mockClearcutLogger + logAnalyticsEventForType:FIRIAMAnalyticsEventMessageImpression + forCampaignID:[OCMArg isEqual:campaignID] + withCampaignName:[OCMArg isEqual:campaignName] + eventTimeInMs:[OCMArg isNil] + completion:([OCMArg invokeBlockWithArgs:@YES, nil])]); + + XCTestExpectation *expectation2 = + [self expectationWithDescription:@"Completion Callback Triggered"]; + [logger logAnalyticsEventForType:FIRIAMAnalyticsEventMessageImpression + forCampaignID:campaignID + withCampaignName:campaignName + eventTimeInMs:nil + completion:^(BOOL success) { + [expectation2 fulfill]; + }]; + [self waitForExpectationsWithTimeout:1.0 handler:nil]; +} + +- (void)testLogActionEvent { + XCTestExpectation *expectation1 = [self expectationWithDescription:@"Log to Analytics"]; + FakeAnalytics *analytics = [[FakeAnalytics alloc] + initWithEventHandler:^(NSString *origin, NSString *name, NSDictionary *parameters) { + XCTAssertEqualObjects(origin, @"fiam"); + XCTAssertEqualObjects(name, @"firebase_in_app_message_action"); + XCTAssertEqual([parameters count], 3); + XCTAssertNotNil(parameters); + XCTAssertEqual(parameters[@"_nmid"], campaignID); + XCTAssertEqual(parameters[@"_nmn"], campaignName); + [expectation1 fulfill]; + }]; + + FIRIAMAnalyticsEventLoggerImpl *logger = + [[FIRIAMAnalyticsEventLoggerImpl alloc] initWithClearcutLogger:self.mockClearcutLogger + usingTimeFetcher:self.mockTimeFetcher + usingUserDefaults:self.mockUserDefaults + analytics:analytics]; + + NSTimeInterval currentMoment = 10000; + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(currentMoment); + + OCMExpect([self.mockClearcutLogger + logAnalyticsEventForType:FIRIAMAnalyticsEventActionURLFollow + forCampaignID:[OCMArg isEqual:campaignID] + withCampaignName:[OCMArg isEqual:campaignName] + eventTimeInMs:[OCMArg isNil] + completion:([OCMArg invokeBlockWithArgs:@YES, nil])]); + + XCTestExpectation *expectation2 = + [self expectationWithDescription:@"Completion Callback Triggered"]; + + [logger logAnalyticsEventForType:FIRIAMAnalyticsEventActionURLFollow + forCampaignID:campaignID + withCampaignName:campaignName + eventTimeInMs:nil + completion:^(BOOL success) { + [expectation2 fulfill]; + }]; + + [self waitForExpectationsWithTimeout:2.0 handler:nil]; + OCMVerifyAll((id)self.mockClearcutLogger); +} + +@end diff --git a/InAppMessaging/Example/Tests/FIRIAMBookKeeperViaUserDefaultsTests.m b/InAppMessaging/Example/Tests/FIRIAMBookKeeperViaUserDefaultsTests.m new file mode 100644 index 00000000000..85b07845eb8 --- /dev/null +++ b/InAppMessaging/Example/Tests/FIRIAMBookKeeperViaUserDefaultsTests.m @@ -0,0 +1,187 @@ +/* + * 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 +#import + +#import "FIRIAMBookKeeper.h" + +@interface FIRIAMBookKeeperViaUserDefaultsTests : XCTestCase +@property(nonatomic) NSUserDefaults *userDefaultsForTesting; +@end + +extern NSString *FIRIAM_UserDefaultsKeyForImpressions; +extern NSString *FIRIAM_UserDefaultsKeyForLastImpressionTimestamp; + +extern NSString *FIRIAM_ImpressionDictKeyForID; +extern NSString *FIRIAM_ImpressionDictKeyForTimestamp; + +@implementation FIRIAMBookKeeperViaUserDefaultsTests +- (void)setUp { + [super setUp]; + self.userDefaultsForTesting = + [[NSUserDefaults alloc] initWithSuiteName:@"FIRIAMBookKeeperViaUserDefaultsTests"]; +} + +- (void)tearDown { + // Put teardown code here. This method is called after the invocation of each test method in the + // class. + [super tearDown]; + [self.userDefaultsForTesting removeSuiteNamed:@"FIRIAMBookKeeperViaUserDefaultsTests"]; +} + +- (void)testRecordImpressionRecords { + FIRIAMBookKeeperViaUserDefaults *bookKeeper = + [[FIRIAMBookKeeperViaUserDefaults alloc] initWithUserDefaults:self.userDefaultsForTesting]; + [bookKeeper cleanupImpressions]; + + NSArray *impressions = [bookKeeper getImpressions]; + XCTAssertEqual(0, [impressions count]); + + double impression1_ts = 12345; + double impression2_ts = 34567; + + [bookKeeper recordNewImpressionForMessage:@"m1" withStartTimestampInSeconds:impression1_ts]; + [bookKeeper recordNewImpressionForMessage:@"m1" withStartTimestampInSeconds:impression2_ts]; + + impressions = [bookKeeper getImpressions]; + // For the same message, we only record the last impression record. + XCTAssertEqual(1, [impressions count]); + XCTAssertEqualWithAccuracy(impression2_ts, impressions[0].impressionTimeInSeconds, 0.1); + + // Verify the last display time. + XCTAssertEqualWithAccuracy(impression2_ts, [bookKeeper lastDisplayTime], 0.1); + + double impression3_ts = 45000; + + [bookKeeper recordNewImpressionForMessage:@"m2" withStartTimestampInSeconds:impression3_ts]; + impressions = [bookKeeper getImpressions]; + // Now we should see two different impression records for two different messages. + XCTAssertEqual(2, [impressions count]); + // Verify the last display time is updated again. + XCTAssertEqualWithAccuracy(impression3_ts, [bookKeeper lastDisplayTime], 0.1); +} + +- (void)testRecordFetchTimes { + FIRIAMBookKeeperViaUserDefaults *bookKeeper = + [[FIRIAMBookKeeperViaUserDefaults alloc] initWithUserDefaults:self.userDefaultsForTesting]; + [bookKeeper cleanupImpressions]; + + double fetch1_ts = 12345; + double fetch2_ts = 34567; + [bookKeeper recordNewFetchWithFetchCount:10 + withTimestampInSeconds:fetch1_ts + nextFetchWaitTime:nil]; + [bookKeeper recordNewFetchWithFetchCount:10 + withTimestampInSeconds:fetch2_ts + nextFetchWaitTime:nil]; + + XCTAssertEqualWithAccuracy(fetch2_ts, [bookKeeper lastFetchTime], 0.1); +} + +- (void)testRecordFetchTimesWithFetchWaitTime { + FIRIAMBookKeeperViaUserDefaults *bookKeeper = + [[FIRIAMBookKeeperViaUserDefaults alloc] initWithUserDefaults:self.userDefaultsForTesting]; + [bookKeeper cleanupImpressions]; + + double fetch1_ts = 12345; + NSNumber *fetchWaitTime = [NSNumber numberWithInt:30000]; + [bookKeeper recordNewFetchWithFetchCount:10 + withTimestampInSeconds:fetch1_ts + nextFetchWaitTime:fetchWaitTime]; + XCTAssertEqualWithAccuracy(fetchWaitTime.doubleValue, [bookKeeper nextFetchWaitTime], 0.1); +} + +- (void)testRecordFetchTimesWithFetchWaitTimeOverCap { + FIRIAMBookKeeperViaUserDefaults *bookKeeper = + [[FIRIAMBookKeeperViaUserDefaults alloc] initWithUserDefaults:self.userDefaultsForTesting]; + [bookKeeper cleanupImpressions]; + + double fetch1_ts = 12345; + NSNumber *fetchWaitTime = [NSNumber numberWithInt:30000]; + [bookKeeper recordNewFetchWithFetchCount:10 + withTimestampInSeconds:fetch1_ts + nextFetchWaitTime:fetchWaitTime]; + XCTAssertEqualWithAccuracy(fetchWaitTime.doubleValue, [bookKeeper nextFetchWaitTime], 0.1); + + // Second recording use a very large fetch wait time: 30000000 is to large to be accepted. + NSNumber *fetchWaitTime2 = [NSNumber numberWithInt:30000000]; + [bookKeeper recordNewFetchWithFetchCount:10 + withTimestampInSeconds:fetch1_ts + nextFetchWaitTime:fetchWaitTime2]; + // Next fetch wait time is still the same as from fetchWaitTime + XCTAssertEqualWithAccuracy(fetchWaitTime.doubleValue, [bookKeeper nextFetchWaitTime], 0.1); +} + +- (void)testFetchImpressions { + NSString *message1 = @"message1 id"; + double message1ImpressionTime = 1000.0; + + NSString *message2 = @"message2 id"; + double message2ImpressionTime = 2000.0; + + FIRIAMBookKeeperViaUserDefaults *bookKeeper = + [[FIRIAMBookKeeperViaUserDefaults alloc] initWithUserDefaults:self.userDefaultsForTesting]; + [bookKeeper cleanupImpressions]; + // Set up existing impressions. + [bookKeeper recordNewImpressionForMessage:message1 + withStartTimestampInSeconds:message1ImpressionTime]; + [bookKeeper recordNewImpressionForMessage:message2 + withStartTimestampInSeconds:message2ImpressionTime]; + + NSArray *fetchedImpressions = [bookKeeper getImpressions]; + + XCTAssertEqual(2, fetchedImpressions.count); + + FIRIAMImpressionRecord *first = fetchedImpressions[0]; + XCTAssertEqualObjects(first.messageID, message1); + XCTAssertEqualWithAccuracy((double)first.impressionTimeInSeconds, message1ImpressionTime, 0.1); + + FIRIAMImpressionRecord *second = fetchedImpressions[1]; + XCTAssertEqualObjects(second.messageID, message2); + XCTAssertEqualWithAccuracy((double)second.impressionTimeInSeconds, message2ImpressionTime, 0.1); + + NSArray *messageIDs = [bookKeeper getMessageIDsFromImpressions]; + XCTAssertEqualObjects(messageIDs[0], message1); + XCTAssertEqualObjects(messageIDs[1], message2); +} + +- (void)testClearImpressionsForMessageIDs { + FIRIAMBookKeeperViaUserDefaults *bookKeeper = + [[FIRIAMBookKeeperViaUserDefaults alloc] initWithUserDefaults:self.userDefaultsForTesting]; + [bookKeeper cleanupImpressions]; + + NSArray *impressions = [bookKeeper getImpressions]; + XCTAssertEqual(0, [impressions count]); + + double impression1_ts = 12345; + double impression2_ts = 34567; + double impression3_ts = 34567; + + [bookKeeper recordNewImpressionForMessage:@"m1" withStartTimestampInSeconds:impression1_ts]; + [bookKeeper recordNewImpressionForMessage:@"m2" withStartTimestampInSeconds:impression2_ts]; + [bookKeeper recordNewImpressionForMessage:@"m3" withStartTimestampInSeconds:impression3_ts]; + + [bookKeeper clearImpressionsWithMessageList:@[ @"m1", @"m3" ]]; + + impressions = [bookKeeper getImpressions]; + + // Only impressions about m2 remains. + XCTAssertEqual(1, [impressions count]); + XCTAssertEqualObjects(impressions[0].messageID, @"m2"); +} + +@end diff --git a/InAppMessaging/Example/Tests/FIRIAMClearcutLoggerTests.m b/InAppMessaging/Example/Tests/FIRIAMClearcutLoggerTests.m new file mode 100644 index 00000000000..dfdc70c246e --- /dev/null +++ b/InAppMessaging/Example/Tests/FIRIAMClearcutLoggerTests.m @@ -0,0 +1,144 @@ +/* + * Copyright 2018 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 +#import + +#import "FIRIAMClearcutHttpRequestSender.h" +#import "FIRIAMClearcutLogStorage.h" +#import "FIRIAMClearcutLogger.h" +#import "FIRIAMClearcutUploader.h" + +@interface FIRIAMClearcutLoggerTests : XCTestCase +@property(nonatomic) FIRIAMClientInfoFetcher *mockClientInfoFetcher; +@property(nonatomic) id mockTimeFetcher; +@property(nonatomic) FIRIAMClearcutHttpRequestSender *mockRequestSender; +@property(nonatomic) FIRIAMClearcutUploader *mockCtUploader; + +@end + +NSString *iid = @"my iid"; +NSString *osVersion = @"iOS version"; +NSString *sdkVersion = @"SDK version"; + +// we need to access the some internal things in FIRIAMClearcutLogger in our unit tests +// verifications +@interface FIRIAMClearcutLogger (UnitTestAccess) +@property(readonly, nonatomic) FIRIAMClearcutLogStorage *retryStorage; +@property(nonatomic) FIRIAMClearcutHttpRequestSender *requestSender; +@property(nonatomic) id timeFetcher; +- (void)checkAndRetryClearcutLogs; +@end +@interface FIRIAMClearcutLogStorage (UnitTestAccess) +@property(nonatomic) NSMutableArray *records; +@end + +@implementation FIRIAMClearcutLoggerTests +- (void)setUp { + [super setUp]; + + self.mockTimeFetcher = OCMProtocolMock(@protocol(FIRIAMTimeFetcher)); + self.mockClientInfoFetcher = OCMClassMock(FIRIAMClientInfoFetcher.class); + self.mockRequestSender = OCMClassMock(FIRIAMClearcutHttpRequestSender.class); + self.mockCtUploader = OCMClassMock(FIRIAMClearcutUploader.class); + + OCMStub([self.mockClientInfoFetcher + fetchFirebaseIIDDataWithProjectNumber:[OCMArg any] + withCompletion:([OCMArg invokeBlockWithArgs:iid, @"token", + [NSNull null], nil])]); + + OCMStub([self.mockClientInfoFetcher getIAMSDKVersion]).andReturn(sdkVersion); + OCMStub([self.mockClientInfoFetcher getOSVersion]).andReturn(osVersion); +} + +- (void)tearDown { + // Put teardown code here. This method is called after the invocation of each test method in the + // class. + [super tearDown]; +} + +// verify that the produced FIRIAMClearcutLogRecord record has expected content for +// the event extension json string +- (void)testEventLogBodyContent_Expected { + NSString *fbProjectNumber = @"clearcutserver"; + NSString *fbAppId = @"test Firebase app"; + + FIRIAMClearcutLogger *logger = + [[FIRIAMClearcutLogger alloc] initWithFBProjectNumber:fbProjectNumber + fbAppId:fbAppId + clientInfoFetcher:self.mockClientInfoFetcher + usingTimeFetcher:self.mockTimeFetcher + usingUploader:self.mockCtUploader]; + + NSTimeInterval eventMoment = 10000; + __block NSDictionary *capturedEventDict; + + OCMExpect([self.mockCtUploader + addNewLogRecord:[OCMArg checkWithBlock:^BOOL(FIRIAMClearcutLogRecord *newLogRecord) { + NSString *jsonString = newLogRecord.eventExtensionJsonString; + + capturedEventDict = [NSJSONSerialization + JSONObjectWithData:[jsonString dataUsingEncoding:NSUTF8StringEncoding] + options:kNilOptions + error:nil]; + return (int)newLogRecord.eventTimestampInSeconds == (int)eventMoment; + }]]); + + NSString *campaignID = @"test campaign"; + [logger logAnalyticsEventForType:FIRIAMAnalyticsEventActionURLFollow + forCampaignID:campaignID + withCampaignName:@"name" + eventTimeInMs:[NSNumber numberWithInteger:eventMoment * 1000] + completion:^(BOOL success){ + }]; + + OCMVerifyAll((id)self.mockCtUploader); + + XCTAssertEqualObjects(@"CLICK_EVENT_TYPE", capturedEventDict[@"event_type"]); + XCTAssertEqualObjects(fbProjectNumber, capturedEventDict[@"project_number"]); + XCTAssertEqualObjects(campaignID, capturedEventDict[@"campaign_id"]); + XCTAssertEqualObjects(fbAppId, capturedEventDict[@"client_app"][@"google_app_id"]); + XCTAssertEqualObjects(iid, capturedEventDict[@"client_app"][@"firebase_instance_id"]); + XCTAssertEqualObjects(sdkVersion, capturedEventDict[@"fiam_sdk_version"]); +} + +// calling logAnalyticsEventForType with event time set to nil +- (void)testNilEventTimestamp { + FIRIAMClearcutLogger *logger = + [[FIRIAMClearcutLogger alloc] initWithFBProjectNumber:@"clearcutserver" + fbAppId:@"test Firebase app" + clientInfoFetcher:self.mockClientInfoFetcher + usingTimeFetcher:self.mockTimeFetcher + usingUploader:self.mockCtUploader]; + + NSTimeInterval currentMoment = 10000; + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(currentMoment); + + OCMExpect([self.mockCtUploader + addNewLogRecord:[OCMArg checkWithBlock:^BOOL(FIRIAMClearcutLogRecord *newLogRecord) { + return (int)newLogRecord.eventTimestampInSeconds == (int)currentMoment; + }]]); + + [logger logAnalyticsEventForType:FIRIAMAnalyticsEventActionURLFollow + forCampaignID:@"test campaign" + withCampaignName:@"name" + eventTimeInMs:nil + completion:^(BOOL success){ + }]; + + OCMVerifyAll((id)self.mockCtUploader); +} +@end diff --git a/InAppMessaging/Example/Tests/FIRIAMClearcutRetryLocalStorageTests.m b/InAppMessaging/Example/Tests/FIRIAMClearcutRetryLocalStorageTests.m new file mode 100644 index 00000000000..95901f055a5 --- /dev/null +++ b/InAppMessaging/Example/Tests/FIRIAMClearcutRetryLocalStorageTests.m @@ -0,0 +1,81 @@ +/* + * Copyright 2018 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 +#import + +#import "FIRIAMClearcutLogStorage.h" +#import "FIRIAMTimeFetcher.h" + +@interface FIRIAMClearcutLogStorage (UnitTestAccess) +@property(nonatomic) NSMutableArray *records; +@end + +@interface FIRIAMClearcutLogStorageTests : XCTestCase + +@end + +@implementation FIRIAMClearcutLogStorageTests + +- (void)setUp { + [super setUp]; + // Put setup code here. This method is called before the invocation of each test method in the + // class. +} + +- (void)tearDown { + // Put teardown code here. This method is called after the invocation of each test method in the + // class. + [super tearDown]; +} + +- (void)testExpiringOldLogs { + id mockTimeFetcher = OCMProtocolMock(@protocol(FIRIAMTimeFetcher)); + NSInteger logExpiresInSeconds = 20; + + FIRIAMClearcutLogStorage *storage = + [[FIRIAMClearcutLogStorage alloc] initWithExpireAfterInSeconds:logExpiresInSeconds + withTimeFetcher:mockTimeFetcher]; + + NSInteger eventTimestamp = 1000; + // insert 10 logs with event timestamp as eventTimestamp + for (int i = 0; i < 10; i++) { + FIRIAMClearcutLogRecord *nextRecord = + [[FIRIAMClearcutLogRecord alloc] initWithExtensionJsonString:@"json string" + eventTimestampInSeconds:eventTimestamp]; + [storage pushRecords:@[ nextRecord ]]; + } + + // insert 2 logs with event timestamp as eventTimestamp + 10 + for (int i = 0; i < 2; i++) { + FIRIAMClearcutLogRecord *nextRecord = + [[FIRIAMClearcutLogRecord alloc] initWithExtensionJsonString:@"json string" + eventTimestampInSeconds:eventTimestamp + 10]; + [storage pushRecords:@[ nextRecord ]]; + } + + // with this stub, 10 out of the 12 the retry logs are going expired + OCMStub([mockTimeFetcher currentTimestampInSeconds]) + .andReturn(eventTimestamp + logExpiresInSeconds + 1); + + NSArray *results = [storage popStillValidRecordsForUpTo:6]; + // only 2 out of 12 retry logs are still valid + XCTAssertEqual(2, results.count); + + // all the messages should be gone here + XCTAssertEqual(0, storage.records.count); +} +@end diff --git a/InAppMessaging/Example/Tests/FIRIAMClearcutUploaderTests.m b/InAppMessaging/Example/Tests/FIRIAMClearcutUploaderTests.m new file mode 100644 index 00000000000..c4bc6b6725e --- /dev/null +++ b/InAppMessaging/Example/Tests/FIRIAMClearcutUploaderTests.m @@ -0,0 +1,373 @@ +/* + * Copyright 2018 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 +#import + +#import "FIRIAMClearcutHttpRequestSender.h" +#import "FIRIAMClearcutLogStorage.h" +#import "FIRIAMClearcutUploader.h" +#import "FIRIAMTimeFetcher.h" + +@interface FIRIAMClearcutUploaderTests : XCTestCase +@property(nonatomic) id mockTimeFetcher; +@property(nonatomic) FIRIAMClearcutHttpRequestSender *mockRequestSender; +@property(nonatomic) FIRIAMClearcutLogStorage *mockLogStorage; + +@property(nonatomic) FIRIAMClearcutStrategy *defaultStrategy; + +@property(nonatomic) NSUserDefaults *mockUserDefaults; +@end + +// expose certain internal things to help with unit testing +@interface FIRIAMClearcutUploader (UnitTest) +@property(nonatomic, assign) int64_t nextValidSendTimeInMills; +@end + +@implementation FIRIAMClearcutUploaderTests + +- (void)setUp { + [super setUp]; + self.mockTimeFetcher = OCMProtocolMock(@protocol(FIRIAMTimeFetcher)); + self.mockRequestSender = OCMClassMock(FIRIAMClearcutHttpRequestSender.class); + self.mockLogStorage = OCMClassMock(FIRIAMClearcutLogStorage.class); + + self.defaultStrategy = [[FIRIAMClearcutStrategy alloc] initWithMinWaitTimeInMills:1000 + maxWaitTimeInMills:2000 + failureBackoffTimeInMills:1000 + batchSendSize:10]; + + self.mockUserDefaults = OCMClassMock(NSUserDefaults.class); + OCMStub([self.mockUserDefaults integerForKey:[OCMArg any]]).andReturn(0); +} + +- (void)tearDown { + // Put teardown code here. This method is called after the invocation of each test method in the + // class. + [super tearDown]; +} + +- (void)testUploadTriggeredWhenWaitTimeConditionSatisfied { + // using a real storage in this case + FIRIAMClearcutLogStorage *logStorage = + [[FIRIAMClearcutLogStorage alloc] initWithExpireAfterInSeconds:1000 + withTimeFetcher:self.mockTimeFetcher]; + + FIRIAMClearcutUploader *uploader = + [[FIRIAMClearcutUploader alloc] initWithRequestSender:self.mockRequestSender + timeFetcher:self.mockTimeFetcher + logStorage:logStorage + usingStrategy:self.defaultStrategy + usingUserDefaults:self.mockUserDefaults]; + + // so the following setup is to say that it's now ok to do upload right away + // nextValidSendTimeInMills < currnt time + NSTimeInterval currentMoment = 10000; + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(currentMoment); + uploader.nextValidSendTimeInMills = (int64_t)(currentMoment - 1) * 1000; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Triggers send on sender"]; + + OCMStub([self.mockRequestSender sendClearcutHttpRequestForLogs:[OCMArg any] + withCompletion:[OCMArg any]]) + .andDo(^(NSInvocation *invocation) { + [expectation fulfill]; + }); + + FIRIAMClearcutLogRecord *newRecord = + [[FIRIAMClearcutLogRecord alloc] initWithExtensionJsonString:@"string" + eventTimestampInSeconds:currentMoment]; + + [uploader addNewLogRecord:newRecord]; + + // we expect expectation to be fulfilled right away since the upload can be carried out without + // delay + [self waitForExpectationsWithTimeout:1.0 handler:nil]; +} + +- (void)testUploadNotTriggeredWhenWaitTimeConditionNotSatisfied { + // using a real storage in this case + FIRIAMClearcutLogStorage *logStorage = + [[FIRIAMClearcutLogStorage alloc] initWithExpireAfterInSeconds:1000 + withTimeFetcher:self.mockTimeFetcher]; + + FIRIAMClearcutUploader *uploader = + [[FIRIAMClearcutUploader alloc] initWithRequestSender:self.mockRequestSender + timeFetcher:self.mockTimeFetcher + logStorage:logStorage + usingStrategy:self.defaultStrategy + usingUserDefaults:self.mockUserDefaults]; + + // so the following setup is to say that we need at least 5 seconds from now to attempt the + // the uploading + NSTimeInterval currentMoment = 10000; + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(currentMoment); + uploader.nextValidSendTimeInMills = (int64_t)(currentMoment + 5) * 1000; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Triggers send on sender"]; + + FIRIAMClearcutLogRecord *newRecord = + [[FIRIAMClearcutLogRecord alloc] initWithExtensionJsonString:@"string" + eventTimestampInSeconds:currentMoment]; + + __block BOOL sendingAttempted = NO; + // we don't expect sendClearcutHttpRequestForLogs:withCompletion: to be triggered + // after wait for 2.0 seconds below. We have a BOOL flag to be used for that kind verification + // checking + OCMStub([self.mockRequestSender sendClearcutHttpRequestForLogs:[OCMArg any] + withCompletion:[OCMArg any]]) + .andDo(^(NSInvocation *invocation) { + sendingAttempted = YES; + }); + [uploader addNewLogRecord:newRecord]; + + // we wait for 2 seconds and we expect nothing should happen to self.mockRequestSender right after + // 2 seconds: the upload will eventually be attempted in after 10 seconds based on the setup + // in this unit test + double delayInSeconds = 2.0; + dispatch_time_t popTime = + dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); + dispatch_after(popTime, dispatch_get_main_queue(), ^(void) { + [expectation fulfill]; + }); + + // we expect expectation to be fulfilled right away since the upload can be carried out without + // delay + [self waitForExpectationsWithTimeout:10.0 handler:nil]; + XCTAssertFalse(sendingAttempted); +} + +- (void)testUploadBatchSizeIsBasedOnStrategySetting { + int batchSendSize = 5; + + // using a strategy with batch send size as 5 + FIRIAMClearcutStrategy *strategy = + [[FIRIAMClearcutStrategy alloc] initWithMinWaitTimeInMills:1000 + maxWaitTimeInMills:2000 + failureBackoffTimeInMills:1000 + batchSendSize:batchSendSize]; + + FIRIAMClearcutUploader *uploader = + [[FIRIAMClearcutUploader alloc] initWithRequestSender:self.mockRequestSender + timeFetcher:self.mockTimeFetcher + logStorage:self.mockLogStorage + usingStrategy:strategy + usingUserDefaults:self.mockUserDefaults]; + + // so the following setup is to say that next upload is going to be now + NSTimeInterval currentMoment = 10000; + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(currentMoment); + uploader.nextValidSendTimeInMills = (int64_t)currentMoment * 1000; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Triggers send on sender"]; + OCMExpect([self.mockLogStorage popStillValidRecordsForUpTo:batchSendSize]); + + FIRIAMClearcutLogRecord *newRecord = + [[FIRIAMClearcutLogRecord alloc] initWithExtensionJsonString:@"string" + eventTimestampInSeconds:currentMoment]; + [uploader addNewLogRecord:newRecord]; + + // we wait for 2 seconds to ensure that the next send is attempted and then verify its + // interacton with the underlying storage + double delayInSeconds = 2.0; + dispatch_time_t popTime = + dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); + dispatch_after(popTime, dispatch_get_main_queue(), ^(void) { + [expectation fulfill]; + }); + + // we expect expectation to be fulfilled right away since the upload can be carried out without + // delay + [self waitForExpectationsWithTimeout:10.0 handler:nil]; + OCMVerifyAll((id)self.mockLogStorage); +} + +- (void)testRespectingWaitTimeFromRequestSender { + // using a real storage in this case + FIRIAMClearcutLogStorage *logStorage = + [[FIRIAMClearcutLogStorage alloc] initWithExpireAfterInSeconds:1000 + withTimeFetcher:self.mockTimeFetcher]; + + FIRIAMClearcutUploader *uploader = + [[FIRIAMClearcutUploader alloc] initWithRequestSender:self.mockRequestSender + timeFetcher:self.mockTimeFetcher + logStorage:logStorage + usingStrategy:self.defaultStrategy + usingUserDefaults:self.mockUserDefaults]; + + // so the following setup is to say that next upload is going to be now + NSTimeInterval currentMoment = 10000; + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(currentMoment); + uploader.nextValidSendTimeInMills = (int64_t)currentMoment * 1000; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Triggers send on sender"]; + + // notice that waitTime is between minWaitTimeInMills and maxWaitTimeInMills in the default + // strategy + NSNumber *waitTime = [NSNumber numberWithLongLong:1500]; + // set up request sender which triggers the callback with a wait time interval to be 1000 + // milliseconds + OCMStub( + [self.mockRequestSender + sendClearcutHttpRequestForLogs:[OCMArg any] + withCompletion:([OCMArg invokeBlockWithArgs:@YES, @NO, waitTime, nil])]) + .andDo(^(NSInvocation *invocation) { + [expectation fulfill]; + }); + + FIRIAMClearcutLogRecord *newRecord = + [[FIRIAMClearcutLogRecord alloc] initWithExtensionJsonString:@"string" + eventTimestampInSeconds:currentMoment]; + [uploader addNewLogRecord:newRecord]; + + [self waitForExpectationsWithTimeout:10.0 handler:nil]; + // verify the update to nextValidSendTimeInMills is expected + XCTAssertEqual(currentMoment * 1000 + 1500, uploader.nextValidSendTimeInMills); +} + +- (void)testWaitTimeFromRequestSenderAdjustedByMinWaitTimeInStrategy { + // using a real storage in this case + FIRIAMClearcutLogStorage *logStorage = + [[FIRIAMClearcutLogStorage alloc] initWithExpireAfterInSeconds:1000 + withTimeFetcher:self.mockTimeFetcher]; + + FIRIAMClearcutUploader *uploader = + [[FIRIAMClearcutUploader alloc] initWithRequestSender:self.mockRequestSender + timeFetcher:self.mockTimeFetcher + logStorage:logStorage + usingStrategy:self.defaultStrategy + usingUserDefaults:self.mockUserDefaults]; + + // so the following setup is to say that next upload is going to be now + NSTimeInterval currentMoment = 10000; + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(currentMoment); + uploader.nextValidSendTimeInMills = (int64_t)currentMoment * 1000; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Triggers send on sender"]; + + // notice that waitTime is below minWaitTimeInMills in the default strategy + NSNumber *waitTime = + [NSNumber numberWithLongLong:self.defaultStrategy.minimalWaitTimeInMills - 200]; + // set up request sender which triggers the callback with a wait time interval to be 1000 + // milliseconds + OCMStub( + [self.mockRequestSender + sendClearcutHttpRequestForLogs:[OCMArg any] + withCompletion:([OCMArg invokeBlockWithArgs:@YES, @NO, waitTime, nil])]) + .andDo(^(NSInvocation *invocation) { + [expectation fulfill]; + }); + + FIRIAMClearcutLogRecord *newRecord = + [[FIRIAMClearcutLogRecord alloc] initWithExtensionJsonString:@"string" + eventTimestampInSeconds:currentMoment]; + [uploader addNewLogRecord:newRecord]; + + [self waitForExpectationsWithTimeout:10.0 handler:nil]; + // verify the update to nextValidSendTimeInMills is expected + XCTAssertEqual(currentMoment * 1000 + self.defaultStrategy.minimalWaitTimeInMills, + uploader.nextValidSendTimeInMills); +} + +- (void)testWaitTimeFromRequestSenderAdjustedByMaxWaitTimeInStrategy { + // using a real storage in this case + FIRIAMClearcutLogStorage *logStorage = + [[FIRIAMClearcutLogStorage alloc] initWithExpireAfterInSeconds:1000 + withTimeFetcher:self.mockTimeFetcher]; + + FIRIAMClearcutUploader *uploader = + [[FIRIAMClearcutUploader alloc] initWithRequestSender:self.mockRequestSender + timeFetcher:self.mockTimeFetcher + logStorage:logStorage + usingStrategy:self.defaultStrategy + usingUserDefaults:self.mockUserDefaults]; + + // so the following setup is to say that next upload is going to be now + NSTimeInterval currentMoment = 10000; + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(currentMoment); + uploader.nextValidSendTimeInMills = (int64_t)currentMoment * 1000; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Triggers send on sender"]; + + // notice that waitTime is larger than maximumWaitTimeInMills in the default strategy + NSNumber *waitTime = + [NSNumber numberWithLongLong:self.defaultStrategy.maximumWaitTimeInMills + 200]; + // set up request sender which triggers the callback with a wait time interval to be 1000 + // milliseconds + OCMStub( + [self.mockRequestSender + sendClearcutHttpRequestForLogs:[OCMArg any] + withCompletion:([OCMArg invokeBlockWithArgs:@YES, @NO, waitTime, nil])]) + .andDo(^(NSInvocation *invocation) { + [expectation fulfill]; + }); + + FIRIAMClearcutLogRecord *newRecord = + [[FIRIAMClearcutLogRecord alloc] initWithExtensionJsonString:@"string" + eventTimestampInSeconds:currentMoment]; + [uploader addNewLogRecord:newRecord]; + + [self waitForExpectationsWithTimeout:10.0 handler:nil]; + // verify the update to nextValidSendTimeInMills is expected + XCTAssertEqual(currentMoment * 1000 + self.defaultStrategy.maximumWaitTimeInMills, + uploader.nextValidSendTimeInMills); +} + +- (void)testRepushLogsIfRequestSenderSaysSo { + // using a real storage in this case + FIRIAMClearcutLogStorage *logStorage = + [[FIRIAMClearcutLogStorage alloc] initWithExpireAfterInSeconds:1000 + withTimeFetcher:self.mockTimeFetcher]; + + FIRIAMClearcutUploader *uploader = + [[FIRIAMClearcutUploader alloc] initWithRequestSender:self.mockRequestSender + timeFetcher:self.mockTimeFetcher + logStorage:logStorage + usingStrategy:self.defaultStrategy + usingUserDefaults:self.mockUserDefaults]; + + // so the following setup is to say that next upload is going to be now + NSTimeInterval currentMoment = 10000; + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(currentMoment); + uploader.nextValidSendTimeInMills = (int64_t)currentMoment * 1000; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Triggers send on sender"]; + + // notice that waitTime is larger than maximumWaitTimeInMills in the default strategy + NSNumber *waitTime = + [NSNumber numberWithLongLong:self.defaultStrategy.maximumWaitTimeInMills + 200]; + + // Notice that it's invoking completion with falure flag and a flag to re-push those logs + OCMStub( + [self.mockRequestSender + sendClearcutHttpRequestForLogs:[OCMArg any] + withCompletion:([OCMArg invokeBlockWithArgs:@NO, @YES, waitTime, nil])]) + .andDo(^(NSInvocation *invocation) { + [expectation fulfill]; + }); + + FIRIAMClearcutLogRecord *newRecord = + [[FIRIAMClearcutLogRecord alloc] initWithExtensionJsonString:@"string" + eventTimestampInSeconds:currentMoment]; + [uploader addNewLogRecord:newRecord]; + + [self waitForExpectationsWithTimeout:10.0 handler:nil]; + + // we should still be able to fetch one log record from storage since it's re-pushed due + // to send failure + XCTAssertEqual([logStorage popStillValidRecordsForUpTo:10].count, 1); +} +@end diff --git a/InAppMessaging/Example/Tests/FIRIAMDisplayExecutorTests.m b/InAppMessaging/Example/Tests/FIRIAMDisplayExecutorTests.m new file mode 100644 index 00000000000..f71a32abe60 --- /dev/null +++ b/InAppMessaging/Example/Tests/FIRIAMDisplayExecutorTests.m @@ -0,0 +1,761 @@ +/* + * 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 +#import + +#import "FIRIAMActionURLFollower.h" +#import "FIRIAMDisplayExecutor.h" +#import "FIRIAMDisplayTriggerDefinition.h" +#import "FIRIAMMessageContentData.h" + +// A class implementing protocol FIRIAMMessageContentData to be used for unit testing +@interface FIRIAMMessageContentDataForTesting : NSObject +@property(nonatomic, readwrite, nonnull) NSString *titleText; +@property(nonatomic, readwrite, nonnull) NSString *bodyText; +@property(nonatomic, nullable) NSString *actionButtonText; +@property(nonatomic, nullable) NSURL *actionURL; +@property(nonatomic, nullable) NSURL *imageURL; +@property BOOL errorEncountered; + +- (instancetype)initWithMessageTitle:(NSString *)title + messageBody:(NSString *)body + actionButtonText:(nullable NSString *)actionButtonText + actionURL:(nullable NSURL *)actionURL + imageURL:(nullable NSURL *)imageURL + hasImageError:(BOOL)hasImageError; +@end + +@implementation FIRIAMMessageContentDataForTesting +- (instancetype)initWithMessageTitle:(NSString *)title + messageBody:(NSString *)body + actionButtonText:(nullable NSString *)actionButtonText + actionURL:(nullable NSURL *)actionURL + imageURL:(nullable NSURL *)imageURL + hasImageError:(BOOL)hasImageError { + if (self = [super init]) { + _titleText = title; + _bodyText = body; + _imageURL = imageURL; + _actionButtonText = actionButtonText; + _actionURL = actionURL; + _errorEncountered = hasImageError; + } + return self; +} + +- (void)loadImageDataWithBlock:(void (^)(NSData *_Nullable imageData, + NSError *_Nullable error))block { + if (self.errorEncountered) { + block(nil, [NSError errorWithDomain:@"image error" code:0 userInfo:nil]); + } else { + NSString *str = @"image data"; + NSData *data = [str dataUsingEncoding:NSUTF8StringEncoding]; + block(data, nil); + } +} +@end + +// Defines how the message display component triggers the delegate in unit testing +typedef NS_ENUM(NSInteger, FIRInAppMessagingDelegateInteraction) { + FIRInAppMessagingDelegateInteractionDismiss, // message display component triggers + // messageDismissedWithType: + FIRInAppMessagingDelegateInteractionClick, // message display component triggers + // messageClicked: + FIRInAppMessagingDelegateInteractionError, // message display component triggers + // displayErrorEncountered: + FIRInAppMessagingDelegateInteractionImpressionDetected, // message has finished a valid + // impression, but it's not getting + // closed by the user. +}; + +// A class implementing protocol FIRInAppMessagingDisplay to be used for unit testing +@interface FIRIAMMessageDisplayForTesting : NSObject +@property FIRInAppMessagingDelegateInteraction delegateInteraction; + +// used for interaction verificatio +@property FIRInAppMessagingDisplayMessageBase *message; +- (instancetype)initWithDelegateInteraction:(FIRInAppMessagingDelegateInteraction)interaction; +@end + +@implementation FIRIAMMessageDisplayForTesting +- (instancetype)initWithDelegateInteraction:(FIRInAppMessagingDelegateInteraction)interaction { + if (self = [super init]) { + _delegateInteraction = interaction; + } + return self; +} + +- (void)displayMessage:(FIRInAppMessagingDisplayMessageBase *)messageForDisplay + displayDelegate:(id)displayDelegate { + self.message = messageForDisplay; + + switch (self.delegateInteraction) { + case FIRInAppMessagingDelegateInteractionClick: + [displayDelegate messageClicked]; + break; + case FIRInAppMessagingDelegateInteractionDismiss: + [displayDelegate messageDismissedWithType:FIRInAppMessagingDismissTypeAuto]; + break; + case FIRInAppMessagingDelegateInteractionError: + [displayDelegate displayErrorEncountered:[NSError errorWithDomain:NSURLErrorDomain + code:0 + userInfo:nil]]; + break; + case FIRInAppMessagingDelegateInteractionImpressionDetected: + [displayDelegate impressionDetected]; + break; + } +} +@end + +@interface FIRIAMDisplayExecutorTests : XCTestCase + +@property(nonatomic) FIRIAMDisplaySetting *displaySetting; +@property FIRIAMMessageClientCache *clientMessageCache; +@property id mockBookkeeper; +@property id mockTimeFetcher; + +@property FIRIAMDisplayExecutor *displayExecutor; + +@property FIRIAMActivityLogger *mockActivityLogger; + +@property id mockAnalyticsEventLogger; + +@property FIRIAMActionURLFollower *mockActionURLFollower; + +@property id mockMessageDisplayComponent; + +// three pre-defined messages +@property FIRIAMMessageDefinition *m1, *m2, *m3, *m4; +@end + +@implementation FIRIAMDisplayExecutorTests + +- (void)setupMessageTexture { + // startTime, endTime here ensures messages with them are active + NSTimeInterval activeStartTime = 0; + NSTimeInterval activeEndTime = [[NSDate date] timeIntervalSince1970] + 10000; + + // m1 & m3 will be of contextual trigger + FIRIAMDisplayTriggerDefinition *contextualTriggerDefinition = + [[FIRIAMDisplayTriggerDefinition alloc] initWithFirebaseAnalyticEvent:@"test_event"]; + + // m2 and m4 will be of app open trigger + FIRIAMDisplayTriggerDefinition *appOpentriggerDefinition = + [[FIRIAMDisplayTriggerDefinition alloc] initForAppForegroundTrigger]; + + FIRIAMMessageContentDataForTesting *m1ContentData = [[FIRIAMMessageContentDataForTesting alloc] + initWithMessageTitle:@"m1 title" + messageBody:@"message body" + actionButtonText:nil + actionURL:[NSURL URLWithString:@"http://google.com"] + imageURL:[NSURL URLWithString:@"https://google.com/image"] + hasImageError:NO]; + + FIRIAMRenderingEffectSetting *renderSetting1 = + [FIRIAMRenderingEffectSetting getDefaultRenderingEffectSetting]; + renderSetting1.viewMode = FIRIAMRenderAsBannerView; + + FIRIAMMessageRenderData *renderData1 = + [[FIRIAMMessageRenderData alloc] initWithMessageID:@"m1" + messageName:@"name" + contentData:m1ContentData + renderingEffect:renderSetting1]; + + self.m1 = [[FIRIAMMessageDefinition alloc] initWithRenderData:renderData1 + startTime:activeStartTime + endTime:activeEndTime + triggerDefinition:@[ contextualTriggerDefinition ]]; + + FIRIAMMessageContentDataForTesting *m2ContentData = [[FIRIAMMessageContentDataForTesting alloc] + initWithMessageTitle:@"m2 title" + messageBody:@"message body" + actionButtonText:nil + actionURL:[NSURL URLWithString:@"http://google.com"] + imageURL:[NSURL URLWithString:@"https://unsplash.it/300/400"] + hasImageError:NO]; + + FIRIAMRenderingEffectSetting *renderSetting2 = + [FIRIAMRenderingEffectSetting getDefaultRenderingEffectSetting]; + renderSetting2.viewMode = FIRIAMRenderAsModalView; + + FIRIAMMessageRenderData *renderData2 = + [[FIRIAMMessageRenderData alloc] initWithMessageID:@"m2" + messageName:@"name" + contentData:m2ContentData + renderingEffect:renderSetting2]; + + self.m2 = [[FIRIAMMessageDefinition alloc] initWithRenderData:renderData2 + startTime:activeStartTime + endTime:activeEndTime + triggerDefinition:@[ appOpentriggerDefinition ]]; + + FIRIAMMessageContentDataForTesting *m3ContentData = [[FIRIAMMessageContentDataForTesting alloc] + initWithMessageTitle:@"m3 title" + messageBody:@"message body" + actionButtonText:nil + actionURL:[NSURL URLWithString:@"http://google.com"] + imageURL:[NSURL URLWithString:@"https://google.com/image"] + hasImageError:NO]; + + FIRIAMRenderingEffectSetting *renderSetting3 = + [FIRIAMRenderingEffectSetting getDefaultRenderingEffectSetting]; + renderSetting3.viewMode = FIRIAMRenderAsImageOnlyView; + + FIRIAMMessageRenderData *renderData3 = + [[FIRIAMMessageRenderData alloc] initWithMessageID:@"m3" + messageName:@"name" + contentData:m3ContentData + renderingEffect:renderSetting3]; + + self.m3 = [[FIRIAMMessageDefinition alloc] initWithRenderData:renderData3 + startTime:activeStartTime + endTime:activeEndTime + triggerDefinition:@[ contextualTriggerDefinition ]]; + + FIRIAMMessageContentDataForTesting *m4ContentData = [[FIRIAMMessageContentDataForTesting alloc] + initWithMessageTitle:@"m4 title" + messageBody:@"message body" + actionButtonText:nil + actionURL:[NSURL URLWithString:@"http://google.com"] + imageURL:[NSURL URLWithString:@"https://google.com/image"] + hasImageError:NO]; + + FIRIAMRenderingEffectSetting *renderSetting4 = + [FIRIAMRenderingEffectSetting getDefaultRenderingEffectSetting]; + renderSetting4.viewMode = FIRIAMRenderAsImageOnlyView; + + FIRIAMMessageRenderData *renderData4 = + [[FIRIAMMessageRenderData alloc] initWithMessageID:@"m4" + messageName:@"name" + contentData:m4ContentData + renderingEffect:renderSetting4]; + + self.m4 = [[FIRIAMMessageDefinition alloc] initWithRenderData:renderData4 + startTime:activeStartTime + endTime:activeEndTime + triggerDefinition:@[ appOpentriggerDefinition ]]; +} + +NSTimeInterval DISPLAY_MIN_INTERVALS = 1; + +- (void)setUp { + [super setUp]; + [self setupMessageTexture]; + + self.displaySetting = [[FIRIAMDisplaySetting alloc] init]; + self.displaySetting.displayMinIntervalInMinutes = DISPLAY_MIN_INTERVALS; + self.mockBookkeeper = OCMProtocolMock(@protocol(FIRIAMBookKeeper)); + + FIRIAMFetchResponseParser *parser = + [[FIRIAMFetchResponseParser alloc] initWithTimeFetcher:[[FIRIAMTimerWithNSDate alloc] init]]; + + self.clientMessageCache = [[FIRIAMMessageClientCache alloc] initWithBookkeeper:self.mockBookkeeper + usingResponseParser:parser]; + self.mockTimeFetcher = OCMProtocolMock(@protocol(FIRIAMTimeFetcher)); + self.mockActivityLogger = OCMClassMock([FIRIAMActivityLogger class]); + self.mockAnalyticsEventLogger = OCMProtocolMock(@protocol(FIRIAMAnalyticsEventLogger)); + + self.mockActionURLFollower = OCMClassMock([FIRIAMActionURLFollower class]); + + self.displayExecutor = + [[FIRIAMDisplayExecutor alloc] initWithSetting:self.displaySetting + messageCache:self.clientMessageCache + timeFetcher:self.mockTimeFetcher + bookKeeper:self.mockBookkeeper + actionURLFollower:self.mockActionURLFollower + activityLogger:self.mockActivityLogger + analyticsEventLogger:self.mockAnalyticsEventLogger]; + + OCMStub([self.mockBookkeeper recordNewImpressionForMessage:[OCMArg any] + withStartTimestampInSeconds:1000]); +} + +- (void)tearDown { + // Put teardown code here. This method is called after the invocation of each test method in the + // class. + [super tearDown]; +} + +- (void)testRegularMessageAvailableCase { + // This setup allows next message to be displayed from display interval perspective. + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]) + .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100); + + FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc] + initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick]; + self.displayExecutor.messageDisplayComponent = display; + [self.clientMessageCache setMessageData:@[ self.m2, self.m4 ]]; + [self.displayExecutor checkAndDisplayNextAppForegroundMessage]; + NSInteger remainingMsgCount = [self.clientMessageCache allRegularMessages].count; + XCTAssertEqual(1, remainingMsgCount); + + // Verify that the message content handed to display component is expected + XCTAssertEqualObjects(self.m2.renderData.messageID, display.message.messageID); +} + +- (void)testFollowingActionURL { + // This setup allows next message to be displayed from display interval perspective. + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]) + .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100); + + FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc] + initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick]; + self.displayExecutor.messageDisplayComponent = display; + [self.clientMessageCache setMessageData:@[ self.m2 ]]; + + // not expecting triggering analytics recording + OCMExpect([self.mockActionURLFollower + followActionURL:[OCMArg isEqual:self.m2.renderData.contentData.actionURL] + withCompletionBlock:[OCMArg any]]); + [self.displayExecutor checkAndDisplayNextAppForegroundMessage]; + + OCMVerifyAll((id)self.mockActionURLFollower); +} + +- (void)testFollowingActionURLForTestMessage { + // This setup allows next message to be displayed from display interval perspective. + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]) + .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100); + + FIRIAMMessageDefinition *testMessage = + [[FIRIAMMessageDefinition alloc] initTestMessageWithRenderData:self.m1.renderData]; + + FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc] + initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick]; + self.displayExecutor.messageDisplayComponent = display; + [self.clientMessageCache setMessageData:@[ testMessage ]]; + + // not expecting triggering analytics recording + OCMExpect([self.mockActionURLFollower + followActionURL:[OCMArg isEqual:testMessage.renderData.contentData.actionURL] + withCompletionBlock:[OCMArg any]]); + [self.displayExecutor checkAndDisplayNextAppForegroundMessage]; + + OCMVerifyAll((id)self.mockActionURLFollower); +} + +- (void)testClientTestMessageAvailableCase { + // When test message is present in cache, even if the display time interval has not been + // reached, we still render. + + // 10 seconds is less than DISPLAY_MIN_INTERVALS minutes, so we have not reached + // minimal display time interval yet. + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(10); + FIRIAMMessageDefinition *testMessage = + [[FIRIAMMessageDefinition alloc] initTestMessageWithRenderData:self.m1.renderData]; + + [self.clientMessageCache setMessageData:@[ self.m2, testMessage, self.m4 ]]; + + // We have test message in the cache now. + XCTAssertTrue([self.clientMessageCache hasTestMessage]); + + FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc] + initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick]; + self.displayExecutor.messageDisplayComponent = display; + + [self.displayExecutor checkAndDisplayNextAppForegroundMessage]; + + // No more test message in the cache now. + XCTAssertFalse([self.clientMessageCache hasTestMessage]); +} + +// If a message is still being displayed, we won't try to display a second one on top of it +- (void)testNoDualDisplay { + // This setup allows next message to be displayed from display interval perspective. + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]) + .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100); + + // This display component only detects a valid impression, but does not end the renderig + FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc] + initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionImpressionDetected]; + self.displayExecutor.messageDisplayComponent = display; + + [self.clientMessageCache setMessageData:@[ self.m2, self.m4 ]]; + [self.displayExecutor checkAndDisplayNextAppForegroundMessage]; + + // m2 is being rendered + XCTAssertEqualObjects(self.m2.renderData.messageID, display.message.messageID); + + NSInteger remainingMsgCount = [self.clientMessageCache allRegularMessages].count; + XCTAssertEqual(1, remainingMsgCount); + + // try to display again when the in-display flag is already turned on (and not turned off yet) + [self.displayExecutor checkAndDisplayNextAppForegroundMessage]; + + // Verify that the message in display component is still m2 + XCTAssertEqualObjects(self.m2.renderData.messageID, display.message.messageID); + + // message in cache remain unchanged for the second checkAndDisplayNext call + remainingMsgCount = [self.clientMessageCache allRegularMessages].count; + XCTAssertEqual(1, remainingMsgCount); +} + +// this test case contracts testNoAnalyticsTrackingOnTestMessage to cover both positive +// and negative cases +- (void)testDoesAnalyticsTrackingOnNonTestMessage { + // This setup allows next message to be displayed from display interval perspective. + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]) + .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100); + + // not expecting triggering analytics recording + OCMExpect([self.mockAnalyticsEventLogger + logAnalyticsEventForType:FIRIAMAnalyticsEventMessageImpression + forCampaignID:[OCMArg isEqual:self.m2.renderData.messageID] + withCampaignName:[OCMArg any] + eventTimeInMs:[OCMArg any] + completion:[OCMArg any]]); + [self.clientMessageCache setMessageData:@[ self.m2 ]]; + + FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc] + initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick]; + self.displayExecutor.messageDisplayComponent = display; + [self.displayExecutor checkAndDisplayNextAppForegroundMessage]; + OCMVerifyAll((id)self.mockAnalyticsEventLogger); +} + +- (void)testDoesAnalyticsTrackingOnDisplayError { + // 1000 seconds is larger than DISPLAY_MIN_INTERVALS minutes + // last display time is set to 0 by default + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(1000); + + // not expecting triggering analytics recording + OCMExpect([self.mockAnalyticsEventLogger + logAnalyticsEventForType:FIRIAMAnalyticsEventImageFetchError + forCampaignID:[OCMArg isEqual:self.m2.renderData.messageID] + withCampaignName:[OCMArg any] + eventTimeInMs:[OCMArg any] + completion:[OCMArg any]]); + + [self.clientMessageCache setMessageData:@[ self.m2 ]]; + + FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc] + initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionError]; + self.displayExecutor.messageDisplayComponent = display; + + [self.displayExecutor checkAndDisplayNextAppForegroundMessage]; + OCMVerifyAll((id)self.mockAnalyticsEventLogger); +} + +- (void)testAnalyticsTrackingOnMessageDismissCase { + // This setup allows next message to be displayed from display interval perspective. + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]) + .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100); + + // not expecting triggering analytics recording + OCMExpect([self.mockAnalyticsEventLogger + logAnalyticsEventForType:FIRIAMAnalyticsEventMessageDismissAuto + forCampaignID:[OCMArg isEqual:self.m2.renderData.messageID] + withCampaignName:[OCMArg any] + eventTimeInMs:[OCMArg any] + completion:[OCMArg any]]); + + // Make sure we don't log the url follow event. + OCMReject([self.mockAnalyticsEventLogger + logAnalyticsEventForType:FIRIAMAnalyticsEventActionURLFollow + forCampaignID:[OCMArg isEqual:self.m2.renderData.messageID] + withCampaignName:[OCMArg any] + eventTimeInMs:[OCMArg any] + completion:[OCMArg any]]); + + [self.clientMessageCache setMessageData:@[ self.m2 ]]; + + FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc] + initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionDismiss]; + self.displayExecutor.messageDisplayComponent = display; + + [self.displayExecutor checkAndDisplayNextAppForegroundMessage]; + OCMVerifyAll((id)self.mockAnalyticsEventLogger); +} + +- (void)testAnalyticsTrackingOnMessageClickCase { + // This setup allows next message to be displayed from display interval perspective. + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]) + .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100); + + // We expect two analytics events for a click action: + // An impression event and an action URL follow event + OCMExpect([self.mockAnalyticsEventLogger + logAnalyticsEventForType:FIRIAMAnalyticsEventMessageImpression + forCampaignID:[OCMArg isEqual:self.m2.renderData.messageID] + withCampaignName:[OCMArg any] + eventTimeInMs:[OCMArg any] + completion:[OCMArg any]]); + + OCMExpect([self.mockAnalyticsEventLogger + logAnalyticsEventForType:FIRIAMAnalyticsEventActionURLFollow + forCampaignID:[OCMArg isEqual:self.m2.renderData.messageID] + withCampaignName:[OCMArg any] + eventTimeInMs:[OCMArg any] + completion:[OCMArg any]]); + + [self.clientMessageCache setMessageData:@[ self.m2 ]]; + + FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc] + initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick]; + self.displayExecutor.messageDisplayComponent = display; + + [self.displayExecutor checkAndDisplayNextAppForegroundMessage]; + OCMVerifyAll((id)self.mockAnalyticsEventLogger); +} + +- (void)testAnalyticsTrackingOnTestMessageClickCase { + // 1000 seconds is larger than DISPLAY_MIN_INTERVALS minutes + // last display time is set to 0 by default + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(1000); + + // We expect two analytics events for a click action: + // An test message impression event and a test message click event + OCMExpect([self.mockAnalyticsEventLogger + logAnalyticsEventForType:FIRIAMAnalyticsEventTestMessageImpression + forCampaignID:[OCMArg isEqual:self.m2.renderData.messageID] + withCampaignName:[OCMArg any] + eventTimeInMs:[OCMArg any] + completion:[OCMArg any]]); + + OCMExpect([self.mockAnalyticsEventLogger + logAnalyticsEventForType:FIRIAMAnalyticsEventTestMessageClick + forCampaignID:[OCMArg isEqual:self.m2.renderData.messageID] + withCampaignName:[OCMArg any] + eventTimeInMs:[OCMArg any] + completion:[OCMArg any]]); + + FIRIAMMessageDefinition *testMessage = + [[FIRIAMMessageDefinition alloc] initTestMessageWithRenderData:self.m2.renderData]; + + [self.clientMessageCache setMessageData:@[ testMessage ]]; + + FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc] + initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick]; + self.displayExecutor.messageDisplayComponent = display; + + [self.displayExecutor checkAndDisplayNextAppForegroundMessage]; + OCMVerifyAll((id)self.mockAnalyticsEventLogger); +} + +- (void)testAnalyticsTrackingOnTestMessageDismissCase { + // 1000 seconds is larger than DISPLAY_MIN_INTERVALS minutes + // last display time is set to 0 by default + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(1000); + + // We expect a test message impression + OCMExpect([self.mockAnalyticsEventLogger + logAnalyticsEventForType:FIRIAMAnalyticsEventTestMessageImpression + forCampaignID:[OCMArg isEqual:self.m2.renderData.messageID] + withCampaignName:[OCMArg any] + eventTimeInMs:[OCMArg any] + completion:[OCMArg any]]); + // No click event + OCMReject([self.mockAnalyticsEventLogger + logAnalyticsEventForType:FIRIAMAnalyticsEventTestMessageClick + forCampaignID:[OCMArg isEqual:self.m2.renderData.messageID] + withCampaignName:[OCMArg any] + eventTimeInMs:[OCMArg any] + completion:[OCMArg any]]); + + FIRIAMMessageDefinition *testMessage = + [[FIRIAMMessageDefinition alloc] initTestMessageWithRenderData:self.m2.renderData]; + + [self.clientMessageCache setMessageData:@[ testMessage ]]; + + FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc] + initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionDismiss]; + self.displayExecutor.messageDisplayComponent = display; + + [self.displayExecutor checkAndDisplayNextAppForegroundMessage]; + OCMVerifyAll((id)self.mockAnalyticsEventLogger); +} + +- (void)testAnalyticsTrackingImpressionOnValidImpressionDetectedCase { + // This setup allows next message to be displayed from display interval perspective. + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]) + .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100); + + // not expecting triggering analytics recording + OCMExpect([self.mockAnalyticsEventLogger + logAnalyticsEventForType:FIRIAMAnalyticsEventMessageImpression + forCampaignID:[OCMArg isEqual:self.m2.renderData.messageID] + withCampaignName:[OCMArg any] + eventTimeInMs:[OCMArg any] + completion:[OCMArg any]]); + + [self.clientMessageCache setMessageData:@[ self.m2 ]]; + + FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc] + initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionImpressionDetected]; + self.displayExecutor.messageDisplayComponent = display; + + [self.displayExecutor checkAndDisplayNextAppForegroundMessage]; + OCMVerifyAll((id)self.mockAnalyticsEventLogger); +} + +- (void)testNoAnalyticsTrackingOnTestMessage { + // This setup allows next message to be displayed from display interval perspective. + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]) + .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100); + + FIRIAMMessageDefinition *testMessage = + [[FIRIAMMessageDefinition alloc] initTestMessageWithRenderData:self.m1.renderData]; + + // not expecting triggering analytics recording + OCMReject([self.mockAnalyticsEventLogger + logAnalyticsEventForType:FIRIAMAnalyticsEventMessageImpression + forCampaignID:[OCMArg isEqual:self.m1.renderData.messageID] + withCampaignName:[OCMArg any] + eventTimeInMs:[OCMArg any] + completion:[OCMArg any]]); + [self.clientMessageCache setMessageData:@[ testMessage ]]; + + FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc] + initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick]; + self.displayExecutor.messageDisplayComponent = display; + [self.displayExecutor checkAndDisplayNextAppForegroundMessage]; + OCMVerifyAll((id)self.mockAnalyticsEventLogger); +} + +- (void)testNoMessageAvailableCase { + // This setup allows next message to be displayed from display interval perspective. + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]) + .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100); + + FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc] + initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick]; + self.displayExecutor.messageDisplayComponent = display; + [self.clientMessageCache setMessageData:@[]]; + [self.displayExecutor checkAndDisplayNextAppForegroundMessage]; + + // No display has happened so the message stored in the display component should be nil + XCTAssertNil(display.message); + NSInteger remainingMsgCount = [self.clientMessageCache allRegularMessages].count; + XCTAssertEqual(0, remainingMsgCount); +} + +- (void)testIntervalBetweenOnAppOpenDisplays { + self.displaySetting.displayMinIntervalInMinutes = 10; + + // last display time is set to 0 by default + // 10 seconds is not long enough for satisfying the 10-min internal requirement + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(10); + + FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc] + initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick]; + self.displayExecutor.messageDisplayComponent = display; + + [self.clientMessageCache setMessageData:@[ self.m1 ]]; + [self.displayExecutor checkAndDisplayNextAppForegroundMessage]; + + NSInteger remainingMsgCount = [self.clientMessageCache allRegularMessages].count; + // No display has happened so the message stored in the display component should be nil + XCTAssertNil(display.message); + + // still got one in the queue + XCTAssertEqual(1, remainingMsgCount); +} + +// making sure that we match on the event names for analytics based events +- (void)testOnFirebaseAnalyticsEventDisplayMessages { + // This setup allows next message to be displayed from display interval perspective. + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]) + .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100); + + FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc] + initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick]; + self.displayExecutor.messageDisplayComponent = display; + + // m1 and m3 are messages triggered by 'test_event' analytics events + [self.clientMessageCache setMessageData:@[ self.m1, self.m3 ]]; + + [self.displayExecutor checkAndDisplayNextContextualMessageForAnalyticsEvent:@"different event"]; + NSInteger remainingMsgCount = [self.clientMessageCache allRegularMessages].count; + + // No message matching event "different event", so no message is nil + XCTAssertNil(display.message); + // still got 2 in the queue + XCTAssertEqual(2, remainingMsgCount); + + // now trigger it with 'test_event' and we would expect one message to be displayed and removed + // from cache + [self.displayExecutor checkAndDisplayNextContextualMessageForAnalyticsEvent:@"test_event"]; + // Expecting the m1 being used for display + XCTAssertEqualObjects(self.m1.renderData.messageID, display.message.messageID); + + remainingMsgCount = [self.clientMessageCache allRegularMessages].count; + + // Now only one message remaining in the queue + XCTAssertEqual(1, remainingMsgCount); +} + +// no regular message rendering if suppress message display flag is turned on +- (void)testNoRenderingIfMessageDisplayIsSuppressed { + // This setup allows next message to be displayed from display interval perspective. + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]) + .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100); + + [self.clientMessageCache setMessageData:@[ self.m2, self.m4 ]]; + + FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc] + initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick]; + self.displayExecutor.messageDisplayComponent = display; + self.displayExecutor.suppressMessageDisplay = YES; + [self.displayExecutor checkAndDisplayNextAppForegroundMessage]; + + // no message display has happened + XCTAssertNil(display.message); + + NSInteger remainingMsgCount = [self.clientMessageCache allRegularMessages].count; + // no message is removed from the cache + XCTAssertEqual(2, remainingMsgCount); + + // now allow message rendering again + self.displayExecutor.suppressMessageDisplay = NO; + [self.displayExecutor checkAndDisplayNextAppForegroundMessage]; + NSInteger remainingMsgCount2 = [self.clientMessageCache allRegularMessages].count; + // one message was rendered and removed from the cache + XCTAssertEqual(1, remainingMsgCount2); +} + +// No contextual message rendering if suppress message display flag is turned on +- (void)testNoContextualMsgRenderingIfMessageDisplayIsSuppressed { + // This setup allows next message to be displayed from display interval perspective. + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]) + .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100); + + [self.clientMessageCache setMessageData:@[ self.m1, self.m3 ]]; + + FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc] + initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick]; + self.displayExecutor.messageDisplayComponent = display; + + self.displayExecutor.suppressMessageDisplay = YES; + [self.displayExecutor checkAndDisplayNextContextualMessageForAnalyticsEvent:@"test_event"]; + + // no message display has happened + XCTAssertNil(display.message); + + NSInteger remainingMsgCount = [self.clientMessageCache allRegularMessages].count; + // No message is removed from the cache. + XCTAssertEqual(2, remainingMsgCount); + + // now re-enable message rendering again + self.displayExecutor.suppressMessageDisplay = NO; + [self.displayExecutor checkAndDisplayNextContextualMessageForAnalyticsEvent:@"test_event"]; + + NSInteger remainingMsgCount2 = [self.clientMessageCache allRegularMessages].count; + // one message was rendered and removed from the cache + XCTAssertEqual(1, remainingMsgCount2); +} +@end diff --git a/InAppMessaging/Example/Tests/FIRIAMElapsedTimeTrackerTests.m b/InAppMessaging/Example/Tests/FIRIAMElapsedTimeTrackerTests.m new file mode 100644 index 00000000000..1e69b430d09 --- /dev/null +++ b/InAppMessaging/Example/Tests/FIRIAMElapsedTimeTrackerTests.m @@ -0,0 +1,72 @@ +/* + * 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 +#import + +#import "FIRIAMElapsedTimeTracker.h" + +@interface FIRIAMElapsedTimeTrackerTests : XCTestCase +@property id mockTimeFetcher; +@property FIRIAMElapsedTimeTracker *tracker; + +@end + +@implementation FIRIAMElapsedTimeTrackerTests + +- (void)setUp { + [super setUp]; + self.mockTimeFetcher = OCMProtocolMock(@protocol(FIRIAMTimeFetcher)); +} + +- (void)tearDown { + // Put teardown code here. This method is called after the invocation of each test method in the + // class. + [super tearDown]; +} + +- (void)testTrackingTimeWithPauses { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + + // set up the time moments to be returned + // 0 start + // 15 pause + // 20 resume + // 30 measure the total tracked time + // given the above sequence, + // at time = 30 seconds, we expect the tracked time to be 15 + (30 - 20) = 25 seconds + + NSArray *currentTimes = @[ + [NSNumber numberWithDouble:0], [NSNumber numberWithDouble:15], [NSNumber numberWithDouble:20], + [NSNumber numberWithDouble:30] + ]; + __block int nextTimeToReturn = 0; + + // start with timestamp as 0 + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andDo(^(NSInvocation *invocation) { + NSTimeInterval time = [currentTimes[nextTimeToReturn++] doubleValue]; + [invocation setReturnValue:&time]; + }); + + self.tracker = [[FIRIAMElapsedTimeTracker alloc] initWithTimeFetcher:_mockTimeFetcher]; + [self.tracker pause]; + [self.tracker resume]; + + NSTimeInterval trackedTime = [self.tracker trackedTimeSoFar]; + XCTAssertEqualWithAccuracy(25, trackedTime, 0.01); +} +@end diff --git a/InAppMessaging/Example/Tests/FIRIAMFetchFlowTests.m b/InAppMessaging/Example/Tests/FIRIAMFetchFlowTests.m new file mode 100644 index 00000000000..65eaffee468 --- /dev/null +++ b/InAppMessaging/Example/Tests/FIRIAMFetchFlowTests.m @@ -0,0 +1,331 @@ +/* + * 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 + +#import + +#import "FIRIAMAnalyticsEventLogger.h" +#import "FIRIAMDisplayTriggerDefinition.h" +#import "FIRIAMFetchFlow.h" +#import "FIRIAMMessageContentDataWithImageURL.h" +#import "FIRIAMSDKModeManager.h" + +@interface FIRIAMFetchFlowTests : XCTestCase +@property(nonatomic) FIRIAMFetchSetting *fetchSetting; +@property FIRIAMMessageClientCache *clientMessageCache; +@property id mockMessageFetcher; +@property id mockBookkeeper; +@property id mockTimeFetcher; +@property FIRIAMFetchFlow *flow; +@property FIRIAMActivityLogger *activityLogger; +@property FIRIAMSDKModeManager *mockSDKModeManager; + +@property id mockAnaltycisEventLogger; + +// three pre-defined messages +@property FIRIAMMessageDefinition *m1, *m2, *m3; +@end + +CGFloat FETCH_MIN_INTERVALS = 1; + +@implementation FIRIAMFetchFlowTests +- (void)setupMessageTexture { + // startTime, endTime here ensures messages with them are active + NSTimeInterval activeStartTime = 0; + NSTimeInterval activeEndTime = [[NSDate date] timeIntervalSince1970] + 10000; + + FIRIAMDisplayTriggerDefinition *triggerDefinition = + [[FIRIAMDisplayTriggerDefinition alloc] initWithFirebaseAnalyticEvent:@"test_event"]; + + FIRIAMMessageContentDataWithImageURL *m1ContentData = + [[FIRIAMMessageContentDataWithImageURL alloc] + initWithMessageTitle:@"m1 title" + messageBody:@"message body" + actionButtonText:nil + actionURL:[NSURL URLWithString:@"http://google.com"] + imageURL:[NSURL URLWithString:@"https://unsplash.it/300/300"] + usingURLSession:nil]; + + FIRIAMRenderingEffectSetting *renderSetting1 = + [FIRIAMRenderingEffectSetting getDefaultRenderingEffectSetting]; + renderSetting1.viewMode = FIRIAMRenderAsBannerView; + + FIRIAMMessageRenderData *renderData1 = + [[FIRIAMMessageRenderData alloc] initWithMessageID:@"m1" + messageName:@"name" + contentData:m1ContentData + renderingEffect:renderSetting1]; + + self.m1 = [[FIRIAMMessageDefinition alloc] initWithRenderData:renderData1 + startTime:activeStartTime + endTime:activeEndTime + triggerDefinition:@[ triggerDefinition ]]; + + FIRIAMMessageContentDataWithImageURL *m2ContentData = + [[FIRIAMMessageContentDataWithImageURL alloc] + initWithMessageTitle:@"m2 title" + messageBody:@"message body" + actionButtonText:nil + actionURL:[NSURL URLWithString:@"http://google.com"] + imageURL:[NSURL URLWithString:@"https://unsplash.it/300/400"] + usingURLSession:nil]; + + FIRIAMRenderingEffectSetting *renderSetting2 = + [FIRIAMRenderingEffectSetting getDefaultRenderingEffectSetting]; + renderSetting2.viewMode = FIRIAMRenderAsModalView; + + FIRIAMMessageRenderData *renderData2 = + [[FIRIAMMessageRenderData alloc] initWithMessageID:@"m2" + messageName:@"name" + contentData:m2ContentData + renderingEffect:renderSetting2]; + + self.m2 = [[FIRIAMMessageDefinition alloc] initWithRenderData:renderData2 + startTime:activeStartTime + endTime:activeEndTime + triggerDefinition:@[ triggerDefinition ]]; + + FIRIAMMessageContentDataWithImageURL *m3ContentData = + [[FIRIAMMessageContentDataWithImageURL alloc] + initWithMessageTitle:@"m3 title" + messageBody:@"message body" + actionButtonText:nil + actionURL:[NSURL URLWithString:@"http://google.com"] + imageURL:[NSURL URLWithString:@"https://unsplash.it/400/300"] + usingURLSession:nil]; + + FIRIAMRenderingEffectSetting *renderSetting3 = + [FIRIAMRenderingEffectSetting getDefaultRenderingEffectSetting]; + renderSetting3.viewMode = FIRIAMRenderAsImageOnlyView; + + FIRIAMMessageRenderData *renderData3 = + [[FIRIAMMessageRenderData alloc] initWithMessageID:@"m3" + messageName:@"name" + contentData:m3ContentData + renderingEffect:renderSetting3]; + + self.m3 = [[FIRIAMMessageDefinition alloc] initWithRenderData:renderData3 + startTime:activeStartTime + endTime:activeEndTime + triggerDefinition:@[ triggerDefinition ]]; +} + +- (void)setUp { + [super setUp]; + [self setupMessageTexture]; + + self.fetchSetting = [[FIRIAMFetchSetting alloc] init]; + self.fetchSetting.fetchMinIntervalInMinutes = FETCH_MIN_INTERVALS; + self.mockMessageFetcher = OCMProtocolMock(@protocol(FIRIAMMessageFetcher)); + + FIRIAMFetchResponseParser *parser = + [[FIRIAMFetchResponseParser alloc] initWithTimeFetcher:[[FIRIAMTimerWithNSDate alloc] init]]; + + self.clientMessageCache = [[FIRIAMMessageClientCache alloc] initWithBookkeeper:self.mockBookkeeper + usingResponseParser:parser]; + self.mockTimeFetcher = OCMProtocolMock(@protocol(FIRIAMTimeFetcher)); + self.mockBookkeeper = OCMProtocolMock(@protocol(FIRIAMBookKeeper)); + self.activityLogger = OCMClassMock([FIRIAMActivityLogger class]); + self.mockAnaltycisEventLogger = OCMProtocolMock(@protocol(FIRIAMAnalyticsEventLogger)); + + self.mockSDKModeManager = OCMClassMock([FIRIAMSDKModeManager class]); + + self.mockTimeFetcher = OCMProtocolMock(@protocol(FIRIAMTimeFetcher)); + + self.flow = [[FIRIAMFetchFlow alloc] initWithSetting:self.fetchSetting + messageCache:self.clientMessageCache + messageFetcher:self.mockMessageFetcher + timeFetcher:self.mockTimeFetcher + bookKeeper:self.mockBookkeeper + activityLogger:self.activityLogger + analyticsEventLogger:self.mockAnaltycisEventLogger + FIRIAMSDKModeManager:self.mockSDKModeManager]; +} + +- (void)tearDown { + // Put teardown code here. This method is called after the invocation of each test method in the + // class. + [super tearDown]; +} + +// In happy path, the fetch is allowed and we are able to fetch two messages back +- (void)testHappyPath { + OCMStub([self.mockBookkeeper lastFetchTime]).andReturn(0); + + // Set it up so that we already have impressions for m1 and m3 + FIRIAMImpressionRecord *impression1 = + [[FIRIAMImpressionRecord alloc] initWithMessageID:self.m1.renderData.messageID + impressionTimeInSeconds:1233]; + + FIRIAMImpressionRecord *impression2 = [[FIRIAMImpressionRecord alloc] initWithMessageID:@"m3" + impressionTimeInSeconds:5678]; + + NSArray *impressions = @[ impression1, impression2 ]; + OCMStub([self.mockBookkeeper getImpressions]).andReturn(impressions); + + NSArray *fetchedMessages = @[ self.m1, self.m2 ]; + + // 200 seconds is larger than fetch wait time which is 100 in this setup + OCMStub([self.mockBookkeeper nextFetchWaitTime]).andReturn(100); + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(200); + + OCMStub([self.mockSDKModeManager currentMode]).andReturn(FIRIAMSDKModeRegular); + + NSNumber *fetchWaitTimeFromResponse = [NSNumber numberWithInt:2000]; + + OCMStub([self.mockMessageFetcher + fetchMessagesWithImpressionList:[OCMArg any] + withCompletion:([OCMArg invokeBlockWithArgs:fetchedMessages, + fetchWaitTimeFromResponse, + [NSNull null], [NSNull null], + nil])]); + [self.flow checkAndFetch]; + + // We expect m1 and m2 to be dumped into clientMessageCache. + NSArray *foundMessages = [self.clientMessageCache allRegularMessages]; + XCTAssertEqual(2, foundMessages.count); + XCTAssertEqualObjects(foundMessages[0].renderData.messageID, self.m1.renderData.messageID); + XCTAssertEqualObjects(foundMessages[1].renderData.messageID, self.m2.renderData.messageID); + + // Verify that we record the new fetch with bookkeeper + OCMVerify([self.mockBookkeeper recordNewFetchWithFetchCount:2 + withTimestampInSeconds:200 + nextFetchWaitTime:fetchWaitTimeFromResponse]); + + // So we are sending the request with impression for m1 and m3 and getting back messages for m1 + // and m2. In here m1 is a recurring message and after the fetch, we should call + // book keeper's clearImpressionsWithMessageList: method with m1 which is an intersection + // between the request impression list and the response message id list. We are skipping + // m2 since it's not included in the impression records sent along with the request. + OCMVerify( + [self.mockBookkeeper clearImpressionsWithMessageList:@[ self.m1.renderData.messageID ]]); +} + +// No fetch is to be performed if the required fetch interval is not met +- (void)testNoFetchDueToIntervalConstraint { + OCMStub([self.mockSDKModeManager currentMode]).andReturn(FIRIAMSDKModeRegular); + + // We need to wait at least 300 seconds before making another fetch + OCMStub([self.mockBookkeeper nextFetchWaitTime]).andReturn(300); + + // And it's only been 200 seconds since last fetch, so no fetch should happen + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(200); + OCMStub([self.mockBookkeeper lastFetchTime]).andReturn(0); + + // We don't expect fetchMessages: for self.mockMessageFetcher to be triggred + OCMReject([self.mockMessageFetcher fetchMessagesWithImpressionList:[OCMArg any] + withCompletion:[OCMArg any]]); + [self.flow checkAndFetch]; + + NSArray *foundMessages = [self.clientMessageCache allRegularMessages]; + XCTAssertEqual(0, foundMessages.count); +} + +// Fetch always in newly installed mode +- (void)testAlwaysFetchForNewlyInstalledMode { + OCMStub([self.mockBookkeeper lastFetchTime]).andReturn(0); + OCMStub([self.mockSDKModeManager currentMode]).andReturn(FIRIAMSDKModeNewlyInstalled); + OCMStub([self.mockMessageFetcher + fetchMessagesWithImpressionList:[OCMArg any] + withCompletion:([OCMArg invokeBlockWithArgs:@[ self.m1, self.m2 ], + [NSNull null], [NSNull null], + [NSNull null], nil])]); + + // 100 seconds is less than fetch wait time which is 1000 in this setup, + // but since we are in newly installed mode, fetch would still happen + OCMStub([self.mockBookkeeper nextFetchWaitTime]).andReturn(1000); + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(100); + + [self.flow checkAndFetch]; + + // we expect m1 and m2 to be dumped into clientMessageCache + NSArray *foundMessages = [self.clientMessageCache allRegularMessages]; + XCTAssertEqual(2, foundMessages.count); + XCTAssertEqualObjects(foundMessages[0].renderData.messageID, self.m1.renderData.messageID); + XCTAssertEqualObjects(foundMessages[1].renderData.messageID, self.m2.renderData.messageID); + + // we expect to register a fetch with sdk manager + OCMVerify([self.mockSDKModeManager registerOneMoreFetch]); +} + +// Fetch always in testing app instance mode +- (void)testAlwaysFetchForTestingAppInstanceMode { + OCMStub([self.mockBookkeeper lastFetchTime]).andReturn(0); + OCMStub([self.mockSDKModeManager currentMode]).andReturn(FIRIAMSDKModeTesting); + OCMStub([self.mockMessageFetcher + fetchMessagesWithImpressionList:[OCMArg any] + withCompletion:([OCMArg invokeBlockWithArgs:@[ self.m1, self.m2 ], + [NSNull null], [NSNull null], + [NSNull null], nil])]); + // 100 seconds is less than fetch wait time which is 1000 in this setup, + // but since we are in testing app instance mode, fetch would still happen + OCMStub([self.mockBookkeeper nextFetchWaitTime]).andReturn(1000); + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(100); + + [self.flow checkAndFetch]; + + // we expect m1 and m2 to be dumped into clientMessageCache + NSArray *foundMessages = [self.clientMessageCache allRegularMessages]; + XCTAssertEqual(2, foundMessages.count); + XCTAssertEqualObjects(foundMessages[0].renderData.messageID, self.m1.renderData.messageID); + XCTAssertEqualObjects(foundMessages[1].renderData.messageID, self.m2.renderData.messageID); + + // we expect to register a fetch with sdk manager + OCMVerify([self.mockSDKModeManager registerOneMoreFetch]); +} + +- (void)testTurnIntoTestigModeOnSeeingTestMessage { + OCMStub([self.mockBookkeeper lastFetchTime]).andReturn(0); + OCMStub([self.mockSDKModeManager currentMode]).andReturn(FIRIAMSDKModeNewlyInstalled); + + FIRIAMMessageDefinition *testMessage = + [[FIRIAMMessageDefinition alloc] initTestMessageWithRenderData:self.m2.renderData]; + + OCMStub([self.mockMessageFetcher + fetchMessagesWithImpressionList:[OCMArg any] + withCompletion:([OCMArg invokeBlockWithArgs:@[ self.m1, testMessage ], + [NSNull null], [NSNull null], + [NSNull null], nil])]); + self.fetchSetting.fetchMinIntervalInMinutes = 10; // at least 600 seconds between fetches + // 100 seconds is larger than FETCH_MIN_INTERVALS minutes + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(100); + + [self.flow checkAndFetch]; + + // Expecting turning sdk mode into a testing instance + OCMVerify([self.mockSDKModeManager becomeTestingInstance]); +} + +- (void)testNotTurningIntoTestingModeIfAlreadyInTestingMode { + OCMStub([self.mockBookkeeper lastFetchTime]).andReturn(0); + OCMStub([self.mockSDKModeManager currentMode]).andReturn(FIRIAMSDKModeTesting); + + FIRIAMMessageDefinition *testMessage = + [[FIRIAMMessageDefinition alloc] initTestMessageWithRenderData:self.m2.renderData]; + + OCMStub([self.mockMessageFetcher + fetchMessagesWithImpressionList:[OCMArg any] + withCompletion:([OCMArg invokeBlockWithArgs:@[ self.m1, testMessage ], + [NSNull null], [NSNull null], + [NSNull null], nil])]); + self.fetchSetting.fetchMinIntervalInMinutes = 10; // at least 600 seconds between fetches + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(1000); + OCMReject([self.mockSDKModeManager becomeTestingInstance]); + + [self.flow checkAndFetch]; +} +@end diff --git a/InAppMessaging/Example/Tests/FIRIAMFetchResponseParserTests.m b/InAppMessaging/Example/Tests/FIRIAMFetchResponseParserTests.m new file mode 100644 index 00000000000..0365d072d86 --- /dev/null +++ b/InAppMessaging/Example/Tests/FIRIAMFetchResponseParserTests.m @@ -0,0 +1,180 @@ +/* + * 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 +#import + +#import "FIRIAMDisplayTriggerDefinition.h" +#import "FIRIAMFetchResponseParser.h" +#import "FIRIAMMessageContentDataWithImageURL.h" +#import "FIRIAMMessageDefinition.h" +#import "FIRIAMTimeFetcher.h" +#import "UIColor+FIRIAMHexString.h" + +@interface FIRIAMFetchResponseParserTests : XCTestCase +@property(nonatomic, copy) NSString *jsonResposne; +@property(nonatomic) FIRIAMFetchResponseParser *parser; +@property(nonatomic) id mockTimeFetcher; +@end + +@implementation FIRIAMFetchResponseParserTests + +- (void)setUp { + [super setUp]; + self.mockTimeFetcher = OCMProtocolMock(@protocol(FIRIAMTimeFetcher)); + self.parser = [[FIRIAMFetchResponseParser alloc] initWithTimeFetcher:self.mockTimeFetcher]; +} +- (void)tearDown { + [super tearDown]; +} + +- (void)testRegularConversion { + NSString *testJsonDataFilePath = + [[NSBundle bundleForClass:[self class]] pathForResource:@"TestJsonDataFromFetch" + ofType:@"txt"]; + + NSTimeInterval currentMoment = 100000000; + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(currentMoment); + + self.jsonResposne = [[NSString alloc] initWithContentsOfFile:testJsonDataFilePath + encoding:NSUTF8StringEncoding + error:nil]; + + NSData *data = [self.jsonResposne dataUsingEncoding:NSUTF8StringEncoding]; + NSError *errorJson = nil; + NSDictionary *responseDict = [NSJSONSerialization JSONObjectWithData:data + options:kNilOptions + error:&errorJson]; + + NSInteger discardCount; + NSNumber *fetchWaitTime; + NSArray *results = + [self.parser parseAPIResponseDictionary:responseDict + discardedMsgCount:&discardCount + fetchWaitTimeInSeconds:&fetchWaitTime]; + + double nextFetchEpochTimeInResponse = + [responseDict[@"expirationEpochTimestampMillis"] doubleValue]; + + // fetch wait time should be (next fetch epoch time - curret moment) + XCTAssertEqualWithAccuracy([fetchWaitTime doubleValue], + nextFetchEpochTimeInResponse / 1000 - currentMoment, 0.1); + + XCTAssertEqual(4, [results count]); + XCTAssertEqual(0, discardCount); + + FIRIAMMessageDefinition *first = results[0]; + XCTAssertEqualObjects(@"13313766398414028800", first.renderData.messageID); + XCTAssertEqualObjects(@"first campaign", first.renderData.name); + XCTAssertEqualObjects(@"I heard you like In-App Messages", + first.renderData.contentData.titleText); + XCTAssertEqualObjects(@"This is message body", first.renderData.contentData.bodyText); + XCTAssertEqual(FIRIAMRenderAsModalView, first.renderData.renderingEffectSettings.viewMode); + XCTAssertEqualWithAccuracy(1523986039, first.startTime, 0.1); + XCTAssertEqualWithAccuracy(1526986039, first.endTime, 0.1); + XCTAssertNotNil(first.renderData.renderingEffectSettings.textColor); + XCTAssertEqualObjects(first.renderData.renderingEffectSettings.displayBGColor, + [UIColor firiam_colorWithHexString:@"#fffff8"]); + XCTAssertEqualObjects(first.renderData.renderingEffectSettings.btnBGColor, + [UIColor firiam_colorWithHexString:@"#000000"]); + XCTAssertEqualObjects(first.renderData.contentData.actionURL.absoluteString, + @"https://www.google.com"); + XCTAssertEqual(FIRIAMRenderTriggerOnAppForeground, first.renderTriggers[0].triggerType); + + FIRIAMMessageDefinition *second = results[1]; + XCTAssertEqualObjects(@"9350598726327992320", second.renderData.messageID); + XCTAssertEqualObjects(@"Inception1", second.renderData.name); + XCTAssertEqualObjects(@"Test 2", second.renderData.contentData.titleText); + XCTAssertNil(second.renderData.contentData.bodyText); + XCTAssertEqual(FIRIAMRenderAsModalView, second.renderData.renderingEffectSettings.viewMode); + XCTAssertEqual(2, second.renderTriggers.count); + + XCTAssertEqualObjects(second.renderData.renderingEffectSettings.displayBGColor, + [UIColor firiam_colorWithHexString:@"#ffffff"]); + + // Third message is a banner view message based on a analytics event trigger. + FIRIAMMessageDefinition *third = results[2]; + XCTAssertEqualObjects(@"14819094573862617088", third.renderData.messageID); + XCTAssertEqual(FIRIAMRenderAsBannerView, third.renderData.renderingEffectSettings.viewMode); + XCTAssertEqual(1, third.renderTriggers.count); + XCTAssertEqualObjects(@"jackpot", third.renderTriggers[0].firebaseEventName); +} + +- (void)testParsingTestMessage { + NSString *testJsonDataFilePath = [[NSBundle bundleForClass:[self class]] + pathForResource:@"TestJsonDataWithTestMessageFromFetch" + ofType:@"txt"]; + + self.jsonResposne = [[NSString alloc] initWithContentsOfFile:testJsonDataFilePath + encoding:NSUTF8StringEncoding + error:nil]; + + NSData *data = [self.jsonResposne dataUsingEncoding:NSUTF8StringEncoding]; + NSError *errorJson = nil; + NSDictionary *responseDict = [NSJSONSerialization JSONObjectWithData:data + options:kNilOptions + error:&errorJson]; + + NSInteger discardCount; + NSNumber *fetchWaitTime; + NSArray *results = + [self.parser parseAPIResponseDictionary:responseDict + discardedMsgCount:&discardCount + fetchWaitTimeInSeconds:&fetchWaitTime]; + + // In our fixture file used in this test, there is no fetch expiration time + XCTAssertNil(fetchWaitTime); + + XCTAssertEqual(2, [results count]); + XCTAssertEqual(0, discardCount); + + // First is a test message and the second one is not. + XCTAssertTrue(results[0].isTestMessage); + XCTAssertTrue(results[0].renderData.renderingEffectSettings.isTestMessage); + + XCTAssertFalse(results[1].isTestMessage); + XCTAssertFalse(results[1].renderData.renderingEffectSettings.isTestMessage); +} + +- (void)testParsingInvalidTestMessageNodes { + NSString *testJsonDataFilePath = [[NSBundle bundleForClass:[self class]] + pathForResource:@"JsonDataWithInvalidMessagesFromFetch" + ofType:@"txt"]; + + self.jsonResposne = [[NSString alloc] initWithContentsOfFile:testJsonDataFilePath + encoding:NSUTF8StringEncoding + error:nil]; + + NSData *data = [self.jsonResposne dataUsingEncoding:NSUTF8StringEncoding]; + NSError *errorJson = nil; + NSDictionary *responseDict = [NSJSONSerialization JSONObjectWithData:data + options:kNilOptions + error:&errorJson]; + + NSInteger discardCount; + NSNumber *fetchWaitTime; + NSArray *results = + [self.parser parseAPIResponseDictionary:responseDict + discardedMsgCount:&discardCount + fetchWaitTimeInSeconds:&fetchWaitTime]; + + XCTAssertEqual(0, [results count]); + + // First node missing title, second one missig triggering conditions and the third one + // contains invalid type node. + XCTAssertEqual(3, discardCount); +} + +@end diff --git a/InAppMessaging/Example/Tests/FIRIAMMessageClientCacheTests.m b/InAppMessaging/Example/Tests/FIRIAMMessageClientCacheTests.m new file mode 100644 index 00000000000..c0b95c539ac --- /dev/null +++ b/InAppMessaging/Example/Tests/FIRIAMMessageClientCacheTests.m @@ -0,0 +1,378 @@ +/* + * 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 +#import + +#import "FIRIAMDisplayCheckOnAnalyticEventsFlow.h" +#import "FIRIAMDisplayTriggerDefinition.h" +#import "FIRIAMMessageClientCache.h" +#import "FIRIAMMessageContentDataWithImageURL.h" +#import "FIRIAMMessageDefinition.h" +#import "FIRIAMTimeFetcher.h" + +@interface FIRIAMMessageClientCacheTests : XCTestCase +@property id mockBookkeeper; +@property(nonatomic) FIRIAMMessageClientCache *clientCache; +@end + +@interface FIRIAMMessageClientCache () +// for the purpose of unit testing validations +@property(nonatomic) NSMutableSet *firebaseAnalyticEventsToWatch; +@end + +@implementation FIRIAMMessageClientCacheTests { + // some predefined message definitions that are handy for certain test cases + FIRIAMMessageDefinition *m1, *m2, *m3, *m4, *m5; +} + +- (void)setUp { + [super setUp]; + self.mockBookkeeper = OCMProtocolMock(@protocol(FIRIAMBookKeeper)); + + FIRIAMFetchResponseParser *parser = + [[FIRIAMFetchResponseParser alloc] initWithTimeFetcher:[[FIRIAMTimerWithNSDate alloc] init]]; + self.clientCache = [[FIRIAMMessageClientCache alloc] initWithBookkeeper:self.mockBookkeeper + usingResponseParser:parser]; + + // startTime, endTime here ensures messages with them are active + NSTimeInterval activeStartTime = 0; + NSTimeInterval activeEndTime = [[NSDate date] timeIntervalSince1970] + 10000; + // m2 & m 4 will be of contextual trigger + FIRIAMDisplayTriggerDefinition *contextualTriggerDefinition = + [[FIRIAMDisplayTriggerDefinition alloc] initWithFirebaseAnalyticEvent:@"test_event"]; + + FIRIAMDisplayTriggerDefinition *contextualTriggerDefinition2 = + [[FIRIAMDisplayTriggerDefinition alloc] initWithFirebaseAnalyticEvent:@"second_event"]; + + // m1 and m3 will be of app open trigger + FIRIAMDisplayTriggerDefinition *appOpentriggerDefinition = + [[FIRIAMDisplayTriggerDefinition alloc] initForAppForegroundTrigger]; + + FIRIAMMessageContentDataWithImageURL *msgContentData = + [[FIRIAMMessageContentDataWithImageURL alloc] + initWithMessageTitle:@"title" + messageBody:@"message body" + actionButtonText:nil + actionURL:[NSURL URLWithString:@"http://google.com"] + imageURL:[NSURL URLWithString:@"https://unsplash.it/300/300"] + usingURLSession:nil]; + + FIRIAMRenderingEffectSetting *renderSetting = + [FIRIAMRenderingEffectSetting getDefaultRenderingEffectSetting]; + renderSetting.viewMode = FIRIAMRenderAsBannerView; + + FIRIAMMessageRenderData *renderData1 = + [[FIRIAMMessageRenderData alloc] initWithMessageID:@"m1" + messageName:@"name" + contentData:msgContentData + renderingEffect:renderSetting]; + + m1 = [[FIRIAMMessageDefinition alloc] initWithRenderData:renderData1 + startTime:activeStartTime + endTime:activeEndTime + triggerDefinition:@[ appOpentriggerDefinition ]]; + + FIRIAMMessageRenderData *renderData2 = + [[FIRIAMMessageRenderData alloc] initWithMessageID:@"m2" + messageName:@"name" + contentData:msgContentData + renderingEffect:renderSetting]; + + m2 = [[FIRIAMMessageDefinition alloc] initWithRenderData:renderData2 + startTime:activeStartTime + endTime:activeEndTime + triggerDefinition:@[ contextualTriggerDefinition ]]; + + FIRIAMMessageRenderData *renderData3 = + [[FIRIAMMessageRenderData alloc] initWithMessageID:@"m3" + messageName:@"name" + contentData:msgContentData + renderingEffect:renderSetting]; + + m3 = [[FIRIAMMessageDefinition alloc] initWithRenderData:renderData3 + startTime:activeStartTime + endTime:activeEndTime + triggerDefinition:@[ appOpentriggerDefinition ]]; + + FIRIAMMessageRenderData *renderData4 = + [[FIRIAMMessageRenderData alloc] initWithMessageID:@"m4" + messageName:@"name" + contentData:msgContentData + renderingEffect:renderSetting]; + + FIRIAMMessageRenderData *renderData5 = + [[FIRIAMMessageRenderData alloc] initWithMessageID:@"m5" + messageName:@"name" + contentData:msgContentData + renderingEffect:renderSetting]; + + m4 = [[FIRIAMMessageDefinition alloc] + initWithRenderData:renderData4 + startTime:activeStartTime + endTime:activeEndTime + triggerDefinition:@[ contextualTriggerDefinition, contextualTriggerDefinition2 ]]; + + // m5 is of mixture of both app-foreground and contextual triggers + m5 = [[FIRIAMMessageDefinition alloc] + initWithRenderData:renderData5 + startTime:activeStartTime + endTime:activeEndTime + triggerDefinition:@[ contextualTriggerDefinition, appOpentriggerDefinition ]]; +} + +- (void)tearDown { + // Put teardown code here. This method is called after the invocation of each test method in the + // class. + [super tearDown]; +} + +- (void)testResetMessages { + // test setting a mixture of display-on-app open messages and Firebase Analytics based messages + // to see if the cache will keep them correctly + OCMStub([self.mockBookkeeper getImpressions]).andReturn(@[]); + [self.clientCache setMessageData:@[ m1, m2, m3, m4 ]]; + + NSArray *messages = [self.clientCache allRegularMessages]; + XCTAssertEqual(4, [messages count]); + + // m4 have two contextual events defined as triggers + XCTAssertEqual(2, [self.clientCache.firebaseAnalyticEventsToWatch count]); + XCTAssert([self.clientCache.firebaseAnalyticEventsToWatch containsObject:@"test_event"]); + XCTAssert([self.clientCache.firebaseAnalyticEventsToWatch containsObject:@"second_event"]); +} + +- (void)testResetMessagesWithImpressionsData { + // test setting a mixture of display-on-app open messages and Firebase Analytics based messages + // to see if the cache will keep them correctly + + NSArray *impressionList = @[ @"m1", @"m2" ]; + OCMStub([self.mockBookkeeper getMessageIDsFromImpressions]).andReturn(impressionList); + [self.clientCache setMessageData:@[ m1, m2, m3, m4 ]]; + + // m1 and m2 should have been filtered out + NSArray *messages = [self.clientCache allRegularMessages]; + XCTAssertEqual(2, messages.count); + + // m4 have two contextual events defined as triggers + XCTAssertEqual(2, self.clientCache.firebaseAnalyticEventsToWatch.count); + XCTAssert([self.clientCache.firebaseAnalyticEventsToWatch containsObject:@"test_event"]); + XCTAssert([self.clientCache.firebaseAnalyticEventsToWatch containsObject:@"second_event"]); +} + +- (void)testNextOnAppOpenDisplayMsg_ok { + OCMStub([self.mockBookkeeper getImpressions]).andReturn(@[]); + // m1 and m3 are messages rendered on app open + [self.clientCache setMessageData:@[ m1, m2, m3, m4 ]]; + + FIRIAMMessageDefinition *nextMsgOnAppOpen = [self.clientCache nextOnAppOpenDisplayMsg]; + XCTAssertEqual(@"m1", nextMsgOnAppOpen.renderData.messageID); + // remove m1 + [self.clientCache removeMessageWithId:@"m1"]; + + // read m2 and remove it + nextMsgOnAppOpen = [self.clientCache nextOnAppOpenDisplayMsg]; + XCTAssertEqual(@"m3", nextMsgOnAppOpen.renderData.messageID); + [self.clientCache removeMessageWithId:@"m3"]; + + // no more message for display on app open + nextMsgOnAppOpen = [self.clientCache nextOnAppOpenDisplayMsg]; + XCTAssertNil(nextMsgOnAppOpen); +} + +- (void)testNextOnFirebaseAnalyticsEventDisplayMsg_ok { + OCMStub([self.mockBookkeeper getImpressions]).andReturn(@[]); + // m2 and m4 are messages rendered on 'app open'test_event' Firebase Analytics event + [self.clientCache setMessageData:@[ m1, m2, m3, m4 ]]; + + FIRIAMMessageDefinition *nextMsgOnFIREvent = + [self.clientCache nextOnFirebaseAnalyticEventDisplayMsg:@"test_event"]; + XCTAssertEqual(@"m2", nextMsgOnFIREvent.renderData.messageID); + // remove m2 + [self.clientCache removeMessageWithId:@"m2"]; + // verify that the watch set is empty after draining all the messages + XCTAssertTrue([self.clientCache.firebaseAnalyticEventsToWatch containsObject:@"test_event"]); + + // read m4 and remove it + nextMsgOnFIREvent = [self.clientCache nextOnFirebaseAnalyticEventDisplayMsg:@"test_event"]; + XCTAssertEqual(@"m4", nextMsgOnFIREvent.renderData.messageID); + // remove m4 + [self.clientCache removeMessageWithId:@"m4"]; + + // no more message for display on Firebase Analytics event + nextMsgOnFIREvent = [self.clientCache nextOnFirebaseAnalyticEventDisplayMsg:@"test_event"]; + XCTAssertNil(nextMsgOnFIREvent); + + // verify that the watch set is empty after draining all the messages + XCTAssertEqual(0, self.clientCache.firebaseAnalyticEventsToWatch.count); +} + +- (void)testNextOnFirebaseAnalyticsEventDisplayMsgEventNameMustMatch_ok { + OCMStub([self.mockBookkeeper getImpressions]).andReturn(@[]); + // m2 and m4 are messages rendered on 'app open'test_event' Firebase Analytics event + [self.clientCache setMessageData:@[ m1, m2, m3, m4 ]]; + + FIRIAMMessageDefinition *nextMsgOnFIREvent = + [self.clientCache nextOnFirebaseAnalyticEventDisplayMsg:@"different_event"]; + XCTAssertNil(nextMsgOnFIREvent); +} + +- (void)testNextOnFirebaseAnalyticsEventDisplayMsgEventNameCanMatchAny_ok { + OCMStub([self.mockBookkeeper getImpressions]).andReturn(@[]); + // m4 are messages of multiple contextual triggers, one of which is for event + // 'second_event' + [self.clientCache setMessageData:@[ m1, m2, m3, m4 ]]; + + FIRIAMMessageDefinition *nextMsgOnFIREvent = + [self.clientCache nextOnFirebaseAnalyticEventDisplayMsg:@"second_event"]; + XCTAssertNotNil(nextMsgOnFIREvent); +} + +- (void)testMessageCanHaveMixedTypeOfTriggers_ok { + OCMStub([self.mockBookkeeper getImpressions]).andReturn(@[]); + [self.clientCache setMessageData:@[ m5 ]]; + + FIRIAMMessageDefinition *nextMsgOnFIREvent = + [self.clientCache nextOnFirebaseAnalyticEventDisplayMsg:@"test_event"]; + XCTAssertNotNil(nextMsgOnFIREvent); + + // in the meanwhile, retrieving an app-foreground message should be successful + FIRIAMMessageDefinition *nextMsgOnAppForeground = [self.clientCache nextOnAppOpenDisplayMsg]; + XCTAssertNotNil(nextMsgOnAppForeground); +} + +- (void)testNextOnFirebaseAnalyticsEventDisplayMsg_handleStartEndTimeCorrectly { + OCMStub([self.mockBookkeeper getImpressions]).andReturn(@[]); + + FIRIAMDisplayTriggerDefinition *appOpentriggerDefinition = + [[FIRIAMDisplayTriggerDefinition alloc] initForAppForegroundTrigger]; + + FIRIAMMessageContentDataWithImageURL *msgContentData = + [[FIRIAMMessageContentDataWithImageURL alloc] + initWithMessageTitle:@"title" + messageBody:@"message body" + actionButtonText:nil + actionURL:[NSURL URLWithString:@"http://google.com"] + imageURL:[NSURL URLWithString:@"https://unsplash.it/300/300"] + usingURLSession:nil]; + + FIRIAMRenderingEffectSetting *renderSetting = + [FIRIAMRenderingEffectSetting getDefaultRenderingEffectSetting]; + renderSetting.viewMode = FIRIAMRenderAsBannerView; + + FIRIAMMessageRenderData *renderData1 = + [[FIRIAMMessageRenderData alloc] initWithMessageID:@"m1" + messageName:@"name" + contentData:msgContentData + renderingEffect:renderSetting]; + + // m1 has not started yet + FIRIAMMessageDefinition *unstartedMessage = [[FIRIAMMessageDefinition alloc] + initWithRenderData:renderData1 + startTime:[[NSDate date] timeIntervalSince1970] + 10000 + endTime:[[NSDate date] timeIntervalSince1970] + 20000 + triggerDefinition:@[ appOpentriggerDefinition ]]; + + FIRIAMMessageRenderData *renderData2 = + [[FIRIAMMessageRenderData alloc] initWithMessageID:@"m2" + messageName:@"name" + contentData:msgContentData + renderingEffect:renderSetting]; + + // m2 has ended + FIRIAMMessageDefinition *endedMessage = [[FIRIAMMessageDefinition alloc] + initWithRenderData:renderData2 + startTime:[[NSDate date] timeIntervalSince1970] - 20000 + endTime:[[NSDate date] timeIntervalSince1970] - 10000 + triggerDefinition:@[ appOpentriggerDefinition ]]; + + // m3, m4 are campaigns with good start/end time + [self.clientCache setMessageData:@[ unstartedMessage, endedMessage, m3, m4 ]]; + + FIRIAMMessageDefinition *nextMsgOnAppOpen = [self.clientCache nextOnAppOpenDisplayMsg]; + FIRIAMMessageDefinition *nextMsgOnFIREvent = + [self.clientCache nextOnFirebaseAnalyticEventDisplayMsg:@"test_event"]; + XCTAssertEqual(nextMsgOnAppOpen.renderData.messageID, @"m3"); + XCTAssertEqual(nextMsgOnFIREvent.renderData.messageID, @"m4"); + + // no more on app open display message + [self.clientCache removeMessageWithId:@"m3"]; + nextMsgOnAppOpen = [self.clientCache nextOnAppOpenDisplayMsg]; + XCTAssertNil(nextMsgOnAppOpen); +} + +- (void)testCallingStartAnalyticsEventListenFlow_ok { + OCMStub([self.mockBookkeeper getImpressions]).andReturn(@[]); + + FIRIAMDisplayCheckOnAnalyticEventsFlow *mockAnalyticsEventFlow = + OCMClassMock(FIRIAMDisplayCheckOnAnalyticEventsFlow.class); + self.clientCache.analycisEventDislayCheckFlow = mockAnalyticsEventFlow; + + // m2 and m4 are messages rendered on 'test_event' Firebase Analytics event + // so we espect the analytics event listening flow to be started + OCMExpect([mockAnalyticsEventFlow start]); + [self.clientCache setMessageData:@[ m1, m2, m3, m4 ]]; + OCMVerifyAll((id)mockAnalyticsEventFlow); +} + +- (void)testCallingStopAnalyticsEventListenFlow_ok { + OCMStub([self.mockBookkeeper getImpressions]).andReturn(@[]); + + FIRIAMDisplayCheckOnAnalyticEventsFlow *mockAnalyticsEventFlow = + OCMClassMock(FIRIAMDisplayCheckOnAnalyticEventsFlow.class); + self.clientCache.analycisEventDislayCheckFlow = mockAnalyticsEventFlow; + + // m1 and m3 are messages rendered on app foreground triggers + OCMExpect([mockAnalyticsEventFlow stop]); + [self.clientCache setMessageData:@[ m1, m3 ]]; + OCMVerifyAll((id)mockAnalyticsEventFlow); +} + +- (void)testCallingStartAndThenStopAnalyticsEventListenFlow_ok { + OCMStub([self.mockBookkeeper getImpressions]).andReturn(@[]); + + FIRIAMDisplayCheckOnAnalyticEventsFlow *mockAnalyticsEventFlow = + OCMClassMock(FIRIAMDisplayCheckOnAnalyticEventsFlow.class); + self.clientCache.analycisEventDislayCheckFlow = mockAnalyticsEventFlow; + + // start is triggered on the setMessageData: call + OCMExpect([mockAnalyticsEventFlow start]); + // stop is triggered on removeMessageWithId: call since m2 is the only message + // using contextual triggers + OCMExpect([mockAnalyticsEventFlow stop]); + + [self.clientCache setMessageData:@[ m1, m2, m3 ]]; + [self.clientCache removeMessageWithId:m2.renderData.messageID]; + OCMVerifyAll((id)mockAnalyticsEventFlow); +} + +- (void)testFetchTestMessageFirstOnNextOnAppOpenDisplayMsg_ok { + OCMStub([self.mockBookkeeper getImpressions]).andReturn(@[]); + + FIRIAMMessageDefinition *testMessage = + [[FIRIAMMessageDefinition alloc] initTestMessageWithRenderData:m2.renderData]; + + // m1 and m3 are messages rendered on app open + [self.clientCache setMessageData:@[ m1, m2, testMessage, m3, m4 ]]; + + // we are fetching test message back + FIRIAMMessageDefinition *nextMsgOnAppOpen = [self.clientCache nextOnAppOpenDisplayMsg]; + XCTAssertEqual(testMessage.renderData.messageID, nextMsgOnAppOpen.renderData.messageID); + + // we still have 4 regular messages after the first fetch + XCTAssertEqual(4, self.clientCache.allRegularMessages.count); +} +@end diff --git a/InAppMessaging/Example/Tests/FIRIAMMessageContentDataWithImageURLTests.m b/InAppMessaging/Example/Tests/FIRIAMMessageContentDataWithImageURLTests.m new file mode 100644 index 00000000000..0a71613dd70 --- /dev/null +++ b/InAppMessaging/Example/Tests/FIRIAMMessageContentDataWithImageURLTests.m @@ -0,0 +1,241 @@ +/* + * 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 +#import +#import "FIRIAMMessageContentDataWithImageURL.h" + +static NSString *defaultTitle = @"Message Title"; +static NSString *defaultBody = @"Message Body"; +static NSString *defaultActionButtonText = @"Take action"; +static NSString *defaultActionURL = @"https://foo.com/bar"; +static NSString *defaultImageURL = @"http://firebase.com/iam/test.png"; + +@interface FIRIAMMessageContentDataWithImageURLTests : XCTestCase +@property NSURLSession *mockedNSURLSession; + +@property FIRIAMMessageContentDataWithImageURL *defaultContentDataWithImageURL; +@end + +@implementation FIRIAMMessageContentDataWithImageURLTests + +- (void)setUp { + [super setUp]; + + _mockedNSURLSession = OCMClassMock([NSURLSession class]); + _defaultContentDataWithImageURL = [[FIRIAMMessageContentDataWithImageURL alloc] + initWithMessageTitle:defaultTitle + messageBody:defaultBody + actionButtonText:defaultActionButtonText + actionURL:[NSURL URLWithString:defaultActionURL] + imageURL:[NSURL URLWithString:defaultImageURL] + usingURLSession:_mockedNSURLSession]; +} + +- (void)tearDown { + [super tearDown]; +} + +- (void)testReadingTitleAndBodyBackCorrectly { + XCTAssertEqualObjects(defaultTitle, self.defaultContentDataWithImageURL.titleText); + XCTAssertEqualObjects(defaultBody, self.defaultContentDataWithImageURL.bodyText); +} + +- (void)testReadingActionButtonTextCorrectly { + XCTAssertEqualObjects(defaultActionButtonText, + self.defaultContentDataWithImageURL.actionButtonText); +} + +- (void)testURLRequestUsingCorrectImageURL { + __block NSURLRequest *capturedNSURLRequest; + OCMStub([self.mockedNSURLSession + dataTaskWithRequest:[OCMArg checkWithBlock:^BOOL(NSURLRequest *request) { + capturedNSURLRequest = request; + return YES; + }] + completionHandler:[OCMArg any] // second parameter is the callback which we don't care in + // this unit testing + ]); + + [_defaultContentDataWithImageURL + loadImageDataWithBlock:^(NSData *_Nullable imageData, NSError *_Nullable error){ + }]; + + // verify that the dataTaskWithRequest:completionHandler: is triggered for NSURLSession object + OCMVerify([self.mockedNSURLSession dataTaskWithRequest:[OCMArg any] + completionHandler:[OCMArg any]]); + + XCTAssertEqualObjects([capturedNSURLRequest URL].absoluteString, defaultImageURL); +} + +- (void)testReportErrorOnNonSuccessHTTPStatusCode { + // NSURLSessionDataTask * mockedDataTask = OCMClassMock([NSURLSessionDataTask class]); + __block void (^capturedCompletionHandler)(NSData *data, NSURLResponse *response, NSError *error); + + OCMStub([self.mockedNSURLSession + dataTaskWithRequest:[OCMArg any] + completionHandler:[OCMArg checkWithBlock:^BOOL(id completionHandler) { + capturedCompletionHandler = completionHandler; + return YES; + }] // second parameter is the callback which we don't care in this unit testing + ]); + + XCTestExpectation *expectation = + [self expectationWithDescription:@"image load callback triggered."]; + [_defaultContentDataWithImageURL + loadImageDataWithBlock:^(NSData *_Nullable imageData, NSError *_Nullable error) { + XCTAssertNil(imageData); + XCTAssertNotNil(error); // we should report error due to the unsuccessful http status code + [expectation fulfill]; + }]; + + // verify that the dataTaskWithRequest:completionHandler: is triggered for NSURLSession object + OCMVerify([self.mockedNSURLSession dataTaskWithRequest:[OCMArg any] + completionHandler:[OCMArg any]]); + + // by this time we should have capturedCompletionHandler being the callback block for the + // NSURLSessionDataTask, now supply it with invalid http status code to see how the block from + // loadImageDataWithBlock: would react to it. + + NSURL *url = [[NSURL alloc] initWithString:defaultImageURL]; + + NSHTTPURLResponse *unsuccessfulHTTPResponse = [[NSHTTPURLResponse alloc] initWithURL:url + statusCode:404 + HTTPVersion:nil + headerFields:nil]; + capturedCompletionHandler(nil, unsuccessfulHTTPResponse, nil); + + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +- (void)testReportErrorOnGeneralNSErrorFromNSURLSession { + NSError *customError = [[NSError alloc] initWithDomain:@"Error Domain" code:100 userInfo:nil]; + __block void (^capturedCompletionHandler)(NSData *data, NSURLResponse *response, NSError *error); + + OCMStub([self.mockedNSURLSession + dataTaskWithRequest:[OCMArg any] + completionHandler:[OCMArg checkWithBlock:^BOOL(id completionHandler) { + capturedCompletionHandler = completionHandler; + return YES; + }] // second parameter is the callback which we don't care in this unit testing + ]); + + XCTestExpectation *expectation = + [self expectationWithDescription:@"image load callback triggered."]; + [_defaultContentDataWithImageURL + loadImageDataWithBlock:^(NSData *_Nullable imageData, NSError *_Nullable error) { + XCTAssertNil(imageData); + XCTAssertNotNil(error); // we should report error due to the unsuccessful http status code + XCTAssertEqualObjects(error, customError); + [expectation fulfill]; + }]; + + // verify that the dataTaskWithRequest:completionHandler: is triggered for NSURLSession object + OCMVerify([self.mockedNSURLSession dataTaskWithRequest:[OCMArg any] + completionHandler:[OCMArg any]]); + + // by this time we should have capturedCompletionHandler being the callback block for the + // NSURLSessionDataTask, now feed it with an NSError see how the block from + // loadImageDataWithBlock: would react to it. + capturedCompletionHandler(nil, nil, customError); + + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +- (void)testReportErrorOnNonImageContentTypeResponse { + __block void (^capturedCompletionHandler)(NSData *data, NSURLResponse *response, NSError *error); + + OCMStub([self.mockedNSURLSession + dataTaskWithRequest:[OCMArg any] + completionHandler:[OCMArg checkWithBlock:^BOOL(id completionHandler) { + capturedCompletionHandler = completionHandler; + return YES; + }] // second parameter is the callback which we don't care in this unit testing + ]); + + XCTestExpectation *expectation = + [self expectationWithDescription:@"image load callback triggered."]; + [_defaultContentDataWithImageURL + loadImageDataWithBlock:^(NSData *_Nullable imageData, NSError *_Nullable error) { + XCTAssertNil(imageData); + XCTAssertNotNil(error); // we should report error due to the http response + // content type being invalid + [expectation fulfill]; + }]; + + // verify that the dataTaskWithRequest:completionHandler: is triggered for NSURLSession object + OCMVerify([self.mockedNSURLSession dataTaskWithRequest:[OCMArg any] + completionHandler:[OCMArg any]]); + + // by this time we should have capturedCompletionHandler being the callback block for the + // NSURLSessionDataTask, now feed it with a non-image http response to see how the block from + // loadImageDataWithBlock: would react to it. + + NSURL *url = [[NSURL alloc] initWithString:defaultImageURL]; + NSHTTPURLResponse *nonImageContentTypeHTTPResponse = + [[NSHTTPURLResponse alloc] initWithURL:url + statusCode:200 + HTTPVersion:nil + headerFields:@{@"Content-Type" : @"non-image/jpeg"}]; + capturedCompletionHandler(nil, nonImageContentTypeHTTPResponse, nil); + + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +- (void)testGettingImageDataSuccessfully { + NSString *imageDataString = @"test image data"; + NSData *imageData = [imageDataString dataUsingEncoding:NSUTF8StringEncoding]; + + __block void (^capturedCompletionHandler)(NSData *data, NSURLResponse *response, NSError *error); + + OCMStub([self.mockedNSURLSession + dataTaskWithRequest:[OCMArg any] + completionHandler:[OCMArg checkWithBlock:^BOOL(id completionHandler) { + capturedCompletionHandler = completionHandler; + return YES; + }] // second parameter is the callback which we don't care in this unit testing + ]); + + XCTestExpectation *expectation = + [self expectationWithDescription:@"image load callback triggered."]; + [_defaultContentDataWithImageURL + loadImageDataWithBlock:^(NSData *_Nullable imageData, NSError *_Nullable error) { + XCTAssertNil(error); // no error is reported + NSString *fetchedImageDataString = [[NSString alloc] initWithData:imageData + encoding:NSUTF8StringEncoding]; + + XCTAssertEqualObjects(imageDataString, fetchedImageDataString); + + [expectation fulfill]; + }]; + + // verify that the dataTaskWithRequest:completionHandler: is triggered for NSURLSession object + OCMVerify([self.mockedNSURLSession dataTaskWithRequest:[OCMArg any] + completionHandler:[OCMArg any]]); + + NSURL *url = [[NSURL alloc] initWithString:defaultImageURL]; + NSHTTPURLResponse *successfulHTTPResponse = + [[NSHTTPURLResponse alloc] initWithURL:url + statusCode:200 + HTTPVersion:nil + headerFields:@{@"Content-Type" : @"image/jpeg"}]; + // by this time we should have capturedCompletionHandler being the callback block for the + // NSURLSessionDataTask, now feed it with image data to see how the block from + // loadImageDataWithBlock: would react to it. + capturedCompletionHandler(imageData, successfulHTTPResponse, nil); + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} +@end diff --git a/InAppMessaging/Example/Tests/FIRIAMMsgFetcherUsingRestfulTests.m b/InAppMessaging/Example/Tests/FIRIAMMsgFetcherUsingRestfulTests.m new file mode 100644 index 00000000000..efdc327fb06 --- /dev/null +++ b/InAppMessaging/Example/Tests/FIRIAMMsgFetcherUsingRestfulTests.m @@ -0,0 +1,233 @@ +/* + * 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 +#import +#import "FIRIAMFetchFlow.h" +#import "FIRIAMMessageDefinition.h" +#import "FIRIAMMsgFetcherUsingRestful.h" + +static NSString *serverHost = @"myhost"; +static NSString *projectNumber = @"My-project-number"; +static NSString *appId = @"My-app-id"; +static NSString *apiKey = @"Api-key"; + +@interface FIRIAMMsgFetcherUsingRestfulTests : XCTestCase +@property NSURLSession *mockedNSURLSession; +@property FIRIAMClientInfoFetcher *mockclientInfoFetcher; +@property FIRIAMMsgFetcherUsingRestful *fetcher; +@end + +@implementation FIRIAMMsgFetcherUsingRestfulTests + +- (void)setUp { + [super setUp]; + // Put setup code here. This method is called before the invocation of each test method in the + // class. + self.mockedNSURLSession = OCMClassMock([NSURLSession class]); + self.mockclientInfoFetcher = OCMClassMock([FIRIAMClientInfoFetcher class]); + + FIRIAMFetchResponseParser *parser = + [[FIRIAMFetchResponseParser alloc] initWithTimeFetcher:[[FIRIAMTimerWithNSDate alloc] init]]; + + self.fetcher = + [[FIRIAMMsgFetcherUsingRestful alloc] initWithHost:serverHost + HTTPProtocol:@"https" + project:projectNumber + firebaseApp:appId + APIKey:apiKey + fetchStorage:[[FIRIAMServerMsgFetchStorage alloc] init] + instanceIDFetcher:_mockclientInfoFetcher + usingURLSession:_mockedNSURLSession + responseParser:parser]; +} + +- (void)tearDown { + // Put teardown code here. This method is called after the invocation of each test method in the + // class. + [super tearDown]; +} + +- (void)testRequestConstructionWithoutImpressionData { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + + __block NSURLRequest *capturedNSURLRequest; + OCMStub([self.mockedNSURLSession + dataTaskWithRequest:[OCMArg checkWithBlock:^BOOL(NSURLRequest *request) { + capturedNSURLRequest = request; + return YES; + }] + completionHandler:[OCMArg any] // second parameter is the callback which we don't care in + // this unit testing + ]); + + NSString *iidValue = @"my iid"; + NSString *iidToken = @"my iid token"; + OCMStub([self.mockclientInfoFetcher + fetchFirebaseIIDDataWithProjectNumber:[OCMArg any] + withCompletion:([OCMArg invokeBlockWithArgs:iidValue, iidToken, + [NSNull null], nil])]); + + NSString *osVersion = @"OS Version"; + OCMStub([self.mockclientInfoFetcher getOSVersion]).andReturn(osVersion); + NSString *appVersion = @"App Version"; + OCMStub([self.mockclientInfoFetcher getAppVersion]).andReturn(appVersion); + NSString *deviceLanguage = @"Language"; + OCMStub([self.mockclientInfoFetcher getDeviceLanguageCode]).andReturn(deviceLanguage); + NSString *timezone = @"time zone"; + OCMStub([self.mockclientInfoFetcher getTimezone]).andReturn(timezone); + + [self.fetcher + fetchMessagesWithImpressionList:@[] + withCompletion:^(NSArray *_Nullable messages, + NSNumber *nextFetchWaitTime, NSInteger discardCount, + NSError *_Nullable error){ + // blank on purpose: it won't get triggered + }]; + + // verify that the dataTaskWithRequest:completionHandler: is triggered for NSURLSession object + OCMVerify([self.mockedNSURLSession dataTaskWithRequest:[OCMArg any] + completionHandler:[OCMArg any]]); + + XCTAssertEqualObjects(@"POST", capturedNSURLRequest.HTTPMethod); + + NSDictionary *requestHeaders = capturedNSURLRequest.allHTTPHeaderFields; + + // verifying some request header fields + XCTAssertEqualObjects([NSBundle mainBundle].bundleIdentifier, + requestHeaders[@"X-Ios-Bundle-Identifier"]); + + XCTAssertEqualObjects(@"application/json", requestHeaders[@"Content-Type"]); + XCTAssertEqualObjects(@"application/json", requestHeaders[@"Accept"]); + + // verify that the request contains the desired api key + NSString *s = [NSString stringWithFormat:@"key=%@", apiKey]; + XCTAssertTrue([capturedNSURLRequest.URL.absoluteString containsString:s]); + XCTAssertTrue([capturedNSURLRequest.URL.absoluteString containsString:projectNumber]); + + // verify that we the request body contains desired iid data + NSError *errorJson = nil; + NSDictionary *requestBodyDict = + [NSJSONSerialization JSONObjectWithData:capturedNSURLRequest.HTTPBody + options:kNilOptions + error:&errorJson]; + XCTAssertEqualObjects(appId, requestBodyDict[@"requesting_client_app"][@"gmp_app_id"]); + XCTAssertEqualObjects(iidValue, requestBodyDict[@"requesting_client_app"][@"app_instance_id"]); + XCTAssertEqualObjects(iidToken, + requestBodyDict[@"requesting_client_app"][@"app_instance_id_token"]); + + XCTAssertEqualObjects(osVersion, requestBodyDict[@"client_signals"][@"platform_version"]); + XCTAssertEqualObjects(appVersion, requestBodyDict[@"client_signals"][@"app_version"]); + XCTAssertEqualObjects(deviceLanguage, requestBodyDict[@"client_signals"][@"language_code"]); + XCTAssertEqualObjects(timezone, requestBodyDict[@"client_signals"][@"time_zone"]); +} + +- (void)testRequestConstructionWithImpressionData { + __block NSURLRequest *capturedNSURLRequest; + OCMStub([self.mockedNSURLSession + dataTaskWithRequest:[OCMArg checkWithBlock:^BOOL(NSURLRequest *request) { + capturedNSURLRequest = request; + return YES; + }] + completionHandler:[OCMArg any] // second parameter is the callback which we don't care in + // this unit testing + ]); + + NSString *iidValue = @"my iid"; + NSString *iidToken = @"my iid token"; + OCMStub([self.mockclientInfoFetcher + fetchFirebaseIIDDataWithProjectNumber:[OCMArg any] + withCompletion:([OCMArg invokeBlockWithArgs:iidValue, iidToken, + [NSNull null], nil])]); + + // this is to test the case that only partial client signal fields are available + NSString *osVersion = @"OS Version"; + OCMStub([self.mockclientInfoFetcher getOSVersion]).andReturn(osVersion); + NSString *appVersion = @"App Version"; + OCMStub([self.mockclientInfoFetcher getAppVersion]).andReturn(appVersion); + + long impression1Timestamp = 12345; + FIRIAMImpressionRecord *impression1 = + [[FIRIAMImpressionRecord alloc] initWithMessageID:@"impression 1" + impressionTimeInSeconds:impression1Timestamp]; + long impression2Timestamp = 45678; + FIRIAMImpressionRecord *impression2 = + [[FIRIAMImpressionRecord alloc] initWithMessageID:@"impression 2" + impressionTimeInSeconds:impression2Timestamp]; + + [self.fetcher + fetchMessagesWithImpressionList:@[ impression1, impression2 ] + withCompletion:^(NSArray *_Nullable messages, + NSNumber *_Nullable nextFetchWaitTime, + NSInteger discardCount, NSError *_Nullable error){ + // blank on purpose: it won't get triggered + }]; + + // verify that the captured nsurl request has expected body + NSError *errorJson = nil; + NSDictionary *requestBodyDict = + [NSJSONSerialization JSONObjectWithData:capturedNSURLRequest.HTTPBody + options:kNilOptions + error:&errorJson]; + + XCTAssertEqualObjects(impression1.messageID, + requestBodyDict[@"already_seen_campaigns"][0][@"campaign_id"]); + XCTAssertEqualWithAccuracy( + impression1Timestamp * 1000, + ((NSNumber *)requestBodyDict[@"already_seen_campaigns"][0][@"impression_timestamp_millis"]) + .longValue, + 0.1); + XCTAssertEqualObjects(impression2.messageID, + requestBodyDict[@"already_seen_campaigns"][1][@"campaign_id"]); + XCTAssertEqualWithAccuracy( + impression2Timestamp * 1000, + ((NSNumber *)requestBodyDict[@"already_seen_campaigns"][1][@"impression_timestamp_millis"]) + .longValue, + 0.1); + + XCTAssertEqualObjects(osVersion, requestBodyDict[@"client_signals"][@"platform_version"]); + XCTAssertEqualObjects(appVersion, requestBodyDict[@"client_signals"][@"app_version"]); + // not expexting language siganl since it's not mocked on mockclientInfoFetcher + XCTAssertNil(requestBodyDict[@"client_signals"][@"language_code"]); +} + +- (void)testBailoutOnIIDError { + // in this test, the attempt to fetch iid data failed and as a result, we expect the whole + // fetch operation attempt to fail with that error + NSError *iidError = [[NSError alloc] initWithDomain:@"Error Domain" code:100 userInfo:nil]; + OCMStub([self.mockclientInfoFetcher + fetchFirebaseIIDDataWithProjectNumber:[OCMArg any] + withCompletion:([OCMArg invokeBlockWithArgs:[NSNull null], + [NSNull null], iidError, + nil])]); + + XCTestExpectation *expectation = + [self expectationWithDescription:@"fetch callback block triggered."]; + [self.fetcher + fetchMessagesWithImpressionList:@[] + withCompletion:^(NSArray *_Nullable messages, + NSNumber *_Nullable nextFetchWaitTime, + NSInteger discardCount, NSError *_Nullable error) { + // expecting triggering the completion callback with error + XCTAssertNil(messages); + XCTAssertEqualObjects(iidError, error); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} +@end diff --git a/InAppMessaging/Example/Tests/Info.plist b/InAppMessaging/Example/Tests/Info.plist new file mode 100644 index 00000000000..6c6c23c43ad --- /dev/null +++ b/InAppMessaging/Example/Tests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/InAppMessaging/Example/Tests/JsonDataWithInvalidMessagesFromFetch.txt b/InAppMessaging/Example/Tests/JsonDataWithInvalidMessagesFromFetch.txt new file mode 100644 index 00000000000..33f7492b1ec --- /dev/null +++ b/InAppMessaging/Example/Tests/JsonDataWithInvalidMessagesFromFetch.txt @@ -0,0 +1,64 @@ +{ + "messages": [ + { + "vanillaPayload": { + "campaignId": "2108810525516234752" + }, + "content": { + "modal": { + "body": { + "hexColor": "#000000" + }, + "backgroundHexColor": "#ffffff" + } + }, + "triggeringConditions": [ + { + "fiamTrigger": "ON_FOREGROUND" + } + ], + "isTestCampaign": true + }, + + { + "vanillaPayload": { + "campaignId": "2108810525516234752" + }, + "content": { + "modal": { + "title": { + "text": "FAST", + "hexColor": "#000000" + }, + "body": { + "hexColor": "#000000" + }, + "backgroundHexColor": "#ffffff" + } + }, + }, + { + "vanillaPayload": { + "campaignId": "2108810525516234752" + }, + "content": { + "unknown-type": { + "title": { + "text": "FAST", + "hexColor": "#000000" + }, + "body": { + "hexColor": "#000000" + }, + "backgroundHexColor": "#ffffff" + } + }, + "triggeringConditions": [ + { + "fiamTrigger": "ON_FOREGROUND" + } + ], + "isTestCampaign": true + }, + ] +} diff --git a/InAppMessaging/Example/Tests/NSString+InterlaceStringsTests.m b/InAppMessaging/Example/Tests/NSString+InterlaceStringsTests.m new file mode 100644 index 00000000000..727effbc350 --- /dev/null +++ b/InAppMessaging/Example/Tests/NSString+InterlaceStringsTests.m @@ -0,0 +1,60 @@ +/* + * Copyright 2019 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 + +#import "NSString+FIRInterlaceStrings.h" + +@interface NSString_InterlaceStringsTests : XCTestCase + +@end + +@implementation NSString_InterlaceStringsTests + +- (void)testEmptyStrings { + NSString *stringOne = @""; + NSString *stringTwo = @""; + XCTAssertEqualObjects(@"", [NSString fir_interlaceString:stringOne withString:stringTwo]); +} + +- (void)testSimpleExample { + NSString *stringOne = @"fe"; + NSString *stringTwo = @"rd"; + XCTAssertEqualObjects(@"fred", [NSString fir_interlaceString:stringOne withString:stringTwo]); +} + +- (void)testLongerExample { + NSString *stringOne = @"fefittn"; + NSString *stringTwo = @"rdlnsoe"; + XCTAssertEqualObjects(@"fredflintstone", [NSString fir_interlaceString:stringOne + withString:stringTwo]); +} + +- (void)testLongerFirstString { + NSString *stringOne = @"fe'lastnameisflintstone"; + NSString *stringTwo = @"rds"; + XCTAssertEqualObjects(@"fred'slastnameisflintstone", [NSString fir_interlaceString:stringOne + withString:stringTwo]); +} + +- (void)testLongerSecondString { + NSString *stringOne = @"fe'"; + NSString *stringTwo = @"rdslastnameisflintstone"; + XCTAssertEqualObjects(@"fred'slastnameisflintstone", [NSString fir_interlaceString:stringOne + withString:stringTwo]); +} + +@end diff --git a/InAppMessaging/Example/Tests/UIColor+FIRIAMHexStringTests.m b/InAppMessaging/Example/Tests/UIColor+FIRIAMHexStringTests.m new file mode 100644 index 00000000000..483d61b88b2 --- /dev/null +++ b/InAppMessaging/Example/Tests/UIColor+FIRIAMHexStringTests.m @@ -0,0 +1,59 @@ +/* + * 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 + +#import "UIColor+FIRIAMHexString.h" + +@interface UIColor_FIRIAMHexStringTests : XCTestCase + +@end + +@implementation UIColor_FIRIAMHexStringTests + +- (void)setUp { + [super setUp]; + // Put setup code here. This method is called before the invocation of each test method in the + // class. +} + +- (void)tearDown { + // Put teardown code here. This method is called after the invocation of each test method in the + // class. + [super tearDown]; +} + +- (void)testNilHexString { + UIColor *color = [UIColor firiam_colorWithHexString:nil]; + XCTAssertNil(color); +} + +- (void)testEmptyHexString { + UIColor *color = [UIColor firiam_colorWithHexString:@""]; + XCTAssertNil(color); +} + +- (void)testInvalidHexString { + UIColor *color = [UIColor firiam_colorWithHexString:@"#sssfsss"]; + XCTAssertNil(color); +} + +- (void)testValidHexString { + UIColor *color = [UIColor firiam_colorWithHexString:@"#00FFEE"]; + XCTAssertNotNil(color); +} + +@end diff --git a/Interop/Analytics/Public/FIRAnalyticsInterop.h b/Interop/Analytics/Public/FIRAnalyticsInterop.h index 5e30168e4f1..a5143c5e1d1 100644 --- a/Interop/Analytics/Public/FIRAnalyticsInterop.h +++ b/Interop/Analytics/Public/FIRAnalyticsInterop.h @@ -17,6 +17,7 @@ #import @class FIRAConditionalUserProperty; +@protocol FIRAnalyticsInteropListener; NS_ASSUME_NONNULL_BEGIN @@ -47,6 +48,13 @@ NS_ASSUME_NONNULL_BEGIN /// Sets user property. - (void)setUserPropertyWithOrigin:(NSString *)origin name:(NSString *)name value:(id)value; +/// Registers an Analytics listener for the given origin. +- (void)registerAnalyticsListener:(id)listener + withOrigin:(NSString *)origin; + +/// Unregisters an Analytics listener for the given origin. +- (void)unregisterAnalyticsListenerWithOrigin:(NSString *)origin; + @end NS_ASSUME_NONNULL_END diff --git a/Interop/Analytics/Public/FIRAnalyticsInteropListener.h b/Interop/Analytics/Public/FIRAnalyticsInteropListener.h new file mode 100644 index 00000000000..45cde55061d --- /dev/null +++ b/Interop/Analytics/Public/FIRAnalyticsInteropListener.h @@ -0,0 +1,24 @@ +/* + * Copyright 2019 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. + */ + +/// Handles events and messages from Analytics. +@protocol FIRAnalyticsInteropListener + +/// Triggers when an Analytics event happens for the registered origin with +/// `FIRAnalyticsInterop`s `registerAnalyticsListener:withOrigin:`. +- (void)messageTriggered:(NSString *)name parameters:(NSDictionary *)parameters; + +@end \ No newline at end of file From 456e8eb0f2ec75c9c287b28b537b256f2a27dd8d Mon Sep 17 00:00:00 2001 From: Konstantin Varlamov Date: Sun, 3 Feb 2019 18:01:52 -0500 Subject: [PATCH 16/27] C++ migration: port watch stream-related part of `FSTRemoteStore` (#2331) --- .../Tests/Integration/FSTDatastoreTests.mm | 2 +- .../Example/Tests/Local/FSTLocalStoreTests.mm | 9 +- .../Tests/Remote/FSTRemoteEventTests.mm | 15 +- .../Tests/SpecTests/FSTMockDatastore.h | 2 +- .../Tests/SpecTests/FSTMockDatastore.mm | 18 +- .../SpecTests/FSTSyncEngineTestDriver.mm | 2 +- Firestore/Example/Tests/Util/FSTHelpers.h | 89 ++--- Firestore/Example/Tests/Util/FSTHelpers.mm | 110 +++---- Firestore/Source/Core/FSTFirestoreClient.mm | 2 +- Firestore/Source/Remote/FSTRemoteStore.h | 56 +--- Firestore/Source/Remote/FSTRemoteStore.mm | 306 +++--------------- Firestore/Source/Remote/FSTStream.h | 27 -- .../src/firebase/firestore/remote/datastore.h | 2 +- .../firebase/firestore/remote/datastore.mm | 4 +- .../firebase/firestore/remote/remote_event.h | 48 +-- .../firebase/firestore/remote/remote_event.mm | 17 +- .../firestore/remote/remote_objc_bridge.h | 16 - .../firestore/remote/remote_objc_bridge.mm | 15 - .../firebase/firestore/remote/remote_store.h | 218 +++++++++++++ .../firebase/firestore/remote/remote_store.mm | 296 +++++++++++++++++ .../firebase/firestore/remote/watch_stream.h | 41 ++- .../firebase/firestore/remote/watch_stream.mm | 11 +- 22 files changed, 767 insertions(+), 539 deletions(-) create mode 100644 Firestore/core/src/firebase/firestore/remote/remote_store.h create mode 100644 Firestore/core/src/firebase/firestore/remote/remote_store.mm diff --git a/Firestore/Example/Tests/Integration/FSTDatastoreTests.mm b/Firestore/Example/Tests/Integration/FSTDatastoreTests.mm index f057cb5deab..ec43e8ba7eb 100644 --- a/Firestore/Example/Tests/Integration/FSTDatastoreTests.mm +++ b/Firestore/Example/Tests/Integration/FSTDatastoreTests.mm @@ -219,7 +219,7 @@ - (void)testStreamingWrite { FSTRemoteStoreEventCapture *capture = [[FSTRemoteStoreEventCapture alloc] initWithTestCase:self]; [capture expectWriteEventWithDescription:@"write mutations"]; - _remoteStore.syncEngine = capture; + [_remoteStore setSyncEngine:capture]; FSTSetMutation *mutation = [self setMutation]; FSTMutationBatch *batch = [[FSTMutationBatch alloc] initWithBatchID:23 diff --git a/Firestore/Example/Tests/Local/FSTLocalStoreTests.mm b/Firestore/Example/Tests/Local/FSTLocalStoreTests.mm index 6363c652be8..22172c8f959 100644 --- a/Firestore/Example/Tests/Local/FSTLocalStoreTests.mm +++ b/Firestore/Example/Tests/Local/FSTLocalStoreTests.mm @@ -19,6 +19,8 @@ #import #import +#include + #import "Firestore/Source/Core/FSTQuery.h" #import "Firestore/Source/Local/FSTLocalWriteResult.h" #import "Firestore/Source/Local/FSTPersistence.h" @@ -51,6 +53,7 @@ using firebase::firestore::model::SnapshotVersion; using firebase::firestore::model::TargetId; using firebase::firestore::remote::RemoteEvent; +using firebase::firestore::remote::TestTargetMetadataProvider; using firebase::firestore::remote::WatchChangeAggregator; using firebase::firestore::remote::WatchTargetChange; using firebase::firestore::remote::WatchTargetChangeState; @@ -906,9 +909,9 @@ - (void)testPersistsResumeTokens { NSData *resumeToken = FSTTestResumeTokenFromSnapshotVersion(1000); WatchTargetChange watchChange{WatchTargetChangeState::Current, {targetID}, resumeToken}; - WatchChangeAggregator aggregator{[FSTTestTargetMetadataProvider - providerWithSingleResultForKey:testutil::Key("foo/bar") - targets:{targetID}]}; + auto metadataProvider = TestTargetMetadataProvider::CreateSingleResultProvider( + testutil::Key("foo/bar"), std::vector{targetID}); + WatchChangeAggregator aggregator{&metadataProvider}; aggregator.HandleTargetChange(watchChange); RemoteEvent remoteEvent = aggregator.CreateRemoteEvent(testutil::Version(1000)); [self applyRemoteEvent:remoteEvent]; diff --git a/Firestore/Example/Tests/Remote/FSTRemoteEventTests.mm b/Firestore/Example/Tests/Remote/FSTRemoteEventTests.mm index baaa627b63d..3172d63e812 100644 --- a/Firestore/Example/Tests/Remote/FSTRemoteEventTests.mm +++ b/Firestore/Example/Tests/Remote/FSTRemoteEventTests.mm @@ -46,6 +46,7 @@ using firebase::firestore::remote::ExistenceFilterWatchChange; using firebase::firestore::remote::RemoteEvent; using firebase::firestore::remote::TargetChange; +using firebase::firestore::remote::TestTargetMetadataProvider; using firebase::firestore::remote::WatchChange; using firebase::firestore::remote::WatchChangeAggregator; using firebase::firestore::remote::WatchTargetChange; @@ -92,13 +93,12 @@ @interface FSTRemoteEventTests : XCTestCase @implementation FSTRemoteEventTests { NSData *_resumeToken1; - FSTTestTargetMetadataProvider *_targetMetadataProvider; + TestTargetMetadataProvider _targetMetadataProvider; std::unordered_map _noOutstandingResponses; } - (void)setUp { _resumeToken1 = [@"resume1" dataUsingEncoding:NSUTF8StringEncoding]; - _targetMetadataProvider = [FSTTestTargetMetadataProvider new]; } /** @@ -145,7 +145,7 @@ - (void)setUp { * considered active, or `_noOutstandingResponses` if all targets are already active. * @param existingKeys The set of documents that are considered synced with the test targets as * part of a previous listen. To modify this set during test execution, invoke - * `[_targetMetadataProvider setSyncedKeys:forQueryData:]`. + * `_targetMetadataProvider.SetSyncedKeys()`. * @param watchChanges The watch changes to apply before returning the aggregator. Supported * changes are `DocumentWatchChange` and `WatchTargetChange`. */ @@ -154,7 +154,7 @@ - (void)setUp { outstandingResponses:(const std::unordered_map &)outstandingResponses existingKeys:(DocumentKeySet)existingKeys changes:(const std::vector> &)watchChanges { - WatchChangeAggregator aggregator{_targetMetadataProvider}; + WatchChangeAggregator aggregator{&_targetMetadataProvider}; std::vector targetIDs; for (const auto &kv : targetMap) { @@ -162,7 +162,7 @@ - (void)setUp { FSTQueryData *queryData = kv.second; targetIDs.push_back(targetID); - [_targetMetadataProvider setSyncedKeys:existingKeys forQueryData:queryData]; + _targetMetadataProvider.SetSyncedKeys(existingKeys, queryData); }; for (const auto &kv : outstandingResponses) { @@ -223,7 +223,7 @@ - (void)setUp { - (void)testWillAccumulateDocumentAddedAndRemovedEvents { // The target map that contains an entry for every target in this test. If a target ID is - // omitted, the target is considered inactive and FSTTestTargetMetadataProvider will fail on + // omitted, the target is considered inactive and `TestTargetMetadataProvider` will fail on // access. std::unordered_map targetMap{ [self queryDataForTargets:{1, 2, 3, 4, 5, 6}]}; @@ -614,8 +614,7 @@ - (void)testDocumentUpdate { XCTAssertEqualObjects(event.document_updates().at(doc1.key), doc1); XCTAssertEqualObjects(event.document_updates().at(doc2.key), doc2); - [_targetMetadataProvider setSyncedKeys:DocumentKeySet{doc1.key, doc2.key} - forQueryData:targetMap[1]]; + _targetMetadataProvider.SetSyncedKeys(DocumentKeySet{doc1.key, doc2.key}, targetMap[1]); FSTDeletedDocument *deletedDoc1 = [FSTDeletedDocument documentWithKey:doc1.key version:testutil::Version(3) diff --git a/Firestore/Example/Tests/SpecTests/FSTMockDatastore.h b/Firestore/Example/Tests/SpecTests/FSTMockDatastore.h index 9d58e3416bb..59afab20fa1 100644 --- a/Firestore/Example/Tests/SpecTests/FSTMockDatastore.h +++ b/Firestore/Example/Tests/SpecTests/FSTMockDatastore.h @@ -39,7 +39,7 @@ class MockDatastore : public Datastore { util::AsyncQueue* worker_queue, auth::CredentialsProvider* credentials); - std::shared_ptr CreateWatchStream(id delegate) override; + std::shared_ptr CreateWatchStream(WatchStreamCallback* callback) override; std::shared_ptr CreateWriteStream(id delegate) override; /** diff --git a/Firestore/Example/Tests/SpecTests/FSTMockDatastore.mm b/Firestore/Example/Tests/SpecTests/FSTMockDatastore.mm index f8b6a2d6f87..3d7214c246e 100644 --- a/Firestore/Example/Tests/SpecTests/FSTMockDatastore.mm +++ b/Firestore/Example/Tests/SpecTests/FSTMockDatastore.mm @@ -70,11 +70,11 @@ CredentialsProvider* credentials_provider, FSTSerializerBeta* serializer, GrpcConnection* grpc_connection, - id delegate, + WatchStreamCallback* callback, MockDatastore* datastore) - : WatchStream{worker_queue, credentials_provider, serializer, grpc_connection, delegate}, + : WatchStream{worker_queue, credentials_provider, serializer, grpc_connection, callback}, datastore_{datastore}, - delegate_{delegate} { + callback_{callback} { } const std::unordered_map& ActiveTargets() const { @@ -84,7 +84,7 @@ void Start() override { HARD_ASSERT(!open_, "Trying to start already started watch stream"); open_ = true; - [delegate_ watchStreamDidOpen]; + callback_->OnWatchStreamOpen(); } void Stop() override { @@ -118,7 +118,7 @@ void UnwatchTargetId(model::TargetId target_id) override { void FailStream(const Status& error) { open_ = false; - [delegate_ watchStreamWasInterruptedWithError:error]; + callback_->OnWatchStreamClose(error); } void WriteWatchChange(const WatchChange& change, SnapshotVersion snap) { @@ -145,14 +145,14 @@ void WriteWatchChange(const WatchChange& change, SnapshotVersion snap) { } } - [delegate_ watchStreamDidChange:change snapshotVersion:snap]; + callback_->OnWatchStreamChange(change, snap); } private: bool open_ = false; std::unordered_map active_targets_; MockDatastore* datastore_ = nullptr; - id delegate_ = nullptr; + WatchStreamCallback* callback_ = nullptr; }; class MockWriteStream : public WriteStream { @@ -248,11 +248,11 @@ int sent_mutations_count() const { credentials_{credentials} { } -std::shared_ptr MockDatastore::CreateWatchStream(id delegate) { +std::shared_ptr MockDatastore::CreateWatchStream(WatchStreamCallback* callback) { watch_stream_ = std::make_shared( worker_queue_, credentials_, [[FSTSerializerBeta alloc] initWithDatabaseID:&database_info_->database_id()], - grpc_connection(), delegate, this); + grpc_connection(), callback, this); return watch_stream_; } diff --git a/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.mm b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.mm index 8d7b7a236da..299f15703db 100644 --- a/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.mm +++ b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.mm @@ -165,7 +165,7 @@ - (instancetype)initWithPersistence:(id)persistence _syncEngine = [[FSTSyncEngine alloc] initWithLocalStore:_localStore remoteStore:_remoteStore initialUser:initialUser]; - _remoteStore.syncEngine = _syncEngine; + [_remoteStore setSyncEngine:_syncEngine]; _eventManager = [FSTEventManager eventManagerWithSyncEngine:_syncEngine]; // Set up internal event tracking for the spec tests. diff --git a/Firestore/Example/Tests/Util/FSTHelpers.h b/Firestore/Example/Tests/Util/FSTHelpers.h index c876f2071f4..6e6f57158ab 100644 --- a/Firestore/Example/Tests/Util/FSTHelpers.h +++ b/Firestore/Example/Tests/Util/FSTHelpers.h @@ -17,6 +17,7 @@ #import #include +#include #include #import "Firestore/Source/Model/FSTDocument.h" @@ -141,51 +142,57 @@ inline NSString *FSTRemoveExceptionPrefix(NSString *exception) { } while (0) /** - * An implementation of FSTTargetMetadataProvider that provides controlled access to the - * `FSTTargetMetadataProvider` callbacks. Any target accessed via these callbacks must be + * An implementation of `TargetMetadataProvider` that provides controlled access to the + * `TargetMetadataProvider` callbacks. Any target accessed via these callbacks must be * registered beforehand via the factory methods or via `setSyncedKeys:forQueryData:`. */ -@interface FSTTestTargetMetadataProvider : NSObject - -/** - * Creates an FSTTestTargetMetadataProvider that behaves as if there's an established listen for - * each of the given targets, where each target has previously seen query results containing just - * the given documentKey. - * - * Internally this means that the `remoteKeysForTarget` callback for these targets will return just - * the documentKey and that the provided targets will be returned as active from the - * `queryDataForTarget` target. - */ -+ (instancetype) - providerWithSingleResultForKey:(firebase::firestore::model::DocumentKey)documentKey - targets: - (const std::vector &)targets; - -+ (instancetype) - providerWithSingleResultForKey:(firebase::firestore::model::DocumentKey)documentKey - listenTargets: - (const std::vector &)listenTargets - limboTargets: - (const std::vector &)limboTargets; - -/** - * Creates an FSTTestTargetMetadataProvider that behaves as if there's an established listen for - * each of the given targets, where each target has not seen any previous document. - * - * Internally this means that the `remoteKeysForTarget` callback for these targets will return an - * empty set of document keys and that the provided targets will be returned as active from the - * `queryDataForTarget` target. - */ -+ (instancetype) - providerWithEmptyResultForKey:(firebase::firestore::model::DocumentKey)documentKey - targets: - (const std::vector &)targets; +namespace firebase { +namespace firestore { +namespace remote { -/** Sets or replaces the local state for the provided query data. */ -- (void)setSyncedKeys:(firebase::firestore::model::DocumentKeySet)keys - forQueryData:(FSTQueryData *)queryData; +class TestTargetMetadataProvider : public TargetMetadataProvider { + public: + /** + * Creates a `TestTargetMetadataProvider` that behaves as if there's an established listen for + * each of the given targets, where each target has previously seen query results containing just + * the given `document_key`. + * + * Internally this means that the `GetRemoteKeysForTarget` callback for these targets will return + * just the `document_key` and that the provided targets will be returned as active from the + * `GetQueryDataForTarget` target. + */ + static TestTargetMetadataProvider CreateSingleResultProvider( + model::DocumentKey document_key, const std::vector &targets); + static TestTargetMetadataProvider CreateSingleResultProvider( + model::DocumentKey document_key, + const std::vector &targets, + const std::vector &limbo_targets); + + /** + * Creates an `TestTargetMetadataProvider` that behaves as if there's an established listen for + * each of the given targets, where each target has not seen any previous document. + * + * Internally this means that the `GetRemoteKeysForTarget` callback for these targets will return + * an empty set of document keys and that the provided targets will be returned as active from the + * `GetQueryDataForTarget` target. + */ + static TestTargetMetadataProvider CreateEmptyResultProvider( + const model::DocumentKey &document_key, const std::vector &targets); + + /** Sets or replaces the local state for the provided query data. */ + void SetSyncedKeys(model::DocumentKeySet keys, FSTQueryData *query_data); + + model::DocumentKeySet GetRemoteKeysForTarget(model::TargetId target_id) const override; + FSTQueryData *GetQueryDataForTarget(model::TargetId target_id) const override; + + private: + std::unordered_map synced_keys_; + std::unordered_map query_data_; +}; -@end +} // namespace remote +} // namespace firestore +} // namespace firebase /** Creates a new FIRTimestamp from components. Note that year, month, and day are all one-based. */ FIRTimestamp *FSTTestTimestamp(int year, int month, int day, int hour, int minute, int second); diff --git a/Firestore/Example/Tests/Util/FSTHelpers.mm b/Firestore/Example/Tests/Util/FSTHelpers.mm index e5182240d19..6ee7720c4ac 100644 --- a/Firestore/Example/Tests/Util/FSTHelpers.mm +++ b/Firestore/Example/Tests/Util/FSTHelpers.mm @@ -23,9 +23,7 @@ #include #include #include -#include #include -#include #import "Firestore/Source/API/FIRFieldPath+Internal.h" #import "Firestore/Source/API/FSTUserDataConverter.h" @@ -306,82 +304,87 @@ MaybeDocumentMap FSTTestDocUpdates(NSArray *docs) { .snapshot; } -@implementation FSTTestTargetMetadataProvider { - std::unordered_map _syncedKeys; - std::unordered_map _queryData; -} +namespace firebase { +namespace firestore { +namespace remote { -+ (instancetype)providerWithSingleResultForKey:(DocumentKey)documentKey - listenTargets:(const std::vector &)listenTargets - limboTargets:(const std::vector &)limboTargets { - FSTTestTargetMetadataProvider *metadataProvider = [FSTTestTargetMetadataProvider new]; - FSTQuery *query = [FSTQuery queryWithPath:documentKey.path()]; +TestTargetMetadataProvider TestTargetMetadataProvider::CreateSingleResultProvider( + DocumentKey document_key, + const std::vector &listen_targets, + const std::vector &limbo_targets) { + TestTargetMetadataProvider metadata_provider; + FSTQuery *query = [FSTQuery queryWithPath:document_key.path()]; - for (TargetId targetID : listenTargets) { - FSTQueryData *queryData = [[FSTQueryData alloc] initWithQuery:query - targetID:targetID - listenSequenceNumber:0 - purpose:FSTQueryPurposeListen]; - [metadataProvider setSyncedKeys:DocumentKeySet{documentKey} forQueryData:queryData]; + for (TargetId target_id : listen_targets) { + FSTQueryData *query_data = [[FSTQueryData alloc] initWithQuery:query + targetID:target_id + listenSequenceNumber:0 + purpose:FSTQueryPurposeListen]; + metadata_provider.SetSyncedKeys(DocumentKeySet{document_key}, query_data); } - for (TargetId targetID : limboTargets) { - FSTQueryData *queryData = [[FSTQueryData alloc] initWithQuery:query - targetID:targetID - listenSequenceNumber:0 - purpose:FSTQueryPurposeLimboResolution]; - [metadataProvider setSyncedKeys:DocumentKeySet{documentKey} forQueryData:queryData]; + for (TargetId target_id : limbo_targets) { + FSTQueryData *query_data = [[FSTQueryData alloc] initWithQuery:query + targetID:target_id + listenSequenceNumber:0 + purpose:FSTQueryPurposeLimboResolution]; + metadata_provider.SetSyncedKeys(DocumentKeySet{document_key}, query_data); } - return metadataProvider; + return metadata_provider; } -+ (instancetype)providerWithSingleResultForKey:(DocumentKey)documentKey - targets:(const std::vector &)targets { - return [self providerWithSingleResultForKey:documentKey listenTargets:targets limboTargets:{}]; +TestTargetMetadataProvider TestTargetMetadataProvider::CreateSingleResultProvider( + DocumentKey document_key, const std::vector &targets) { + return CreateSingleResultProvider(document_key, targets, /*limbo_targets=*/{}); } -+ (instancetype)providerWithEmptyResultForKey:(DocumentKey)documentKey - targets:(const std::vector &)targets { - FSTTestTargetMetadataProvider *metadataProvider = [FSTTestTargetMetadataProvider new]; - FSTQuery *query = [FSTQuery queryWithPath:documentKey.path()]; +TestTargetMetadataProvider TestTargetMetadataProvider::CreateEmptyResultProvider( + const DocumentKey &document_key, const std::vector &targets) { + TestTargetMetadataProvider metadata_provider; + FSTQuery *query = [FSTQuery queryWithPath:document_key.path()]; - for (TargetId targetID : targets) { - FSTQueryData *queryData = [[FSTQueryData alloc] initWithQuery:query - targetID:targetID - listenSequenceNumber:0 - purpose:FSTQueryPurposeListen]; - [metadataProvider setSyncedKeys:DocumentKeySet {} forQueryData:queryData]; + for (TargetId target_id : targets) { + FSTQueryData *query_data = [[FSTQueryData alloc] initWithQuery:query + targetID:target_id + listenSequenceNumber:0 + purpose:FSTQueryPurposeListen]; + metadata_provider.SetSyncedKeys(DocumentKeySet{}, query_data); } - return metadataProvider; + return metadata_provider; } -- (void)setSyncedKeys:(DocumentKeySet)keys forQueryData:(FSTQueryData *)queryData { - _syncedKeys[queryData.targetID] = keys; - _queryData[queryData.targetID] = queryData; +void TestTargetMetadataProvider::SetSyncedKeys(DocumentKeySet keys, FSTQueryData *query_data) { + synced_keys_[query_data.targetID] = keys; + query_data_[query_data.targetID] = query_data; } -- (DocumentKeySet)remoteKeysForTarget:(TargetId)targetID { - auto it = _syncedKeys.find(targetID); - HARD_ASSERT(it != _syncedKeys.end(), "Cannot process unknown target %s", targetID); +DocumentKeySet TestTargetMetadataProvider::GetRemoteKeysForTarget(TargetId target_id) const { + auto it = synced_keys_.find(target_id); + HARD_ASSERT(it != synced_keys_.end(), "Cannot process unknown target %s", target_id); return it->second; } -- (nullable FSTQueryData *)queryDataForTarget:(TargetId)targetID { - auto it = _queryData.find(targetID); - HARD_ASSERT(it != _queryData.end(), "Cannot process unknown target %s", targetID); +FSTQueryData *TestTargetMetadataProvider::GetQueryDataForTarget(TargetId target_id) const { + auto it = query_data_.find(target_id); + HARD_ASSERT(it != query_data_.end(), "Cannot process unknown target %s", target_id); return it->second; } -@end +} // namespace remote +} // namespace firestore +} // namespace firebase + +using firebase::firestore::remote::TestTargetMetadataProvider; RemoteEvent FSTTestAddedRemoteEvent(FSTMaybeDocument *doc, const std::vector &addedToTargets) { HARD_ASSERT(![doc isKindOfClass:[FSTDocument class]] || ![(FSTDocument *)doc hasLocalMutations], "Docs from remote updates shouldn't have local changes."); DocumentWatchChange change{addedToTargets, {}, doc.key, doc}; - WatchChangeAggregator aggregator{ - [FSTTestTargetMetadataProvider providerWithEmptyResultForKey:doc.key targets:addedToTargets]}; + auto metadataProvider = + TestTargetMetadataProvider::CreateEmptyResultProvider(doc.key, addedToTargets); + WatchChangeAggregator aggregator{&metadataProvider}; aggregator.HandleDocumentChange(change); return aggregator.CreateRemoteEvent(doc.version); } @@ -414,10 +417,9 @@ RemoteEvent FSTTestUpdateRemoteEventWithLimboTargets( std::vector listens = updatedInTargets; listens.insert(listens.end(), removedFromTargets.begin(), removedFromTargets.end()); - WatchChangeAggregator aggregator{[FSTTestTargetMetadataProvider - providerWithSingleResultForKey:doc.key - listenTargets:listens - limboTargets:limboTargets]}; + auto metadataProvider = + TestTargetMetadataProvider::CreateSingleResultProvider(doc.key, listens, limboTargets); + WatchChangeAggregator aggregator{&metadataProvider}; aggregator.HandleDocumentChange(change); return aggregator.CreateRemoteEvent(doc.version); } diff --git a/Firestore/Source/Core/FSTFirestoreClient.mm b/Firestore/Source/Core/FSTFirestoreClient.mm index 6f9161f1706..4de9a6a39f2 100644 --- a/Firestore/Source/Core/FSTFirestoreClient.mm +++ b/Firestore/Source/Core/FSTFirestoreClient.mm @@ -240,7 +240,7 @@ - (void)initializeWithUser:(const User &)user settings:(FIRFirestoreSettings *)s _eventManager = [FSTEventManager eventManagerWithSyncEngine:_syncEngine]; // Setup wiring for remote store. - _remoteStore.syncEngine = _syncEngine; + [_remoteStore setSyncEngine:_syncEngine]; // NOTE: RemoteStore depends on LocalStore (for persisting stream tokens, refilling mutation // queue, etc.) so must be started after LocalStore. diff --git a/Firestore/Source/Remote/FSTRemoteStore.h b/Firestore/Source/Remote/FSTRemoteStore.h index c52868bf788..6b1aa4824af 100644 --- a/Firestore/Source/Remote/FSTRemoteStore.h +++ b/Firestore/Source/Remote/FSTRemoteStore.h @@ -23,6 +23,7 @@ #include "Firestore/core/src/firebase/firestore/model/types.h" #include "Firestore/core/src/firebase/firestore/remote/datastore.h" #include "Firestore/core/src/firebase/firestore/remote/remote_event.h" +#include "Firestore/core/src/firebase/firestore/remote/remote_store.h" #include "Firestore/core/src/firebase/firestore/util/async_queue.h" @class FSTLocalStore; @@ -33,64 +34,13 @@ NS_ASSUME_NONNULL_BEGIN -#pragma mark - FSTRemoteSyncer - -/** - * A protocol that describes the actions the FSTRemoteStore needs to perform on a cooperating - * synchronization engine. - */ -@protocol FSTRemoteSyncer - -/** - * Applies one remote event to the sync engine, notifying any views of the changes, and releasing - * any pending mutation batches that would become visible because of the snapshot version the - * remote event contains. - */ -- (void)applyRemoteEvent:(const firebase::firestore::remote::RemoteEvent &)remoteEvent; - -/** - * Rejects the listen for the given targetID. This can be triggered by the backend for any active - * target. - * - * @param targetID The targetID corresponding to a listen initiated via - * -listenToTargetWithQueryData: on FSTRemoteStore. - * @param error A description of the condition that has forced the rejection. Nearly always this - * will be an indication that the user is no longer authorized to see the data matching the - * target. - */ -- (void)rejectListenWithTargetID:(const firebase::firestore::model::TargetId)targetID - error:(NSError *)error; - -/** - * Applies the result of a successful write of a mutation batch to the sync engine, emitting - * snapshots in any views that the mutation applies to, and removing the batch from the mutation - * queue. - */ -- (void)applySuccessfulWriteWithResult:(FSTMutationBatchResult *)batchResult; - -/** - * Rejects the batch, removing the batch from the mutation queue, recomputing the local view of - * any documents affected by the batch and then, emitting snapshots with the reverted value. - */ -- (void)rejectFailedWriteWithBatchID:(firebase::firestore::model::BatchId)batchID - error:(NSError *)error; - -/** - * Returns the set of remote document keys for the given target ID. This list includes the - * documents that were assigned to the target when we received the last snapshot. - */ -- (firebase::firestore::model::DocumentKeySet)remoteKeysForTarget: - (firebase::firestore::model::TargetId)targetId; - -@end - #pragma mark - FSTRemoteStore /** * FSTRemoteStore handles all interaction with the backend through a simple, clean interface. This * class is not thread safe and should be only called from the worker dispatch queue. */ -@interface FSTRemoteStore : NSObject +@interface FSTRemoteStore : NSObject - (instancetype) initWithLocalStore:(FSTLocalStore *)localStore @@ -101,7 +51,7 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)init NS_UNAVAILABLE; -@property(nonatomic, weak) id syncEngine; +- (void)setSyncEngine:(id)syncEngine; /** Starts up the remote store, creating streams, restoring state from LocalStore, etc. */ - (void)start; diff --git a/Firestore/Source/Remote/FSTRemoteStore.mm b/Firestore/Source/Remote/FSTRemoteStore.mm index c765e5c46a7..7b4f215c911 100644 --- a/Firestore/Source/Remote/FSTRemoteStore.mm +++ b/Firestore/Source/Remote/FSTRemoteStore.mm @@ -36,6 +36,7 @@ #include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" #include "Firestore/core/src/firebase/firestore/remote/online_state_tracker.h" #include "Firestore/core/src/firebase/firestore/remote/remote_event.h" +#include "Firestore/core/src/firebase/firestore/remote/remote_store.h" #include "Firestore/core/src/firebase/firestore/remote/stream.h" #include "Firestore/core/src/firebase/firestore/util/error_apple.h" #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" @@ -61,6 +62,7 @@ using firebase::firestore::remote::ExistenceFilterWatchChange; using firebase::firestore::remote::OnlineStateTracker; using firebase::firestore::remote::RemoteEvent; +using firebase::firestore::remote::RemoteStore; using firebase::firestore::remote::TargetChange; using firebase::firestore::remote::WatchChange; using firebase::firestore::remote::WatchChangeAggregator; @@ -79,13 +81,7 @@ #pragma mark - FSTRemoteStore -@interface FSTRemoteStore () - -/** - * The local store, used to fill the write pipeline with outbound mutations and resolve existence - * filter mismatches. Immutable after initialization. - */ -@property(nonatomic, strong, readonly) FSTLocalStore *localStore; +@interface FSTRemoteStore () #pragma mark Watch Stream @@ -107,29 +103,11 @@ @interface FSTRemoteStore () @end @implementation FSTRemoteStore { - OnlineStateTracker _onlineStateTracker; - - std::unique_ptr _watchChangeAggregator; - /** The client-side proxy for interacting with the backend. */ std::shared_ptr _datastore; - /** - * A mapping of watched targets that the client cares about tracking and the - * user has explicitly called a 'listen' for this target. - * - * These targets may or may not have been sent to or acknowledged by the - * server. On re-establishing the listen stream, these targets should be sent - * to the server. The targets removed with unlistens are removed eagerly - * without waiting for confirmation from the listen stream. */ - std::unordered_map _listenTargets; - - std::shared_ptr _watchStream; + + std::unique_ptr _remoteStore; std::shared_ptr _writeStream; - /** - * Set to YES by 'enableNetwork:' and NO by 'disableNetworkInternal:' and - * indicates the user-preferred network state. - */ - BOOL _isNetworkEnabled; } - (instancetype)initWithLocalStore:(FSTLocalStore *)localStore @@ -137,22 +115,25 @@ - (instancetype)initWithLocalStore:(FSTLocalStore *)localStore workerQueue:(AsyncQueue *)queue onlineStateHandler:(std::function)onlineStateHandler { if (self = [super init]) { - _localStore = localStore; _datastore = std::move(datastore); _writePipeline = [NSMutableArray array]; - _onlineStateTracker = OnlineStateTracker{queue, std::move(onlineStateHandler)}; _datastore->Start(); - // Create streams (but note they're not started yet) - _watchStream = _datastore->CreateWatchStream(self); + + _remoteStore = absl::make_unique(localStore, _datastore.get(), queue, + std::move(onlineStateHandler)); _writeStream = _datastore->CreateWriteStream(self); - _isNetworkEnabled = NO; + _remoteStore->set_is_network_enabled(false); } return self; } +- (void)setSyncEngine:(id)syncEngine { + _remoteStore->set_sync_engine(syncEngine); +} + - (void)start { // For now, all setup is handled by enableNetwork(). We might expand on this in the future. [self enableNetwork]; @@ -160,23 +141,17 @@ - (void)start { #pragma mark Online/Offline state -- (BOOL)canUseNetwork { - // PORTING NOTE: This method exists mostly because web also has to take into - // account primary vs. secondary state. - return _isNetworkEnabled; -} - - (void)enableNetwork { - _isNetworkEnabled = YES; + _remoteStore->set_is_network_enabled(true); - if ([self canUseNetwork]) { + if (_remoteStore->CanUseNetwork()) { // Load any saved stream token from persistent storage - _writeStream->SetLastStreamToken([self.localStore lastStreamToken]); + _writeStream->SetLastStreamToken([_remoteStore->local_store() lastStreamToken]); - if ([self shouldStartWatchStream]) { - [self startWatchStream]; + if (_remoteStore->ShouldStartWatchStream()) { + _remoteStore->StartWatchStream(); } else { - _onlineStateTracker.UpdateState(OnlineState::Unknown); + _remoteStore->online_state_tracker().UpdateState(OnlineState::Unknown); } // This will start the write stream if necessary. @@ -185,16 +160,16 @@ - (void)enableNetwork { } - (void)disableNetwork { - _isNetworkEnabled = NO; + _remoteStore->set_is_network_enabled(false); [self disableNetworkInternal]; // Set the OnlineState to Offline so get()s return from cache, etc. - _onlineStateTracker.UpdateState(OnlineState::Offline); + _remoteStore->online_state_tracker().UpdateState(OnlineState::Offline); } /** Disables the network, setting the OnlineState to the specified targetOnlineState. */ - (void)disableNetworkInternal { - _watchStream->Stop(); + _remoteStore->watch_stream().Stop(); _writeStream->Stop(); if (self.writePipeline.count > 0) { @@ -203,246 +178,42 @@ - (void)disableNetworkInternal { [self.writePipeline removeAllObjects]; } - [self cleanUpWatchStreamState]; + _remoteStore->CleanUpWatchStreamState(); } #pragma mark Shutdown - (void)shutdown { LOG_DEBUG("FSTRemoteStore %s shutting down", (__bridge void *)self); - _isNetworkEnabled = NO; + _remoteStore->set_is_network_enabled(false); [self disableNetworkInternal]; // Set the OnlineState to Unknown (rather than Offline) to avoid potentially triggering // spurious listener events with cached data, etc. - _onlineStateTracker.UpdateState(OnlineState::Unknown); + _remoteStore->online_state_tracker().UpdateState(OnlineState::Unknown); _datastore->Shutdown(); } - (void)credentialDidChange { - if ([self canUseNetwork]) { + if (_remoteStore->CanUseNetwork()) { // Tear down and re-create our network streams. This will ensure we get a fresh auth token // for the new user and re-fill the write pipeline with new mutations from the LocalStore // (since mutations are per-user). LOG_DEBUG("FSTRemoteStore %s restarting streams for new credential", (__bridge void *)self); - _isNetworkEnabled = NO; + _remoteStore->set_is_network_enabled(false); [self disableNetworkInternal]; - _onlineStateTracker.UpdateState(OnlineState::Unknown); + _remoteStore->online_state_tracker().UpdateState(OnlineState::Unknown); [self enableNetwork]; } } #pragma mark Watch Stream -- (void)startWatchStream { - HARD_ASSERT([self shouldStartWatchStream], - "startWatchStream: called when shouldStartWatchStream: is false."); - _watchChangeAggregator = absl::make_unique(self); - _watchStream->Start(); - - _onlineStateTracker.HandleWatchStreamStart(); -} - - (void)listenToTargetWithQueryData:(FSTQueryData *)queryData { - TargetId targetKey = queryData.targetID; - HARD_ASSERT(_listenTargets.find(targetKey) == _listenTargets.end(), - "listenToQuery called with duplicate target id: %s", targetKey); - - _listenTargets[targetKey] = queryData; - - if ([self shouldStartWatchStream]) { - [self startWatchStream]; - } else if (_watchStream->IsOpen()) { - [self sendWatchRequestWithQueryData:queryData]; - } -} - -- (void)sendWatchRequestWithQueryData:(FSTQueryData *)queryData { - _watchChangeAggregator->RecordPendingTargetRequest(queryData.targetID); - _watchStream->WatchQuery(queryData); + _remoteStore->Listen(queryData); } - (void)stopListeningToTargetID:(TargetId)targetID { - size_t num_erased = _listenTargets.erase(targetID); - HARD_ASSERT(num_erased == 1, "stopListeningToTargetID: target not currently watched: %s", - targetID); - - if (_watchStream->IsOpen()) { - [self sendUnwatchRequestForTargetID:targetID]; - } - if (_listenTargets.empty()) { - if (_watchStream->IsOpen()) { - _watchStream->MarkIdle(); - } else if ([self canUseNetwork]) { - // Revert to OnlineState::Unknown if the watch stream is not open and we have no listeners, - // since without any listens to send we cannot confirm if the stream is healthy and upgrade - // to OnlineState::Online. - _onlineStateTracker.UpdateState(OnlineState::Unknown); - } - } -} - -- (void)sendUnwatchRequestForTargetID:(TargetId)targetID { - _watchChangeAggregator->RecordPendingTargetRequest(targetID); - _watchStream->UnwatchTargetId(targetID); -} - -/** - * Returns YES if the network is enabled, the watch stream has not yet been started and there are - * active watch targets. - */ -- (BOOL)shouldStartWatchStream { - return [self canUseNetwork] && !_watchStream->IsStarted() && !_listenTargets.empty(); -} - -- (void)cleanUpWatchStreamState { - _watchChangeAggregator.reset(); -} - -- (void)watchStreamDidOpen { - // Restore any existing watches. - for (const auto &kv : _listenTargets) { - [self sendWatchRequestWithQueryData:kv.second]; - } -} - -- (void)watchStreamDidChange:(const WatchChange &)change - snapshotVersion:(const SnapshotVersion &)snapshotVersion { - // Mark the connection as Online because we got a message from the server. - _onlineStateTracker.UpdateState(OnlineState::Online); - - if (change.type() == WatchChange::Type::TargetChange) { - const WatchTargetChange &watchTargetChange = static_cast(change); - if (watchTargetChange.state() == WatchTargetChangeState::Removed && - !watchTargetChange.cause().ok()) { - // There was an error on a target, don't wait for a consistent snapshot to raise events - return [self processTargetErrorForWatchChange:watchTargetChange]; - } else { - _watchChangeAggregator->HandleTargetChange(watchTargetChange); - } - } else if (change.type() == WatchChange::Type::Document) { - _watchChangeAggregator->HandleDocumentChange(static_cast(change)); - } else { - HARD_ASSERT(change.type() == WatchChange::Type::ExistenceFilter, - "Expected watchChange to be an instance of ExistenceFilterWatchChange"); - _watchChangeAggregator->HandleExistenceFilter( - static_cast(change)); - } - - if (snapshotVersion != SnapshotVersion::None() && - snapshotVersion >= [self.localStore lastRemoteSnapshotVersion]) { - // We have received a target change with a global snapshot if the snapshot version is not - // equal to SnapshotVersion.None(). - [self raiseWatchSnapshotWithSnapshotVersion:snapshotVersion]; - } -} - -- (void)watchStreamWasInterruptedWithError:(const Status &)error { - if (error.ok()) { - // Graceful stop (due to Stop() or idle timeout). Make sure that's desirable. - HARD_ASSERT(![self shouldStartWatchStream], - "Watch stream was stopped gracefully while still needed."); - } - - [self cleanUpWatchStreamState]; - - // If we still need the watch stream, retry the connection. - if ([self shouldStartWatchStream]) { - _onlineStateTracker.HandleWatchStreamFailure(error); - - [self startWatchStream]; - } else { - // We don't need to restart the watch stream because there are no active targets. The online - // state is set to unknown because there is no active attempt at establishing a connection. - _onlineStateTracker.UpdateState(OnlineState::Unknown); - } -} - -/** - * Takes a batch of changes from the Datastore, repackages them as a `RemoteEvent`, and passes that - * on to the SyncEngine. - */ -- (void)raiseWatchSnapshotWithSnapshotVersion:(const SnapshotVersion &)snapshotVersion { - HARD_ASSERT(snapshotVersion != SnapshotVersion::None(), - "Can't raise event for unknown SnapshotVersion"); - - RemoteEvent remoteEvent = _watchChangeAggregator->CreateRemoteEvent(snapshotVersion); - - // Update in-memory resume tokens. `FSTLocalStore` will update the persistent view of these when - // applying the completed `RemoteEvent`. - for (const auto &entry : remoteEvent.target_changes()) { - const TargetChange &target_change = entry.second; - NSData *resumeToken = target_change.resume_token(); - if (resumeToken.length > 0) { - TargetId targetID = entry.first; - auto found = _listenTargets.find(targetID); - FSTQueryData *queryData = found != _listenTargets.end() ? found->second : nil; - // A watched target might have been removed already. - if (queryData) { - _listenTargets[targetID] = - [queryData queryDataByReplacingSnapshotVersion:snapshotVersion - resumeToken:resumeToken - sequenceNumber:queryData.sequenceNumber]; - } - } - } - - // Re-establish listens for the targets that have been invalidated by existence filter - // mismatches. - for (TargetId targetID : remoteEvent.target_mismatches()) { - auto found = _listenTargets.find(targetID); - if (found == _listenTargets.end()) { - // A watched target might have been removed already. - continue; - } - FSTQueryData *queryData = found->second; - - // Clear the resume token for the query, since we're in a known mismatch state. - queryData = [[FSTQueryData alloc] initWithQuery:queryData.query - targetID:targetID - listenSequenceNumber:queryData.sequenceNumber - purpose:queryData.purpose]; - _listenTargets[targetID] = queryData; - - // Cause a hard reset by unwatching and rewatching immediately, but deliberately don't send a - // resume token so that we get a full update. - [self sendUnwatchRequestForTargetID:targetID]; - - // Mark the query we send as being on behalf of an existence filter mismatch, but don't - // actually retain that in _listenTargets. This ensures that we flag the first re-listen this - // way without impacting future listens of this target (that might happen e.g. on reconnect). - FSTQueryData *requestQueryData = - [[FSTQueryData alloc] initWithQuery:queryData.query - targetID:targetID - listenSequenceNumber:queryData.sequenceNumber - purpose:FSTQueryPurposeExistenceFilterMismatch]; - [self sendWatchRequestWithQueryData:requestQueryData]; - } - - // Finally handle remote event - [self.syncEngine applyRemoteEvent:remoteEvent]; -} - -/** Process a target error and passes the error along to SyncEngine. */ -- (void)processTargetErrorForWatchChange:(const WatchTargetChange &)change { - HARD_ASSERT(!change.cause().ok(), "Handling target error without a cause"); - // Ignore targets that have been removed already. - for (TargetId targetID : change.target_ids()) { - auto found = _listenTargets.find(targetID); - if (found != _listenTargets.end()) { - _listenTargets.erase(found); - _watchChangeAggregator->RemoveTarget(targetID); - [self.syncEngine rejectListenWithTargetID:targetID error:util::MakeNSError(change.cause())]; - } - } -} - -- (DocumentKeySet)remoteKeysForTarget:(TargetId)targetID { - return [self.syncEngine remoteKeysForTarget:targetID]; -} - -- (nullable FSTQueryData *)queryDataForTarget:(TargetId)targetID { - auto found = _listenTargets.find(targetID); - return found != _listenTargets.end() ? found->second : nil; + _remoteStore->StopListening(targetID); } #pragma mark Write Stream @@ -452,7 +223,8 @@ - (nullable FSTQueryData *)queryDataForTarget:(TargetId)targetID { * pending writes. */ - (BOOL)shouldStartWriteStream { - return [self canUseNetwork] && !_writeStream->IsStarted() && self.writePipeline.count > 0; + return _remoteStore->CanUseNetwork() && !_writeStream->IsStarted() && + self.writePipeline.count > 0; } - (void)startWriteStream { @@ -473,7 +245,8 @@ - (void)fillWritePipeline { BatchId lastBatchIDRetrieved = self.writePipeline.count == 0 ? kBatchIdUnknown : self.writePipeline.lastObject.batchID; while ([self canAddToWritePipeline]) { - FSTMutationBatch *batch = [self.localStore nextMutationBatchAfterBatchID:lastBatchIDRetrieved]; + FSTMutationBatch *batch = + [_remoteStore->local_store() nextMutationBatchAfterBatchID:lastBatchIDRetrieved]; if (!batch) { if (self.writePipeline.count == 0) { _writeStream->MarkIdle(); @@ -493,7 +266,7 @@ - (void)fillWritePipeline { * Returns YES if we can add to the write pipeline (i.e. it is not full and the network is enabled). */ - (BOOL)canAddToWritePipeline { - return [self canUseNetwork] && self.writePipeline.count < kMaxPendingWrites; + return _remoteStore->CanUseNetwork() && self.writePipeline.count < kMaxPendingWrites; } /** @@ -520,7 +293,7 @@ - (void)writeStreamDidOpen { */ - (void)writeStreamDidCompleteHandshake { // Record the stream token. - [self.localStore setLastStreamToken:_writeStream->GetLastStreamToken()]; + [_remoteStore->local_store() setLastStreamToken:_writeStream->GetLastStreamToken()]; // Send the write pipeline now that the stream is established. for (FSTMutationBatch *write in self.writePipeline) { @@ -542,7 +315,7 @@ - (void)writeStreamDidReceiveResponseWithVersion:(const SnapshotVersion &)commit commitVersion:commitVersion mutationResults:results streamToken:_writeStream->GetLastStreamToken()]; - [self.syncEngine applySuccessfulWriteWithResult:batchResult]; + [_remoteStore->sync_engine() applySuccessfulWriteWithResult:batchResult]; // It's possible that with the completion of this mutation another slot has freed up. [self fillWritePipeline]; @@ -589,7 +362,7 @@ - (void)handleHandshakeError:(const Status &)error { "error code: '%s', details: '%s'", (__bridge void *)self, token, error.code(), error.error_message()); _writeStream->SetLastStreamToken(nil); - [self.localStore setLastStreamToken:nil]; + [_remoteStore->local_store() setLastStreamToken:nil]; } else { // Some other error, don't reset stream token. Our stream logic will just retry with exponential // backoff. @@ -612,7 +385,8 @@ - (void)handleWriteError:(const Status &)error { // bad request so inhibit backoff on the next restart. _writeStream->InhibitBackoff(); - [self.syncEngine rejectFailedWriteWithBatchID:batch.batchID error:util::MakeNSError(error)]; + [_remoteStore->sync_engine() rejectFailedWriteWithBatchID:batch.batchID + error:util::MakeNSError(error)]; // It's possible that with the completion of this mutation another slot has freed up. [self fillWritePipeline]; diff --git a/Firestore/Source/Remote/FSTStream.h b/Firestore/Source/Remote/FSTStream.h index d4183379329..211a82154f5 100644 --- a/Firestore/Source/Remote/FSTStream.h +++ b/Firestore/Source/Remote/FSTStream.h @@ -24,33 +24,6 @@ NS_ASSUME_NONNULL_BEGIN -#pragma mark - FSTWatchStreamDelegate - -/** A protocol defining the events that can be emitted by the FSTWatchStream. */ -@protocol FSTWatchStreamDelegate - -/** Called by the FSTWatchStream when it is ready to accept outbound request messages. */ -- (void)watchStreamDidOpen; - -/** - * Called by the FSTWatchStream with changes and the snapshot versions included in in the - * WatchChange responses sent back by the server. - */ -- (void)watchStreamDidChange:(const firebase::firestore::remote::WatchChange &)change - snapshotVersion:(const firebase::firestore::model::SnapshotVersion &)snapshotVersion; - -/** - * Called by the FSTWatchStream when the underlying streaming RPC is interrupted for whatever - * reason, usually because of an error, but possibly due to an idle timeout. The error passed to - * this method may be nil, in which case the stream was closed without attributable fault. - * - * NOTE: This will not be called after `stop` is called on the stream. See "Starting and Stopping" - * on FSTStream for details. - */ -- (void)watchStreamWasInterruptedWithError:(const firebase::firestore::util::Status &)error; - -@end - #pragma mark - FSTWriteStreamDelegate @protocol FSTWriteStreamDelegate diff --git a/Firestore/core/src/firebase/firestore/remote/datastore.h b/Firestore/core/src/firebase/firestore/remote/datastore.h index d6d8a358619..9e2268ea571 100644 --- a/Firestore/core/src/firebase/firestore/remote/datastore.h +++ b/Firestore/core/src/firebase/firestore/remote/datastore.h @@ -85,7 +85,7 @@ class Datastore : public std::enable_shared_from_this { * shared channel. */ virtual std::shared_ptr CreateWatchStream( - id delegate); + WatchStreamCallback* callback); /** * Creates a new `WriteStream` that is still unstarted but uses a common * shared channel. diff --git a/Firestore/core/src/firebase/firestore/remote/datastore.mm b/Firestore/core/src/firebase/firestore/remote/datastore.mm index 9cf387fdbec..1bf2376d8f8 100644 --- a/Firestore/core/src/firebase/firestore/remote/datastore.mm +++ b/Firestore/core/src/firebase/firestore/remote/datastore.mm @@ -151,10 +151,10 @@ void LogGrpcCallFinished(absl::string_view rpc_name, } std::shared_ptr Datastore::CreateWatchStream( - id delegate) { + WatchStreamCallback* callback) { return std::make_shared(worker_queue_, credentials_, serializer_bridge_.GetSerializer(), - &grpc_connection_, delegate); + &grpc_connection_, callback); } std::shared_ptr Datastore::CreateWriteStream( diff --git a/Firestore/core/src/firebase/firestore/remote/remote_event.h b/Firestore/core/src/firebase/firestore/remote/remote_event.h index e154b9ee623..8a1fc4ce066 100644 --- a/Firestore/core/src/firebase/firestore/remote/remote_event.h +++ b/Firestore/core/src/firebase/firestore/remote/remote_event.h @@ -43,31 +43,33 @@ NS_ASSUME_NONNULL_BEGIN -/** - * Interface implemented by RemoteStore to expose target metadata to the - * `WatchChangeAggregator`. - */ -@protocol FSTTargetMetadataProvider - -/** - * Returns the set of remote document keys for the given target ID as of the - * last raised snapshot. - */ -- (firebase::firestore::model::DocumentKeySet)remoteKeysForTarget: - (firebase::firestore::model::TargetId)targetID; +namespace firebase { +namespace firestore { +namespace remote { /** - * Returns the FSTQueryData for an active target ID or 'null' if this query has - * become inactive + * Interface implemented by `RemoteStore` to expose target metadata to the + * `WatchChangeAggregator`. */ -- (nullable FSTQueryData*)queryDataForTarget: - (firebase::firestore::model::TargetId)targetID; +class TargetMetadataProvider { + public: + virtual ~TargetMetadataProvider() { + } -@end + /** + * Returns the set of remote document keys for the given target ID as of the + * last raised snapshot. + */ + virtual model::DocumentKeySet GetRemoteKeysForTarget( + model::TargetId target_id) const = 0; -namespace firebase { -namespace firestore { -namespace remote { + /** + * Returns the FSTQueryData for an active target ID or 'null' if this query + * has become inactive + */ + virtual FSTQueryData* GetQueryDataForTarget( + model::TargetId target_id) const = 0; +}; /** * A `TargetChange` specifies the set of changes for a specific target as part @@ -309,9 +311,7 @@ class RemoteEvent { class WatchChangeAggregator { public: explicit WatchChangeAggregator( - id target_metadata_provider) - : target_metadata_provider_{target_metadata_provider} { - } + TargetMetadataProvider* target_metadata_provider); /** * Processes and adds the `DocumentWatchChange` to the current set of changes. @@ -437,7 +437,7 @@ class WatchChangeAggregator { */ std::unordered_set pending_target_resets_; - id target_metadata_provider_; + TargetMetadataProvider* target_metadata_provider_ = nullptr; }; } // namespace remote diff --git a/Firestore/core/src/firebase/firestore/remote/remote_event.mm b/Firestore/core/src/firebase/firestore/remote/remote_event.mm index fd4355ddab2..65bb82b0339 100644 --- a/Firestore/core/src/firebase/firestore/remote/remote_event.mm +++ b/Firestore/core/src/firebase/firestore/remote/remote_event.mm @@ -114,6 +114,11 @@ // WatchChangeAggregator +WatchChangeAggregator::WatchChangeAggregator( + TargetMetadataProvider* target_metadata_provider) + : target_metadata_provider_{NOT_NULL(target_metadata_provider)} { +} + void WatchChangeAggregator::HandleDocumentChange( const DocumentWatchChange& document_change) { for (TargetId target_id : document_change.updated_target_ids()) { @@ -360,9 +365,9 @@ TargetId target_id) { TargetState& target_state = EnsureTargetState(target_id); TargetChange target_change = target_state.ToTargetChange(); - return ([target_metadata_provider_ remoteKeysForTarget:target_id].size() + - target_change.added_documents().size() - - target_change.removed_documents().size()); + return target_metadata_provider_->GetRemoteKeysForTarget(target_id).size() + + target_change.added_documents().size() - + target_change.removed_documents().size(); } void WatchChangeAggregator::RecordPendingTargetRequest(TargetId target_id) { @@ -385,7 +390,7 @@ return target_state != target_states_.end() && target_state->second.IsPending() ? nil - : [target_metadata_provider_ queryDataForTarget:target_id]; + : target_metadata_provider_->GetQueryDataForTarget(target_id); } void WatchChangeAggregator::ResetTarget(TargetId target_id) { @@ -400,7 +405,7 @@ // removals will be part of the initial snapshot if Watch does not resend // these documents. DocumentKeySet existingKeys = - [target_metadata_provider_ remoteKeysForTarget:target_id]; + target_metadata_provider_->GetRemoteKeysForTarget(target_id); for (const DocumentKey& key : existingKeys) { RemoveDocumentFromTarget(target_id, key, nil); @@ -410,7 +415,7 @@ bool WatchChangeAggregator::TargetContainsDocument(TargetId target_id, const DocumentKey& key) { const DocumentKeySet& existing_keys = - [target_metadata_provider_ remoteKeysForTarget:target_id]; + target_metadata_provider_->GetRemoteKeysForTarget(target_id); return existing_keys.contains(key); } diff --git a/Firestore/core/src/firebase/firestore/remote/remote_objc_bridge.h b/Firestore/core/src/firebase/firestore/remote/remote_objc_bridge.h index c655063c4b0..e5cefba467a 100644 --- a/Firestore/core/src/firebase/firestore/remote/remote_objc_bridge.h +++ b/Firestore/core/src/firebase/firestore/remote/remote_objc_bridge.h @@ -172,22 +172,6 @@ class DatastoreSerializer { FSTSerializerBeta* serializer_; }; -/** A C++ bridge that invokes methods on an `FSTWatchStreamDelegate`. */ -class WatchStreamDelegate { - public: - explicit WatchStreamDelegate(id delegate) - : delegate_{delegate} { - } - - void NotifyDelegateOnOpen(); - void NotifyDelegateOnChange(const WatchChange& change, - const model::SnapshotVersion& snapshot_version); - void NotifyDelegateOnClose(const util::Status& status); - - private: - __weak id delegate_; -}; - /** A C++ bridge that invokes methods on an `FSTWriteStreamDelegate`. */ class WriteStreamDelegate { public: diff --git a/Firestore/core/src/firebase/firestore/remote/remote_objc_bridge.mm b/Firestore/core/src/firebase/firestore/remote/remote_objc_bridge.mm index 8159fc96806..406f9199f6d 100644 --- a/Firestore/core/src/firebase/firestore/remote/remote_objc_bridge.mm +++ b/Firestore/core/src/firebase/firestore/remote/remote_objc_bridge.mm @@ -300,21 +300,6 @@ bool IsLoggingEnabled() { return [serializer_ decodedMaybeDocumentFromBatch:response]; } -// WatchStreamDelegate - -void WatchStreamDelegate::NotifyDelegateOnOpen() { - [delegate_ watchStreamDidOpen]; -} - -void WatchStreamDelegate::NotifyDelegateOnChange( - const WatchChange& change, const SnapshotVersion& snapshot_version) { - [delegate_ watchStreamDidChange:change snapshotVersion:snapshot_version]; -} - -void WatchStreamDelegate::NotifyDelegateOnClose(const Status& status) { - [delegate_ watchStreamWasInterruptedWithError:status]; -} - // WriteStreamDelegate void WriteStreamDelegate::NotifyDelegateOnOpen() { diff --git a/Firestore/core/src/firebase/firestore/remote/remote_store.h b/Firestore/core/src/firebase/firestore/remote/remote_store.h new file mode 100644 index 00000000000..4d9180fe365 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/remote/remote_store.h @@ -0,0 +1,218 @@ +/* + * Copyright 2019 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. + */ + +#ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_REMOTE_REMOTE_STORE_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_REMOTE_REMOTE_STORE_H_ + +#if !defined(__OBJC__) +#error "This header only supports Objective-C++" +#endif // !defined(__OBJC__) + +#import + +#include +#include + +#include "Firestore/core/src/firebase/firestore/model/document_key_set.h" +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" +#include "Firestore/core/src/firebase/firestore/model/types.h" +#include "Firestore/core/src/firebase/firestore/remote/datastore.h" +#include "Firestore/core/src/firebase/firestore/remote/online_state_tracker.h" +#include "Firestore/core/src/firebase/firestore/remote/remote_event.h" +#include "Firestore/core/src/firebase/firestore/remote/watch_change.h" +#include "Firestore/core/src/firebase/firestore/remote/watch_stream.h" +#include "Firestore/core/src/firebase/firestore/util/async_queue.h" +#include "Firestore/core/src/firebase/firestore/util/status.h" + +@class FSTLocalStore; +@class FSTMutationBatchResult; +@class FSTQueryData; + +NS_ASSUME_NONNULL_BEGIN + +/** + * A protocol that describes the actions the FSTRemoteStore needs to perform on + * a cooperating synchronization engine. + */ +@protocol FSTRemoteSyncer + +/** + * Applies one remote event to the sync engine, notifying any views of the + * changes, and releasing any pending mutation batches that would become visible + * because of the snapshot version the remote event contains. + */ +- (void)applyRemoteEvent: + (const firebase::firestore::remote::RemoteEvent&)remoteEvent; + +/** + * Rejects the listen for the given targetID. This can be triggered by the + * backend for any active target. + * + * @param targetID The targetID corresponding to a listen initiated via + * -listenToTargetWithQueryData: on FSTRemoteStore. + * @param error A description of the condition that has forced the rejection. + * Nearly always this will be an indication that the user is no longer + * authorized to see the data matching the target. + */ +- (void)rejectListenWithTargetID: + (const firebase::firestore::model::TargetId)targetID + error: + (NSError*)error; // NOLINT(readability/casting) + +/** + * Applies the result of a successful write of a mutation batch to the sync + * engine, emitting snapshots in any views that the mutation applies to, and + * removing the batch from the mutation queue. + */ +- (void)applySuccessfulWriteWithResult: + (FSTMutationBatchResult*)batchResult; // NOLINT(readability/casting) + +/** + * Rejects the batch, removing the batch from the mutation queue, recomputing + * the local view of any documents affected by the batch and then, emitting + * snapshots with the reverted value. + */ +- (void) + rejectFailedWriteWithBatchID:(firebase::firestore::model::BatchId)batchID + error: + (NSError*)error; // NOLINT(readability/casting) + +/** + * Returns the set of remote document keys for the given target ID. This list + * includes the documents that were assigned to the target when we received the + * last snapshot. + */ +- (firebase::firestore::model::DocumentKeySet)remoteKeysForTarget: + (firebase::firestore::model::TargetId)targetId; + +@end + +namespace firebase { +namespace firestore { +namespace remote { + +class RemoteStore : public TargetMetadataProvider, public WatchStreamCallback { + public: + RemoteStore(FSTLocalStore* local_store, + Datastore* datastore, + util::AsyncQueue* worker_queue, + std::function online_state_handler); + + // TODO(varconst): remove the getters and setters + id sync_engine() { + return sync_engine_; + } + void set_sync_engine(id sync_engine) { + sync_engine_ = sync_engine; + } + + FSTLocalStore* local_store() { + return local_store_; + } + + OnlineStateTracker& online_state_tracker() { + return online_state_tracker_; + } + + void set_is_network_enabled(bool value) { + is_network_enabled_ = value; + } + + WatchStream& watch_stream() { + return *watch_stream_; + } + + /** Listens to the target identified by the given `FSTQueryData`. */ + void Listen(FSTQueryData* query_data); + + /** Stops listening to the target with the given target ID. */ + void StopListening(model::TargetId target_id); + + model::DocumentKeySet GetRemoteKeysForTarget( + model::TargetId target_id) const override; + FSTQueryData* GetQueryDataForTarget(model::TargetId target_id) const override; + + void OnWatchStreamOpen() override; + void OnWatchStreamChange( + const WatchChange& change, + const model::SnapshotVersion& snapshot_version) override; + void OnWatchStreamClose(const util::Status& status) override; + + // TODO(varconst): make the following methods private. + + bool CanUseNetwork() const; + + void StartWatchStream(); + + /** + * Returns true if the network is enabled, the watch stream has not yet been + * started and there are active watch targets. + */ + bool ShouldStartWatchStream() const; + + void CleanUpWatchStreamState(); + + private: + void SendWatchRequest(FSTQueryData* query_data); + void SendUnwatchRequest(model::TargetId target_id); + + /** + * Takes a batch of changes from the `Datastore`, repackages them as a + * `RemoteEvent`, and passes that on to the `SyncEngine`. + */ + void RaiseWatchSnapshot(const model::SnapshotVersion& snapshot_version); + + /** Process a target error and passes the error along to `SyncEngine`. */ + void ProcessTargetError(const WatchTargetChange& change); + + id sync_engine_ = nil; + + /** + * The local store, used to fill the write pipeline with outbound mutations + * and resolve existence filter mismatches. Immutable after initialization. + */ + FSTLocalStore* local_store_ = nil; + + /** + * A mapping of watched targets that the client cares about tracking and the + * user has explicitly called a 'listen' for this target. + * + * These targets may or may not have been sent to or acknowledged by the + * server. On re-establishing the listen stream, these targets should be sent + * to the server. The targets removed with unlistens are removed eagerly + * without waiting for confirmation from the listen stream. + */ + std::unordered_map listen_targets_; + + OnlineStateTracker online_state_tracker_; + + /** + * Set to true by `EnableNetwork` and false by `DisableNetworkInternal` and + * indicates the user-preferred network state. + */ + bool is_network_enabled_ = false; + + std::shared_ptr watch_stream_; + std::unique_ptr watch_change_aggregator_; +}; + +} // namespace remote +} // namespace firestore +} // namespace firebase + +NS_ASSUME_NONNULL_END + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_REMOTE_REMOTE_STORE_H_ diff --git a/Firestore/core/src/firebase/firestore/remote/remote_store.mm b/Firestore/core/src/firebase/firestore/remote/remote_store.mm new file mode 100644 index 00000000000..f40a5e7b31d --- /dev/null +++ b/Firestore/core/src/firebase/firestore/remote/remote_store.mm @@ -0,0 +1,296 @@ +/* + * Copyright 2019 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. + */ + +#include "Firestore/core/src/firebase/firestore/remote/remote_store.h" + +#include + +#import "Firestore/Source/Local/FSTLocalStore.h" +#import "Firestore/Source/Local/FSTQueryData.h" + +#include "Firestore/core/src/firebase/firestore/util/error_apple.h" +#include "Firestore/core/src/firebase/firestore/util/hard_assert.h" +#include "absl/memory/memory.h" + +using firebase::firestore::model::DocumentKeySet; +using firebase::firestore::model::OnlineState; +using firebase::firestore::model::SnapshotVersion; +using firebase::firestore::model::DocumentKeySet; +using firebase::firestore::model::TargetId; +using firebase::firestore::remote::Datastore; +using firebase::firestore::remote::WatchStream; +using firebase::firestore::remote::DocumentWatchChange; +using firebase::firestore::remote::ExistenceFilterWatchChange; +using firebase::firestore::remote::OnlineStateTracker; +using firebase::firestore::remote::RemoteEvent; +using firebase::firestore::remote::TargetChange; +using firebase::firestore::remote::WatchChange; +using firebase::firestore::remote::WatchChangeAggregator; +using firebase::firestore::remote::WatchTargetChange; +using firebase::firestore::remote::WatchTargetChangeState; +using firebase::firestore::util::AsyncQueue; +using firebase::firestore::util::Status; + +namespace firebase { +namespace firestore { +namespace remote { + +RemoteStore::RemoteStore( + FSTLocalStore* local_store, + Datastore* datastore, + AsyncQueue* worker_queue, + std::function online_state_handler) + : local_store_{local_store}, + online_state_tracker_{worker_queue, std::move(online_state_handler)} { + // Create streams (but note they're not started yet) + watch_stream_ = datastore->CreateWatchStream(this); +} + +void RemoteStore::Listen(FSTQueryData* query_data) { + TargetId targetKey = query_data.targetID; + HARD_ASSERT(listen_targets_.find(targetKey) == listen_targets_.end(), + "Listen called with duplicate target id: %s", targetKey); + + // Mark this as something the client is currently listening for. + listen_targets_[targetKey] = query_data; + + if (ShouldStartWatchStream()) { + // The listen will be sent in `OnWatchStreamOpen` + StartWatchStream(); + } else if (watch_stream_->IsOpen()) { + SendWatchRequest(query_data); + } +} + +void RemoteStore::StopListening(TargetId target_id) { + size_t num_erased = listen_targets_.erase(target_id); + HARD_ASSERT(num_erased == 1, + "StopListening: target not currently watched: %s", target_id); + + // The watch stream might not be started if we're in a disconnected state + if (watch_stream_->IsOpen()) { + SendUnwatchRequest(target_id); + } + if (listen_targets_.empty()) { + if (watch_stream_->IsOpen()) { + watch_stream_->MarkIdle(); + } else if (CanUseNetwork()) { + // Revert to `OnlineState::Unknown` if the watch stream is not open and we + // have no listeners, since without any listens to send we cannot confirm + // if the stream is healthy and upgrade to `OnlineState::Online`. + online_state_tracker_.UpdateState(OnlineState::Unknown); + } + } +} + +FSTQueryData* RemoteStore::GetQueryDataForTarget(TargetId target_id) const { + auto found = listen_targets_.find(target_id); + return found != listen_targets_.end() ? found->second : nil; +} + +DocumentKeySet RemoteStore::GetRemoteKeysForTarget(TargetId target_id) const { + return [sync_engine_ remoteKeysForTarget:target_id]; +} + +void RemoteStore::SendWatchRequest(FSTQueryData* query_data) { + // We need to increment the the expected number of pending responses we're due + // from watch so we wait for the ack to process any messages from this target. + watch_change_aggregator_->RecordPendingTargetRequest(query_data.targetID); + watch_stream_->WatchQuery(query_data); +} + +void RemoteStore::SendUnwatchRequest(TargetId target_id) { + // We need to increment the expected number of pending responses we're due + // from watch so we wait for the removal on the server before we process any + // messages from this target. + watch_change_aggregator_->RecordPendingTargetRequest(target_id); + watch_stream_->UnwatchTargetId(target_id); +} + +void RemoteStore::StartWatchStream() { + HARD_ASSERT(ShouldStartWatchStream(), + "StartWatchStream called when ShouldStartWatchStream is false."); + watch_change_aggregator_ = absl::make_unique(this); + watch_stream_->Start(); + + online_state_tracker_.HandleWatchStreamStart(); +} + +bool RemoteStore::ShouldStartWatchStream() const { + return CanUseNetwork() && !watch_stream_->IsStarted() && + !listen_targets_.empty(); +} + +bool RemoteStore::CanUseNetwork() const { + // PORTING NOTE: This method exists mostly because web also has to take into + // account primary vs. secondary state. + return is_network_enabled_; +} + +void RemoteStore::CleanUpWatchStreamState() { + watch_change_aggregator_.reset(); +} + +void RemoteStore::OnWatchStreamOpen() { + // Restore any existing watches. + for (const auto& kv : listen_targets_) { + SendWatchRequest(kv.second); + } +} + +void RemoteStore::OnWatchStreamClose(const Status& status) { + if (status.ok()) { + // Graceful stop (due to Stop() or idle timeout). Make sure that's + // desirable. + HARD_ASSERT(!ShouldStartWatchStream(), + "Watch stream was stopped gracefully while still needed."); + } + + CleanUpWatchStreamState(); + + // If we still need the watch stream, retry the connection. + if (ShouldStartWatchStream()) { + online_state_tracker_.HandleWatchStreamFailure(status); + + StartWatchStream(); + } else { + // We don't need to restart the watch stream because there are no active + // targets. The online state is set to unknown because there is no active + // attempt at establishing a connection. + online_state_tracker_.UpdateState(OnlineState::Unknown); + } +} + +void RemoteStore::OnWatchStreamChange(const WatchChange& change, + const SnapshotVersion& snapshot_version) { + // Mark the connection as Online because we got a message from the server. + online_state_tracker_.UpdateState(OnlineState::Online); + + if (change.type() == WatchChange::Type::TargetChange) { + const WatchTargetChange& watch_target_change = + static_cast(change); + if (watch_target_change.state() == WatchTargetChangeState::Removed && + !watch_target_change.cause().ok()) { + // There was an error on a target, don't wait for a consistent snapshot to + // raise events + return ProcessTargetError(watch_target_change); + } else { + watch_change_aggregator_->HandleTargetChange(watch_target_change); + } + } else if (change.type() == WatchChange::Type::Document) { + watch_change_aggregator_->HandleDocumentChange( + static_cast(change)); + } else { + HARD_ASSERT( + change.type() == WatchChange::Type::ExistenceFilter, + "Expected watchChange to be an instance of ExistenceFilterWatchChange"); + watch_change_aggregator_->HandleExistenceFilter( + static_cast(change)); + } + + if (snapshot_version != SnapshotVersion::None() && + snapshot_version >= [local_store_ lastRemoteSnapshotVersion]) { + // We have received a target change with a global snapshot if the snapshot + // version is not equal to `SnapshotVersion::None()`. + RaiseWatchSnapshot(snapshot_version); + } +} + +void RemoteStore::RaiseWatchSnapshot(const SnapshotVersion& snapshot_version) { + HARD_ASSERT(snapshot_version != SnapshotVersion::None(), + "Can't raise event for unknown SnapshotVersion"); + + RemoteEvent remote_event = + watch_change_aggregator_->CreateRemoteEvent(snapshot_version); + + // Update in-memory resume tokens. `FSTLocalStore` will update the persistent + // view of these when applying the completed `RemoteEvent`. + for (const auto& entry : remote_event.target_changes()) { + const TargetChange& target_change = entry.second; + NSData* resumeToken = target_change.resume_token(); + + if (resumeToken.length > 0) { + TargetId target_id = entry.first; + auto found = listen_targets_.find(target_id); + FSTQueryData* query_data = + found != listen_targets_.end() ? found->second : nil; + + // A watched target might have been removed already. + if (query_data) { + listen_targets_[target_id] = [query_data + queryDataByReplacingSnapshotVersion:snapshot_version + resumeToken:resumeToken + sequenceNumber:query_data.sequenceNumber]; + } + } + } + + // Re-establish listens for the targets that have been invalidated by + // existence filter mismatches. + for (TargetId target_id : remote_event.target_mismatches()) { + auto found = listen_targets_.find(target_id); + if (found == listen_targets_.end()) { + // A watched target might have been removed already. + continue; + } + FSTQueryData* query_data = found->second; + + // Clear the resume token for the query, since we're in a known mismatch + // state. + query_data = [[FSTQueryData alloc] initWithQuery:query_data.query + targetID:target_id + listenSequenceNumber:query_data.sequenceNumber + purpose:query_data.purpose]; + listen_targets_[target_id] = query_data; + + // Cause a hard reset by unwatching and rewatching immediately, but + // deliberately don't send a resume token so that we get a full update. + SendUnwatchRequest(target_id); + + // Mark the query we send as being on behalf of an existence filter + // mismatch, but don't actually retain that in listen_targets_. This ensures + // that we flag the first re-listen this way without impacting future + // listens of this target (that might happen e.g. on reconnect). + FSTQueryData* request_query_data = [[FSTQueryData alloc] + initWithQuery:query_data.query + targetID:target_id + listenSequenceNumber:query_data.sequenceNumber + purpose:FSTQueryPurposeExistenceFilterMismatch]; + SendWatchRequest(request_query_data); + } + + // Finally handle remote event + [sync_engine_ applyRemoteEvent:remote_event]; +} + +void RemoteStore::ProcessTargetError(const WatchTargetChange& change) { + HARD_ASSERT(!change.cause().ok(), "Handling target error without a cause"); + + // Ignore targets that have been removed already. + for (TargetId target_id : change.target_ids()) { + auto found = listen_targets_.find(target_id); + if (found != listen_targets_.end()) { + listen_targets_.erase(found); + watch_change_aggregator_->RemoveTarget(target_id); + [sync_engine_ rejectListenWithTargetID:target_id + error:util::MakeNSError(change.cause())]; + } + } +} + +} // namespace remote +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/src/firebase/firestore/remote/watch_stream.h b/Firestore/core/src/firebase/firestore/remote/watch_stream.h index 43b1ef32f7f..b6136cfbb4d 100644 --- a/Firestore/core/src/firebase/firestore/remote/watch_stream.h +++ b/Firestore/core/src/firebase/firestore/remote/watch_stream.h @@ -24,10 +24,12 @@ #include #include +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" #include "Firestore/core/src/firebase/firestore/model/types.h" #include "Firestore/core/src/firebase/firestore/remote/grpc_connection.h" #include "Firestore/core/src/firebase/firestore/remote/remote_objc_bridge.h" #include "Firestore/core/src/firebase/firestore/remote/stream.h" +#include "Firestore/core/src/firebase/firestore/remote/watch_change.h" #include "Firestore/core/src/firebase/firestore/util/async_queue.h" #include "Firestore/core/src/firebase/firestore/util/status.h" #include "absl/strings/string_view.h" @@ -41,12 +43,41 @@ namespace firebase { namespace firestore { namespace remote { +/** + * An interface defining the events that can be emitted by the `WatchStream`. + */ +class WatchStreamCallback { + public: + /** Called by the `WatchStream` when it is ready to accept outbound request + * messages. */ + virtual void OnWatchStreamOpen() = 0; + + /** + * Called by the `WatchStream` with changes and the snapshot versions + * included in in the `WatchChange` responses sent back by the server. + */ + virtual void OnWatchStreamChange( + const WatchChange& change, + const model::SnapshotVersion& snapshot_version) = 0; + + /** + * Called by the `WatchStream` when the underlying streaming RPC is + * interrupted for whatever reason, usually because of an error, but possibly + * due to an idle timeout. The status passed to this method may be ok, in + * which case the stream was closed without attributable fault. + * + * NOTE: This will not be called after `Stop` is called on the stream. See + * "Starting and Stopping" on `Stream` for details. + */ + virtual void OnWatchStreamClose(const util::Status& status) = 0; +}; + /** * A `Stream` that implements the StreamingWatch RPC. * - * Once the `WatchStream` has called the `streamDidOpen` method on the delegate, - * any number of `WatchQuery` and `UnwatchTargetId` calls can be sent to control - * what changes will be sent from the server for WatchChanges. + * Once the `WatchStream` has called the `OnWatchStreamOpen` method on the + * callback, any number of `WatchQuery` and `UnwatchTargetId` calls can be sent + * to control what changes will be sent from the server for WatchChanges. */ class WatchStream : public Stream { public: @@ -54,7 +85,7 @@ class WatchStream : public Stream { auth::CredentialsProvider* credentials_provider, FSTSerializerBeta* serializer, GrpcConnection* grpc_connection, - id delegate); + WatchStreamCallback* callback); /** * Registers interest in the results of the given query. If the query includes @@ -85,7 +116,7 @@ class WatchStream : public Stream { } bridge::WatchStreamSerializer serializer_bridge_; - bridge::WatchStreamDelegate delegate_bridge_; + WatchStreamCallback* callback_; }; } // namespace remote diff --git a/Firestore/core/src/firebase/firestore/remote/watch_stream.mm b/Firestore/core/src/firebase/firestore/remote/watch_stream.mm index 7a487d75664..0066809fae3 100644 --- a/Firestore/core/src/firebase/firestore/remote/watch_stream.mm +++ b/Firestore/core/src/firebase/firestore/remote/watch_stream.mm @@ -16,6 +16,7 @@ #include "Firestore/core/src/firebase/firestore/remote/watch_stream.h" +#include "Firestore/core/src/firebase/firestore/util/hard_assert.h" #include "Firestore/core/src/firebase/firestore/util/log.h" #include "Firestore/core/src/firebase/firestore/util/status.h" @@ -36,11 +37,11 @@ CredentialsProvider* credentials_provider, FSTSerializerBeta* serializer, GrpcConnection* grpc_connection, - id delegate) + WatchStreamCallback* callback) : Stream{async_queue, credentials_provider, grpc_connection, TimerId::ListenStreamConnectionBackoff, TimerId::ListenStreamIdle}, serializer_bridge_{serializer}, - delegate_bridge_{delegate} { + callback_{NOT_NULL(callback)} { } void WatchStream::WatchQuery(FSTQueryData* query) { @@ -73,7 +74,7 @@ } void WatchStream::NotifyStreamOpen() { - delegate_bridge_.NotifyDelegateOnOpen(); + callback_->OnWatchStreamOpen(); } Status WatchStream::NotifyStreamResponse(const grpc::ByteBuffer& message) { @@ -92,14 +93,14 @@ // A successful response means the stream is healthy. backoff_.Reset(); - delegate_bridge_.NotifyDelegateOnChange( + callback_->OnWatchStreamChange( *serializer_bridge_.ToWatchChange(response), serializer_bridge_.ToSnapshotVersion(response)); return Status::OK(); } void WatchStream::NotifyStreamClose(const Status& status) { - delegate_bridge_.NotifyDelegateOnClose(status); + callback_->OnWatchStreamClose(status); } } // namespace remote From 86051203ac8072d2046f6ad4e17b3b1dba9f4225 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Mon, 4 Feb 2019 06:57:42 -0800 Subject: [PATCH 17/27] Add IAM headless to CI (#2341) * Fix new Xcode 10.2 warning --- Firebase/InAppMessaging/Data/FIRIAMFetchResponseParser.h | 3 ++- .../Example/App/InAppMessaging-Example-iOS/AppDelegate.m | 1 - InAppMessaging/Example/Podfile | 5 ++--- scripts/build.sh | 7 +++++++ scripts/install_prereqs.sh | 1 + 5 files changed, 12 insertions(+), 5 deletions(-) diff --git a/Firebase/InAppMessaging/Data/FIRIAMFetchResponseParser.h b/Firebase/InAppMessaging/Data/FIRIAMFetchResponseParser.h index 3146a0570e1..7fea6e6f38c 100644 --- a/Firebase/InAppMessaging/Data/FIRIAMFetchResponseParser.h +++ b/Firebase/InAppMessaging/Data/FIRIAMFetchResponseParser.h @@ -32,7 +32,8 @@ NS_ASSUME_NONNULL_BEGIN // @param fetchWaitTime would be non nil if fetch wait time data is found in the api response. - (NSArray *)parseAPIResponseDictionary:(NSDictionary *)responseDict discardedMsgCount:(NSInteger *)discardCount - fetchWaitTimeInSeconds:(NSNumber **)fetchWaitTime; + fetchWaitTimeInSeconds: + (NSNumber *_Nullable *_Nonnull)fetchWaitTime; - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithTimeFetcher:(id)timeFetcher; diff --git a/InAppMessaging/Example/App/InAppMessaging-Example-iOS/AppDelegate.m b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/AppDelegate.m index 477f3000369..79ab41c8d2b 100644 --- a/InAppMessaging/Example/App/InAppMessaging-Example-iOS/AppDelegate.m +++ b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/AppDelegate.m @@ -53,7 +53,6 @@ - (BOOL)application:(UIApplication *)application sdkSetting.loggerSizeAfterReduce = 600; sdkSetting.appFGRenderMinIntervalInMinutes = 0.1; sdkSetting.loggerInVerboseMode = YES; - sdkSetting.conversionTrackingExpiresInSeconds = 180; sdkSetting.firebaseAutoDataCollectionEnabled = NO; sdkSetting.clearcutStrategy = diff --git a/InAppMessaging/Example/Podfile b/InAppMessaging/Example/Podfile index 13f55e31691..04a75ebcee4 100644 --- a/InAppMessaging/Example/Podfile +++ b/InAppMessaging/Example/Podfile @@ -1,9 +1,8 @@ - use_frameworks! # Uncomment the next two lines for pre-release testing on public repo -source 'https://github.com/Firebase/SpecsStaging.git' -source 'https://github.com/CocoaPods/Specs.git' +# source 'https://github.com/Firebase/SpecsStaging.git' +# source 'https://github.com/CocoaPods/Specs.git' pod 'FirebaseCore', :path => '../..' diff --git a/scripts/build.sh b/scripts/build.sh index b99c2e26bff..2d9aa46a7a8 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -233,6 +233,13 @@ case "$product-$method-$platform" in ;; InAppMessagingDisplay-xcodebuild-iOS) + RunXcodebuild \ + -workspace 'InAppMessaging/Example/InAppMessaging-Example-iOS.xcworkspace' \ + -scheme 'InAppMessaging_Example_iOS' \ + "${xcb_flags[@]}" \ + build \ + test + # Run UI tests on both iPad and iPhone simulators # TODO: Running two destinations from one xcodebuild command stopped working with Xcode 10. # Consider separating static library tests to a separate job. diff --git a/scripts/install_prereqs.sh b/scripts/install_prereqs.sh index a0dc1b96c2f..235cb751c1b 100755 --- a/scripts/install_prereqs.sh +++ b/scripts/install_prereqs.sh @@ -48,6 +48,7 @@ case "$PROJECT-$PLATFORM-$METHOD" in InAppMessagingDisplay-iOS-xcodebuild) gem install xcpretty bundle exec pod install --project-directory=InAppMessagingDisplay/Example --repo-update + bundle exec pod install --project-directory=InAppMessaging/Example --repo-update ;; Firestore-*-xcodebuild | Firestore-*-fuzz) From 6df99be07349f91c640a8cf28fbebee5c4e69d40 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Mon, 4 Feb 2019 08:23:10 -0800 Subject: [PATCH 18/27] Bump FirebaseAnalyticsInterop version (#2315) --- Example/DynamicLinks/Tests/FIRDLScionLoggingTest.m | 14 ++++++++++++++ .../Messaging/Tests/FIRMessagingAnalyticsTest.m | 14 ++++++++++++++ FirebaseAnalyticsInterop.podspec | 2 +- 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/Example/DynamicLinks/Tests/FIRDLScionLoggingTest.m b/Example/DynamicLinks/Tests/FIRDLScionLoggingTest.m index 47778c34708..25f53658e6a 100644 --- a/Example/DynamicLinks/Tests/FIRDLScionLoggingTest.m +++ b/Example/DynamicLinks/Tests/FIRDLScionLoggingTest.m @@ -74,6 +74,20 @@ - (void)setUserPropertyWithOrigin:(nonnull NSString *)origin name:(nonnull NSString *)name value:(nonnull id)value { } + +- (void)checkLastNotificationForOrigin:(nonnull NSString *)origin + queue:(nonnull dispatch_queue_t)queue + callback:(nonnull void (^)(NSString *_Nullable)) + currentLastNotificationProperty { +} + +- (void)registerAnalyticsListener:(nonnull id)listener + withOrigin:(nonnull NSString *)origin { +} + +- (void)unregisterAnalyticsListenerWithOrigin:(nonnull NSString *)origin { +} + @end @interface FIRDLScionLoggingTest : XCTestCase diff --git a/Example/Messaging/Tests/FIRMessagingAnalyticsTest.m b/Example/Messaging/Tests/FIRMessagingAnalyticsTest.m index c8f7e26b803..352fd078c17 100644 --- a/Example/Messaging/Tests/FIRMessagingAnalyticsTest.m +++ b/Example/Messaging/Tests/FIRMessagingAnalyticsTest.m @@ -97,6 +97,20 @@ - (NSInteger)maxUserProperties:(nonnull NSString *)origin { - (void)setConditionalUserProperty:(nonnull FIRAConditionalUserProperty *)conditionalUserProperty { } +- (void)checkLastNotificationForOrigin:(nonnull NSString *)origin + queue:(nonnull dispatch_queue_t)queue + callback:(nonnull void (^)(NSString *_Nullable)) + currentLastNotificationProperty { +} + +- (void)registerAnalyticsListener:(nonnull id)listener + withOrigin:(nonnull NSString *)origin { +} + +- (void)unregisterAnalyticsListenerWithOrigin:(nonnull NSString *)origin { +} + + @end @interface FIRMessagingAnalytics (ExposedForTest) diff --git a/FirebaseAnalyticsInterop.podspec b/FirebaseAnalyticsInterop.podspec index c2743634ed9..bea3f9de282 100644 --- a/FirebaseAnalyticsInterop.podspec +++ b/FirebaseAnalyticsInterop.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseAnalyticsInterop' - s.version = '1.1.0' + s.version = '1.2.0' s.summary = 'Interfaces that allow other Firebase SDKs to use Analytics functionality.' s.description = <<-DESC From c47123b96dab39ae699108246e96429888eb6530 Mon Sep 17 00:00:00 2001 From: Ryan Wilson Date: Tue, 5 Feb 2019 15:59:12 -0500 Subject: [PATCH 19/27] Update library name to `fire-fiam`. (#2352) This is to remove ambiguity from other `iam` acronyms. --- Firebase/InAppMessaging/FIRInAppMessaging.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Firebase/InAppMessaging/FIRInAppMessaging.m b/Firebase/InAppMessaging/FIRInAppMessaging.m index 8ae243cbde3..a0773e78d36 100644 --- a/Firebase/InAppMessaging/FIRInAppMessaging.m +++ b/Firebase/InAppMessaging/FIRInAppMessaging.m @@ -50,7 +50,7 @@ + (void)disableAutoBootstrapWithFIRApp { + (void)load { [FIRApp registerInternalLibrary:(Class)self - withName:@"fire-iam" + withName:@"fire-fiam" withVersion:[NSString stringWithUTF8String:STR(FIRInAppMessaging_LIB_VERSION)]]; } From a6fcd308c20fa3963d33a66b757a74e1e89c8244 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Tue, 5 Feb 2019 12:59:43 -0800 Subject: [PATCH 20/27] Start pod lib lint CI for IAM (#2347) --- .travis.yml | 3 +++ scripts/pod_lib_lint.sh | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index a573308a69c..e6721a9441a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -68,6 +68,7 @@ jobs: - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.sh FirebaseMessaging.podspec - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.sh FirebaseStorage.podspec - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.sh FirebaseFunctions.podspec + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.sh FirebaseInAppMessaging.podspec - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.sh FirebaseInAppMessagingDisplay.podspec - stage: test @@ -98,6 +99,7 @@ jobs: - travis_retry ./scripts/if_cron.sh ./scripts/pod_lib_lint.sh FirebaseMessaging.podspec --use-libraries --allow-warnings - travis_retry ./scripts/if_cron.sh ./scripts/pod_lib_lint.sh FirebaseStorage.podspec --use-libraries - travis_retry ./scripts/if_cron.sh ./scripts/pod_lib_lint.sh FirebaseFunctions.podspec --use-libraries + - travis_retry ./scripts/if_cron.sh ./scripts/pod_lib_lint.sh FirebaseInAppMessaging.podspec --use-libraries - travis_retry ./scripts/if_cron.sh ./scripts/pod_lib_lint.sh FirebaseInAppMessagingDisplay.podspec --use-libraries - stage: test @@ -186,6 +188,7 @@ jobs: - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.sh FirebaseMessaging.podspec - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.sh FirebaseStorage.podspec - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.sh FirebaseFunctions.podspec + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.sh FirebaseInAppMessaging.podspec - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.sh FirebaseInAppMessagingDisplay.podspec - stage: test diff --git a/scripts/pod_lib_lint.sh b/scripts/pod_lib_lint.sh index 8472a8f902a..f6d6f2e67b2 100755 --- a/scripts/pod_lib_lint.sh +++ b/scripts/pod_lib_lint.sh @@ -37,7 +37,7 @@ fi # or Core APIs change. GoogleUtilities.podspec and FirebaseCore.podspec should be # manually pushed to a temporary Specs repo. See # https://guides.cocoapods.org/making/private-cocoapods. -#ALT_SOURCES="--sources=https://github.com/Firebase/SpecsStaging.git,https://github.com/CocoaPods/Specs.git" +ALT_SOURCES="--sources=https://github.com/Firebase/SpecsStaging.git,https://github.com/CocoaPods/Specs.git" podspec="$1" if [[ $# -gt 1 ]]; then From 9be30386aa548eb8c8016700383cb2248b8f2d58 Mon Sep 17 00:00:00 2001 From: Gil Date: Tue, 5 Feb 2019 17:00:32 -0800 Subject: [PATCH 21/27] Update gRPC certificate bundles locations for Firebase 5.16 (#2353) --- Carthage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Carthage.md b/Carthage.md index 1ca28b5542b..1ee4f8da2e7 100644 --- a/Carthage.md +++ b/Carthage.md @@ -65,7 +65,7 @@ binary "https://dl.google.com/dl/firebase/ios/carthage/FirebaseStorageBinary.jso into the Xcode project and make sure they're added to the `Copy Bundle Resources` Build Phase : - For Firestore: - - ./Carthage/Build/iOS/FirebaseFirestore.framework/gRPCCertificates-Firestore.bundle + - ./Carthage/Build/iOS/FirebaseFirestore.framework/gRPCCertificates.bundle - For Invites: - ./Carthage/Build/iOS/FirebaseInvites.framework/GoogleSignIn.bundle - ./Carthage/Build/iOS/FirebaseInvites.framework/GPPACLPickerResources.bundle From a2e13d5b1cbe35b70d9feede43d972e0151e0a39 Mon Sep 17 00:00:00 2001 From: Chuan Ren Date: Wed, 6 Feb 2019 11:15:08 -0800 Subject: [PATCH 22/27] Resolve hard dependency of GameKit (#2355) --- .../GameCenter/FIRGameCenterAuthProvider.m | 17 ++++++++++++++++- Firebase/Auth/Source/FIRAuthErrorUtils.h | 6 ++++++ Firebase/Auth/Source/FIRAuthErrorUtils.m | 16 +++++++++++++++- Firebase/Auth/Source/FIRAuthInternalErrors.h | 5 +++++ Firebase/Auth/Source/Public/FIRAuthErrors.h | 4 ++++ 5 files changed, 46 insertions(+), 2 deletions(-) diff --git a/Firebase/Auth/Source/AuthProviders/GameCenter/FIRGameCenterAuthProvider.m b/Firebase/Auth/Source/AuthProviders/GameCenter/FIRGameCenterAuthProvider.m index 65f79a85965..f037b0cb6cb 100644 --- a/Firebase/Auth/Source/AuthProviders/GameCenter/FIRGameCenterAuthProvider.m +++ b/Firebase/Auth/Source/AuthProviders/GameCenter/FIRGameCenterAuthProvider.m @@ -31,7 +31,22 @@ - (instancetype)init { } + (void)getCredentialWithCompletion:(FIRGameCenterCredentialCallback)completion { - __weak GKLocalPlayer *localPlayer = [GKLocalPlayer localPlayer]; + /** + Linking GameKit.framework without using it on macOS results in App Store rejection. + Thus we don't link GameKit.framework to our SDK directly. `optionalLocalPlayer` is used for + checking whether the APP that consuming our SDK has linked GameKit.framework. If not, a + `GameKitNotLinkedError` will be raised. + **/ + GKLocalPlayer *optionalLocalPlayer = [[NSClassFromString(@"GKLocalPlayer") alloc] init]; + + if (!optionalLocalPlayer) { + if (completion) { + completion(nil, [FIRAuthErrorUtils gameKitNotLinkedError]); + } + return; + } + + __weak GKLocalPlayer *localPlayer = [[optionalLocalPlayer class] localPlayer]; if (!localPlayer.isAuthenticated) { if (completion) { completion(nil, [FIRAuthErrorUtils localPlayerNotAuthenticatedError]); diff --git a/Firebase/Auth/Source/FIRAuthErrorUtils.h b/Firebase/Auth/Source/FIRAuthErrorUtils.h index a8a937b7d6f..eba44370c46 100644 --- a/Firebase/Auth/Source/FIRAuthErrorUtils.h +++ b/Firebase/Auth/Source/FIRAuthErrorUtils.h @@ -451,6 +451,12 @@ NS_ASSUME_NONNULL_BEGIN */ + (NSError *)localPlayerNotAuthenticatedError; +/** @fn gameKitNotLinkedError + @brief Constructs an @c NSError with the @c FIRAuthErrorCodeGameKitNotLinked code. + @return The NSError instance associated with the given FIRAuthError. + */ ++ (NSError *)gameKitNotLinkedError; + /** @fn notificationNotForwardedError @brief Constructs an @c NSError with the @c FIRAuthErrorCodeNotificationNotForwarded code. @return The NSError instance associated with the given FIRAuthError. diff --git a/Firebase/Auth/Source/FIRAuthErrorUtils.m b/Firebase/Auth/Source/FIRAuthErrorUtils.m index 05bd867e053..635e6e5fbd6 100644 --- a/Firebase/Auth/Source/FIRAuthErrorUtils.m +++ b/Firebase/Auth/Source/FIRAuthErrorUtils.m @@ -312,11 +312,17 @@ @"The verification ID used to create the phone auth credential is invalid."; /** @var kFIRAuthErrorMessageLocalPlayerNotAuthenticated - @brief Message for @c FIRAuthErrorCodeLocalPlayerNotAuthenticated error code. + @brief Message for @c FIRAuthErrorCodeLocalPlayerNotAuthenticated error code. */ static NSString *const kFIRAuthErrorMessageLocalPlayerNotAuthenticated = @"The local player is not authenticated. Please log the local player in to Game Center."; +/** @var kFIRAuthErrorMessageGameKitNotLinked + @brief Message for @c kFIRAuthErrorMessageGameKitNotLinked error code. + */ +static NSString *const kFIRAuthErrorMessageGameKitNotLinked = + @"The GameKit framework is not linked. Please turn on the Game Center capability."; + /** @var kFIRAuthErrorMessageSessionExpired @brief Message for @c FIRAuthErrorCodeSessionExpired error code. */ @@ -556,6 +562,8 @@ return kFIRAuthErrorMessageMalformedJWT; case FIRAuthErrorCodeLocalPlayerNotAuthenticated: return kFIRAuthErrorMessageLocalPlayerNotAuthenticated; + case FIRAuthErrorCodeGameKitNotLinked: + return kFIRAuthErrorMessageGameKitNotLinked; } } @@ -683,6 +691,8 @@ return @"ERROR_MALFORMED_JWT"; case FIRAuthErrorCodeLocalPlayerNotAuthenticated: return @"ERROR_LOCAL_PLAYER_NOT_AUTHENTICATED"; + case FIRAuthErrorCodeGameKitNotLinked: + return @"ERROR_GAME_KIT_NOT_LINKED"; } } @@ -1000,6 +1010,10 @@ + (NSError *)localPlayerNotAuthenticatedError { return [self errorWithCode:FIRAuthInternalErrorCodeLocalPlayerNotAuthenticated]; } ++ (NSError *)gameKitNotLinkedError { + return [self errorWithCode:FIRAuthInternalErrorCodeGameKitNotLinked]; +} + + (NSError *)notificationNotForwardedError { return [self errorWithCode:FIRAuthInternalErrorCodeNotificationNotForwarded]; } diff --git a/Firebase/Auth/Source/FIRAuthInternalErrors.h b/Firebase/Auth/Source/FIRAuthInternalErrors.h index 71a8fd0a5c9..8e12c1a4eb2 100644 --- a/Firebase/Auth/Source/FIRAuthInternalErrors.h +++ b/Firebase/Auth/Source/FIRAuthInternalErrors.h @@ -375,6 +375,11 @@ typedef NS_ENUM(NSInteger, FIRAuthInternalErrorCode) { FIRAuthInternalErrorCodeLocalPlayerNotAuthenticated = FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeLocalPlayerNotAuthenticated, + /** Indicates that the Game Center local player was not authenticated. + */ + FIRAuthInternalErrorCodeGameKitNotLinked = + FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeGameKitNotLinked, + /** Indicates that a non-null user was expected as an argmument to the operation but a null user was provided. */ diff --git a/Firebase/Auth/Source/Public/FIRAuthErrors.h b/Firebase/Auth/Source/Public/FIRAuthErrors.h index 9d177b662d7..f25147beb10 100644 --- a/Firebase/Auth/Source/Public/FIRAuthErrors.h +++ b/Firebase/Auth/Source/Public/FIRAuthErrors.h @@ -313,6 +313,10 @@ typedef NS_ENUM(NSInteger, FIRAuthErrorCode) { */ FIRAuthErrorCodeInvalidDynamicLinkDomain = 17074, + /** Indicates that the GameKit framework is not linked prior to attempting Game Center signin. + */ + FIRAuthErrorCodeGameKitNotLinked = 17076, + /** Indicates an error occurred while attempting to access the keychain. */ FIRAuthErrorCodeKeychainError = 17995, From 9b3654b1561da17dbe6a256b3c373ac43c86dd07 Mon Sep 17 00:00:00 2001 From: Konstantin Varlamov Date: Wed, 6 Feb 2019 20:23:29 -0500 Subject: [PATCH 23/27] C++ migration: port write stream-related part of `FSTRemoteStore` (#2335) --- .../Example/Tests/Local/FSTLocalStoreTests.mm | 2 +- .../Tests/SpecTests/FSTMockDatastore.h | 5 +- .../Tests/SpecTests/FSTMockDatastore.mm | 28 +-- .../Example/Tests/SpecTests/FSTSpecTests.mm | 2 +- .../Tests/SpecTests/FSTSyncEngineTestDriver.h | 7 +- .../SpecTests/FSTSyncEngineTestDriver.mm | 5 +- Firestore/Source/Model/FSTMutationBatch.h | 5 +- Firestore/Source/Model/FSTMutationBatch.mm | 23 +- Firestore/Source/Remote/FSTRemoteStore.mm | 206 +--------------- Firestore/Source/Remote/FSTStream.h | 60 ----- .../src/firebase/firestore/remote/datastore.h | 3 +- .../firebase/firestore/remote/datastore.mm | 4 +- .../firestore/remote/remote_objc_bridge.h | 20 +- .../firestore/remote/remote_objc_bridge.mm | 31 +-- .../firebase/firestore/remote/remote_store.h | 75 +++++- .../firebase/firestore/remote/remote_store.mm | 219 ++++++++++++++++-- .../firebase/firestore/remote/write_stream.h | 42 +++- .../firebase/firestore/remote/write_stream.mm | 12 +- 18 files changed, 377 insertions(+), 372 deletions(-) delete mode 100644 Firestore/Source/Remote/FSTStream.h diff --git a/Firestore/Example/Tests/Local/FSTLocalStoreTests.mm b/Firestore/Example/Tests/Local/FSTLocalStoreTests.mm index 22172c8f959..026db12ad05 100644 --- a/Firestore/Example/Tests/Local/FSTLocalStoreTests.mm +++ b/Firestore/Example/Tests/Local/FSTLocalStoreTests.mm @@ -153,7 +153,7 @@ - (void)acknowledgeMutationWithVersion:(FSTTestSnapshotVersion)documentVersion { transformResults:nil]; FSTMutationBatchResult *result = [FSTMutationBatchResult resultWithBatch:batch commitVersion:version - mutationResults:@[ mutationResult ] + mutationResults:{mutationResult} streamToken:nil]; _lastChanges = [self.localStore acknowledgeBatchWithResult:result]; } diff --git a/Firestore/Example/Tests/SpecTests/FSTMockDatastore.h b/Firestore/Example/Tests/SpecTests/FSTMockDatastore.h index 59afab20fa1..c9555cd9455 100644 --- a/Firestore/Example/Tests/SpecTests/FSTMockDatastore.h +++ b/Firestore/Example/Tests/SpecTests/FSTMockDatastore.h @@ -18,6 +18,7 @@ #include #include +#include #include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" #include "Firestore/core/src/firebase/firestore/model/types.h" @@ -40,7 +41,7 @@ class MockDatastore : public Datastore { auth::CredentialsProvider* credentials); std::shared_ptr CreateWatchStream(WatchStreamCallback* callback) override; - std::shared_ptr CreateWriteStream(id delegate) override; + std::shared_ptr CreateWriteStream(WriteStreamCallback* callback) override; /** * A count of the total number of requests sent to the watch stream since the beginning of the @@ -82,7 +83,7 @@ class MockDatastore : public Datastore { int WritesSent() const; /** Injects a write ack as though it had come from the backend in response to a write. */ - void AckWrite(const model::SnapshotVersion& version, NSArray* results); + void AckWrite(const model::SnapshotVersion& version, std::vector results); /** Injects a stream failure as though it had come from the backend. */ void FailWrite(const util::Status& error); diff --git a/Firestore/Example/Tests/SpecTests/FSTMockDatastore.mm b/Firestore/Example/Tests/SpecTests/FSTMockDatastore.mm index 3d7214c246e..49bf0b71375 100644 --- a/Firestore/Example/Tests/SpecTests/FSTMockDatastore.mm +++ b/Firestore/Example/Tests/SpecTests/FSTMockDatastore.mm @@ -25,7 +25,6 @@ #import "Firestore/Source/Local/FSTQueryData.h" #import "Firestore/Source/Model/FSTMutation.h" #import "Firestore/Source/Remote/FSTSerializerBeta.h" -#import "Firestore/Source/Remote/FSTStream.h" #include "Firestore/core/src/firebase/firestore/auth/credentials_provider.h" #include "Firestore/core/src/firebase/firestore/auth/empty_credentials_provider.h" @@ -161,18 +160,18 @@ void WriteWatchChange(const WatchChange& change, SnapshotVersion snap) { CredentialsProvider* credentials_provider, FSTSerializerBeta* serializer, GrpcConnection* grpc_connection, - id delegate, + WriteStreamCallback* callback, MockDatastore* datastore) - : WriteStream{worker_queue, credentials_provider, serializer, grpc_connection, delegate}, + : WriteStream{worker_queue, credentials_provider, serializer, grpc_connection, callback}, datastore_{datastore}, - delegate_{delegate} { + callback_{callback} { } void Start() override { HARD_ASSERT(!open_, "Trying to start already started write stream"); open_ = true; sent_mutations_ = {}; - [delegate_ writeStreamDidOpen]; + callback_->OnWriteStreamOpen(); } void Stop() override { @@ -194,7 +193,7 @@ bool IsOpen() const override { void WriteHandshake() override { datastore_->IncrementWriteStreamRequests(); SetHandshakeComplete(); - [delegate_ writeStreamDidCompleteHandshake]; + callback_->OnWriteStreamHandshakeComplete(); } void WriteMutations(NSArray* mutations) override { @@ -203,14 +202,14 @@ void WriteMutations(NSArray* mutations) override { } /** Injects a write ack as though it had come from the backend in response to a write. */ - void AckWrite(const SnapshotVersion& commitVersion, NSArray* results) { - [delegate_ writeStreamDidReceiveResponseWithVersion:commitVersion mutationResults:results]; + void AckWrite(const SnapshotVersion& commitVersion, std::vector results) { + callback_->OnWriteStreamMutationResult(commitVersion, std::move(results)); } /** Injects a failed write response as though it had come from the backend. */ void FailStream(const Status& error) { open_ = false; - [delegate_ writeStreamWasInterruptedWithError:error]; + callback_->OnWriteStreamClose(error); } /** @@ -236,7 +235,7 @@ int sent_mutations_count() const { bool open_ = false; std::queue*> sent_mutations_; MockDatastore* datastore_ = nullptr; - id delegate_ = nullptr; + WriteStreamCallback* callback_ = nullptr; }; MockDatastore::MockDatastore(const core::DatabaseInfo& database_info, @@ -257,11 +256,11 @@ int sent_mutations_count() const { return watch_stream_; } -std::shared_ptr MockDatastore::CreateWriteStream(id delegate) { +std::shared_ptr MockDatastore::CreateWriteStream(WriteStreamCallback* callback) { write_stream_ = std::make_shared( worker_queue_, credentials_, [[FSTSerializerBeta alloc] initWithDatabaseID:&database_info_->database_id()], - grpc_connection(), delegate, this); + grpc_connection(), callback, this); return write_stream_; } @@ -290,8 +289,9 @@ int sent_mutations_count() const { return write_stream_->sent_mutations_count(); } -void MockDatastore::AckWrite(const SnapshotVersion& version, NSArray* results) { - write_stream_->AckWrite(version, results); +void MockDatastore::AckWrite(const SnapshotVersion& version, + std::vector results) { + write_stream_->AckWrite(version, std::move(results)); } void MockDatastore::FailWrite(const Status& error) { diff --git a/Firestore/Example/Tests/SpecTests/FSTSpecTests.mm b/Firestore/Example/Tests/SpecTests/FSTSpecTests.mm index 266cda72610..24f0fd4a7ee 100644 --- a/Firestore/Example/Tests/SpecTests/FSTSpecTests.mm +++ b/Firestore/Example/Tests/SpecTests/FSTSpecTests.mm @@ -363,7 +363,7 @@ - (void)doWriteAck:(NSDictionary *)spec { FSTMutationResult *mutationResult = [[FSTMutationResult alloc] initWithVersion:version transformResults:nil]; - [self.driver receiveWriteAckWithVersion:version mutationResults:@[ mutationResult ]]; + [self.driver receiveWriteAckWithVersion:version mutationResults:{mutationResult}]; } - (void)doFailWrite:(NSDictionary *)spec { diff --git a/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h index b3b2a1d2a5c..780291a7888 100644 --- a/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h +++ b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h @@ -18,6 +18,7 @@ #include #include +#include #import "Firestore/Source/Remote/FSTRemoteStore.h" @@ -195,9 +196,9 @@ typedef std::unordered_map *)mutationResults; +- (FSTOutstandingWrite *) + receiveWriteAckWithVersion:(const firebase::firestore::model::SnapshotVersion &)commitVersion + mutationResults:(std::vector)mutationResults; /** * A count of the mutations written to the write stream by the FSTSyncEngine, but not yet diff --git a/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.mm b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.mm index 299f15703db..4f196b15033 100644 --- a/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.mm +++ b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.mm @@ -273,12 +273,13 @@ - (void)changeUser:(const User &)user { - (FSTOutstandingWrite *)receiveWriteAckWithVersion:(const SnapshotVersion &)commitVersion mutationResults: - (NSArray *)mutationResults { + (std::vector)mutationResults { FSTOutstandingWrite *write = [self currentOutstandingWrites].firstObject; [[self currentOutstandingWrites] removeObjectAtIndex:0]; [self validateNextWriteSent:write.write]; - _workerQueue->EnqueueBlocking([&] { _datastore->AckWrite(commitVersion, mutationResults); }); + _workerQueue->EnqueueBlocking( + [&] { _datastore->AckWrite(commitVersion, std::move(mutationResults)); }); return write; } diff --git a/Firestore/Source/Model/FSTMutationBatch.h b/Firestore/Source/Model/FSTMutationBatch.h index 0aace1d323f..ac8212db47a 100644 --- a/Firestore/Source/Model/FSTMutationBatch.h +++ b/Firestore/Source/Model/FSTMutationBatch.h @@ -17,6 +17,7 @@ #import #include +#include #include "Firestore/core/src/firebase/firestore/model/document_key.h" #include "Firestore/core/src/firebase/firestore/model/document_key_set.h" @@ -102,13 +103,13 @@ NS_ASSUME_NONNULL_BEGIN */ + (instancetype)resultWithBatch:(FSTMutationBatch *)batch commitVersion:(firebase::firestore::model::SnapshotVersion)commitVersion - mutationResults:(NSArray *)mutationResults + mutationResults:(std::vector)mutationResults streamToken:(nullable NSData *)streamToken; - (const firebase::firestore::model::SnapshotVersion &)commitVersion; +- (const std::vector &)mutationResults; @property(nonatomic, strong, readonly) FSTMutationBatch *batch; -@property(nonatomic, strong, readonly) NSArray *mutationResults; @property(nonatomic, strong, readonly, nullable) NSData *streamToken; - (const firebase::firestore::model::DocumentVersionMap &)docVersions; diff --git a/Firestore/Source/Model/FSTMutationBatch.mm b/Firestore/Source/Model/FSTMutationBatch.mm index aad425f3f62..c76c5e3fd2f 100644 --- a/Firestore/Source/Model/FSTMutationBatch.mm +++ b/Firestore/Source/Model/FSTMutationBatch.mm @@ -82,9 +82,9 @@ - (FSTMaybeDocument *_Nullable)applyToRemoteDocument:(FSTMaybeDocument *_Nullabl "applyTo: key %s doesn't match maybeDoc key %s", documentKey.ToString(), maybeDoc.key.ToString()); - HARD_ASSERT(mutationBatchResult.mutationResults.count == self.mutations.count, + HARD_ASSERT(mutationBatchResult.mutationResults.size() == self.mutations.count, "Mismatch between mutations length (%s) and results length (%s)", - self.mutations.count, mutationBatchResult.mutationResults.count); + self.mutations.count, mutationBatchResult.mutationResults.size()); for (NSUInteger i = 0; i < self.mutations.count; i++) { FSTMutation *mutation = self.mutations[i]; @@ -130,25 +130,26 @@ - (DocumentKeySet)keys { @interface FSTMutationBatchResult () - (instancetype)initWithBatch:(FSTMutationBatch *)batch commitVersion:(SnapshotVersion)commitVersion - mutationResults:(NSArray *)mutationResults + mutationResults:(std::vector)mutationResults streamToken:(nullable NSData *)streamToken docVersions:(DocumentVersionMap)docVersions NS_DESIGNATED_INITIALIZER; @end @implementation FSTMutationBatchResult { SnapshotVersion _commitVersion; + std::vector _mutationResults; DocumentVersionMap _docVersions; } - (instancetype)initWithBatch:(FSTMutationBatch *)batch commitVersion:(SnapshotVersion)commitVersion - mutationResults:(NSArray *)mutationResults + mutationResults:(std::vector)mutationResults streamToken:(nullable NSData *)streamToken docVersions:(DocumentVersionMap)docVersions { if (self = [super init]) { _batch = batch; _commitVersion = std::move(commitVersion); - _mutationResults = mutationResults; + _mutationResults = std::move(mutationResults); _streamToken = streamToken; _docVersions = std::move(docVersions); } @@ -159,17 +160,21 @@ - (instancetype)initWithBatch:(FSTMutationBatch *)batch return _commitVersion; } +- (const std::vector &)mutationResults { + return _mutationResults; +} + - (const DocumentVersionMap &)docVersions { return _docVersions; } + (instancetype)resultWithBatch:(FSTMutationBatch *)batch commitVersion:(SnapshotVersion)commitVersion - mutationResults:(NSArray *)mutationResults + mutationResults:(std::vector)mutationResults streamToken:(nullable NSData *)streamToken { - HARD_ASSERT(batch.mutations.count == mutationResults.count, + HARD_ASSERT(batch.mutations.count == mutationResults.size(), "Mutations sent %s must equal results received %s", batch.mutations.count, - mutationResults.count); + mutationResults.size()); DocumentVersionMap docVersions; NSArray *mutations = batch.mutations; @@ -186,7 +191,7 @@ + (instancetype)resultWithBatch:(FSTMutationBatch *)batch return [[FSTMutationBatchResult alloc] initWithBatch:batch commitVersion:std::move(commitVersion) - mutationResults:mutationResults + mutationResults:std::move(mutationResults) streamToken:streamToken docVersions:std::move(docVersions)]; } diff --git a/Firestore/Source/Remote/FSTRemoteStore.mm b/Firestore/Source/Remote/FSTRemoteStore.mm index 7b4f215c911..9d86bd9ee29 100644 --- a/Firestore/Source/Remote/FSTRemoteStore.mm +++ b/Firestore/Source/Remote/FSTRemoteStore.mm @@ -28,7 +28,6 @@ #import "Firestore/Source/Model/FSTDocument.h" #import "Firestore/Source/Model/FSTMutation.h" #import "Firestore/Source/Model/FSTMutationBatch.h" -#import "Firestore/Source/Remote/FSTStream.h" #include "Firestore/core/src/firebase/firestore/auth/user.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" @@ -73,41 +72,13 @@ NS_ASSUME_NONNULL_BEGIN -/** - * The maximum number of pending writes to allow. - * TODO(bjornick): Negotiate this value with the backend. - */ -static const int kMaxPendingWrites = 10; - #pragma mark - FSTRemoteStore -@interface FSTRemoteStore () - -#pragma mark Watch Stream - -/** - * A list of up to kMaxPendingWrites writes that we have fetched from the LocalStore via - * fillWritePipeline and have or will send to the write stream. - * - * Whenever writePipeline is not empty, the RemoteStore will attempt to start or restart the write - * stream. When the stream is established, the writes in the pipeline will be sent in order. - * - * Writes remain in writePipeline until they are acknowledged by the backend and thus will - * automatically be re-sent if the stream is interrupted / restarted before they're acknowledged. - * - * Write responses from the backend are linked to their originating request purely based on - * order, and so we can just remove writes from the front of the writePipeline as we receive - * responses. - */ -@property(nonatomic, strong, readonly) NSMutableArray *writePipeline; -@end - @implementation FSTRemoteStore { /** The client-side proxy for interacting with the backend. */ std::shared_ptr _datastore; std::unique_ptr _remoteStore; - std::shared_ptr _writeStream; } - (instancetype)initWithLocalStore:(FSTLocalStore *)localStore @@ -116,15 +87,10 @@ - (instancetype)initWithLocalStore:(FSTLocalStore *)localStore onlineStateHandler:(std::function)onlineStateHandler { if (self = [super init]) { _datastore = std::move(datastore); - - _writePipeline = [NSMutableArray array]; - _datastore->Start(); _remoteStore = absl::make_unique(localStore, _datastore.get(), queue, std::move(onlineStateHandler)); - _writeStream = _datastore->CreateWriteStream(self); - _remoteStore->set_is_network_enabled(false); } return self; @@ -146,7 +112,7 @@ - (void)enableNetwork { if (_remoteStore->CanUseNetwork()) { // Load any saved stream token from persistent storage - _writeStream->SetLastStreamToken([_remoteStore->local_store() lastStreamToken]); + _remoteStore->write_stream().SetLastStreamToken([_remoteStore->local_store() lastStreamToken]); if (_remoteStore->ShouldStartWatchStream()) { _remoteStore->StartWatchStream(); @@ -170,12 +136,12 @@ - (void)disableNetwork { /** Disables the network, setting the OnlineState to the specified targetOnlineState. */ - (void)disableNetworkInternal { _remoteStore->watch_stream().Stop(); - _writeStream->Stop(); + _remoteStore->write_stream().Stop(); - if (self.writePipeline.count > 0) { + if (!_remoteStore->write_pipeline().empty()) { LOG_DEBUG("Stopping write stream with %s pending writes", - (unsigned long)self.writePipeline.count); - [self.writePipeline removeAllObjects]; + _remoteStore->write_pipeline().size()); + _remoteStore->write_pipeline().clear(); } _remoteStore->CleanUpWatchStreamState(); @@ -218,21 +184,6 @@ - (void)stopListeningToTargetID:(TargetId)targetID { #pragma mark Write Stream -/** - * Returns YES if the network is enabled, the write stream has not yet been started and there are - * pending writes. - */ -- (BOOL)shouldStartWriteStream { - return _remoteStore->CanUseNetwork() && !_writeStream->IsStarted() && - self.writePipeline.count > 0; -} - -- (void)startWriteStream { - HARD_ASSERT([self shouldStartWriteStream], - "startWriteStream: called when shouldStartWriteStream: is false."); - _writeStream->Start(); -} - /** * Attempts to fill our write pipeline with writes from the LocalStore. * @@ -242,154 +193,11 @@ - (void)startWriteStream { * Starts the write stream if necessary. */ - (void)fillWritePipeline { - BatchId lastBatchIDRetrieved = - self.writePipeline.count == 0 ? kBatchIdUnknown : self.writePipeline.lastObject.batchID; - while ([self canAddToWritePipeline]) { - FSTMutationBatch *batch = - [_remoteStore->local_store() nextMutationBatchAfterBatchID:lastBatchIDRetrieved]; - if (!batch) { - if (self.writePipeline.count == 0) { - _writeStream->MarkIdle(); - } - break; - } - [self addBatchToWritePipeline:batch]; - lastBatchIDRetrieved = batch.batchID; - } - - if ([self shouldStartWriteStream]) { - [self startWriteStream]; - } -} - -/** - * Returns YES if we can add to the write pipeline (i.e. it is not full and the network is enabled). - */ -- (BOOL)canAddToWritePipeline { - return _remoteStore->CanUseNetwork() && self.writePipeline.count < kMaxPendingWrites; + _remoteStore->FillWritePipeline(); } -/** - * Queues additional writes to be sent to the write stream, sending them immediately if the write - * stream is established. - */ - (void)addBatchToWritePipeline:(FSTMutationBatch *)batch { - HARD_ASSERT([self canAddToWritePipeline], "addBatchToWritePipeline called when pipeline is full"); - - [self.writePipeline addObject:batch]; - - if (_writeStream->IsOpen() && _writeStream->handshake_complete()) { - _writeStream->WriteMutations(batch.mutations); - } -} - -- (void)writeStreamDidOpen { - _writeStream->WriteHandshake(); -} - -/** - * Handles a successful handshake response from the server, which is our cue to send any pending - * writes. - */ -- (void)writeStreamDidCompleteHandshake { - // Record the stream token. - [_remoteStore->local_store() setLastStreamToken:_writeStream->GetLastStreamToken()]; - - // Send the write pipeline now that the stream is established. - for (FSTMutationBatch *write in self.writePipeline) { - _writeStream->WriteMutations(write.mutations); - } -} - -/** Handles a successful StreamingWriteResponse from the server that contains a mutation result. */ -- (void)writeStreamDidReceiveResponseWithVersion:(const SnapshotVersion &)commitVersion - mutationResults:(NSArray *)results { - // This is a response to a write containing mutations and should be correlated to the first - // write in our write pipeline. - NSMutableArray *writePipeline = self.writePipeline; - FSTMutationBatch *batch = writePipeline[0]; - [writePipeline removeObjectAtIndex:0]; - - FSTMutationBatchResult *batchResult = - [FSTMutationBatchResult resultWithBatch:batch - commitVersion:commitVersion - mutationResults:results - streamToken:_writeStream->GetLastStreamToken()]; - [_remoteStore->sync_engine() applySuccessfulWriteWithResult:batchResult]; - - // It's possible that with the completion of this mutation another slot has freed up. - [self fillWritePipeline]; -} - -/** - * Handles the closing of the StreamingWrite RPC, either because of an error or because the RPC - * has been terminated by the client or the server. - */ -- (void)writeStreamWasInterruptedWithError:(const Status &)error { - if (error.ok()) { - // Graceful stop (due to Stop() or idle timeout). Make sure that's desirable. - HARD_ASSERT(![self shouldStartWriteStream], - "Write stream was stopped gracefully while still needed."); - } - - // If the write stream closed due to an error, invoke the error callbacks if there are pending - // writes. - if (!error.ok() && self.writePipeline.count > 0) { - if (_writeStream->handshake_complete()) { - // This error affects the actual writes. - [self handleWriteError:error]; - } else { - // If there was an error before the handshake finished, it's possible that the server is - // unable to process the stream token we're sending. (Perhaps it's too old?) - [self handleHandshakeError:error]; - } - } - - // The write stream might have been started by refilling the write pipeline for failed writes - if ([self shouldStartWriteStream]) { - [self startWriteStream]; - } -} - -- (void)handleHandshakeError:(const Status &)error { - HARD_ASSERT(!error.ok(), "Handling write error with status OK."); - // Reset the token if it's a permanent error, signaling the write stream is - // no longer valid. Note that the handshake does not count as a write: see - // comments on `Datastore::IsPermanentWriteError` for details. - if (Datastore::IsPermanentError(error)) { - NSString *token = [_writeStream->GetLastStreamToken() base64EncodedStringWithOptions:0]; - LOG_DEBUG("FSTRemoteStore %s error before completed handshake; resetting stream token %s: " - "error code: '%s', details: '%s'", - (__bridge void *)self, token, error.code(), error.error_message()); - _writeStream->SetLastStreamToken(nil); - [_remoteStore->local_store() setLastStreamToken:nil]; - } else { - // Some other error, don't reset stream token. Our stream logic will just retry with exponential - // backoff. - } -} - -- (void)handleWriteError:(const Status &)error { - HARD_ASSERT(!error.ok(), "Handling write error with status OK."); - // Only handle permanent errors here. If it's transient, just let the retry logic kick in. - if (!Datastore::IsPermanentWriteError(error)) { - return; - } - - // If this was a permanent error, the request itself was the problem so it's not going to - // succeed if we resend it. - FSTMutationBatch *batch = self.writePipeline[0]; - [self.writePipeline removeObjectAtIndex:0]; - - // In this case it's also unlikely that the server itself is melting down--this was just a - // bad request so inhibit backoff on the next restart. - _writeStream->InhibitBackoff(); - - [_remoteStore->sync_engine() rejectFailedWriteWithBatchID:batch.batchID - error:util::MakeNSError(error)]; - - // It's possible that with the completion of this mutation another slot has freed up. - [self fillWritePipeline]; + _remoteStore->AddToWritePipeline(batch); } - (FSTTransaction *)transaction { diff --git a/Firestore/Source/Remote/FSTStream.h b/Firestore/Source/Remote/FSTStream.h deleted file mode 100644 index 211a82154f5..00000000000 --- a/Firestore/Source/Remote/FSTStream.h +++ /dev/null @@ -1,60 +0,0 @@ -/* - * 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 - -#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" -#include "Firestore/core/src/firebase/firestore/remote/watch_change.h" -#include "Firestore/core/src/firebase/firestore/util/status.h" - -@class FSTMutationResult; - -NS_ASSUME_NONNULL_BEGIN - -#pragma mark - FSTWriteStreamDelegate - -@protocol FSTWriteStreamDelegate - -/** Called by the FSTWriteStream when it is ready to accept outbound request messages. */ -- (void)writeStreamDidOpen; - -/** - * Called by the FSTWriteStream upon a successful handshake response from the server, which is the - * receiver's cue to send any pending writes. - */ -- (void)writeStreamDidCompleteHandshake; - -/** - * Called by the FSTWriteStream upon receiving a StreamingWriteResponse from the server that - * contains mutation results. - */ -- (void)writeStreamDidReceiveResponseWithVersion: - (const firebase::firestore::model::SnapshotVersion &)commitVersion - mutationResults:(NSArray *)results; - -/** - * Called when the FSTWriteStream's underlying RPC is interrupted for whatever reason, usually - * because of an error, but possibly due to an idle timeout. The error passed to this method may be - * nil, in which case the stream was closed without attributable fault. - * - * NOTE: This will not be called after `stop` is called on the stream. See "Starting and Stopping" - * on FSTStream for details. - */ -- (void)writeStreamWasInterruptedWithError:(const firebase::firestore::util::Status &)error; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/core/src/firebase/firestore/remote/datastore.h b/Firestore/core/src/firebase/firestore/remote/datastore.h index 9e2268ea571..1c9c592977a 100644 --- a/Firestore/core/src/firebase/firestore/remote/datastore.h +++ b/Firestore/core/src/firebase/firestore/remote/datastore.h @@ -45,7 +45,6 @@ #include "grpcpp/support/status.h" #import "Firestore/Source/Core/FSTTypes.h" -#import "Firestore/Source/Remote/FSTStream.h" namespace firebase { namespace firestore { @@ -91,7 +90,7 @@ class Datastore : public std::enable_shared_from_this { * shared channel. */ virtual std::shared_ptr CreateWriteStream( - id delegate); + WriteStreamCallback* callback); void CommitMutations(NSArray* mutations, FSTVoidErrorBlock completion); diff --git a/Firestore/core/src/firebase/firestore/remote/datastore.mm b/Firestore/core/src/firebase/firestore/remote/datastore.mm index 1bf2376d8f8..d7f955abed8 100644 --- a/Firestore/core/src/firebase/firestore/remote/datastore.mm +++ b/Firestore/core/src/firebase/firestore/remote/datastore.mm @@ -158,10 +158,10 @@ void LogGrpcCallFinished(absl::string_view rpc_name, } std::shared_ptr Datastore::CreateWriteStream( - id delegate) { + WriteStreamCallback* callback) { return std::make_shared(worker_queue_, credentials_, serializer_bridge_.GetSerializer(), - &grpc_connection_, delegate); + &grpc_connection_, callback); } void Datastore::CommitMutations(NSArray* mutations, diff --git a/Firestore/core/src/firebase/firestore/remote/remote_objc_bridge.h b/Firestore/core/src/firebase/firestore/remote/remote_objc_bridge.h index e5cefba467a..63f23e03781 100644 --- a/Firestore/core/src/firebase/firestore/remote/remote_objc_bridge.h +++ b/Firestore/core/src/firebase/firestore/remote/remote_objc_bridge.h @@ -39,7 +39,6 @@ #import "Firestore/Source/Local/FSTQueryData.h" #import "Firestore/Source/Model/FSTMutation.h" #import "Firestore/Source/Remote/FSTSerializerBeta.h" -#import "Firestore/Source/Remote/FSTStream.h" namespace firebase { namespace firestore { @@ -125,7 +124,7 @@ class WriteStreamSerializer { GCFSWriteResponse* ParseResponse(const grpc::ByteBuffer& message, util::Status* out_status) const; model::SnapshotVersion ToCommitVersion(GCFSWriteResponse* proto) const; - NSArray* ToMutationResults( + std::vector ToMutationResults( GCFSWriteResponse* proto) const; /** Creates a pretty-printed description of the proto for debugging. */ @@ -172,23 +171,6 @@ class DatastoreSerializer { FSTSerializerBeta* serializer_; }; -/** A C++ bridge that invokes methods on an `FSTWriteStreamDelegate`. */ -class WriteStreamDelegate { - public: - explicit WriteStreamDelegate(id delegate) - : delegate_{delegate} { - } - - void NotifyDelegateOnOpen(); - void NotifyDelegateOnHandshakeComplete(); - void NotifyDelegateOnCommit(const model::SnapshotVersion& commit_version, - NSArray* results); - void NotifyDelegateOnClose(const util::Status& status); - - private: - __weak id delegate_; -}; - } // namespace bridge } // namespace remote } // namespace firestore diff --git a/Firestore/core/src/firebase/firestore/remote/remote_objc_bridge.mm b/Firestore/core/src/firebase/firestore/remote/remote_objc_bridge.mm index 406f9199f6d..c2f0882b807 100644 --- a/Firestore/core/src/firebase/firestore/remote/remote_objc_bridge.mm +++ b/Firestore/core/src/firebase/firestore/remote/remote_objc_bridge.mm @@ -208,16 +208,16 @@ bool IsLoggingEnabled() { return [serializer_ decodedVersion:proto.commitTime]; } -NSArray* WriteStreamSerializer::ToMutationResults( +std::vector WriteStreamSerializer::ToMutationResults( GCFSWriteResponse* response) const { NSMutableArray* responses = response.writeResultsArray; - NSMutableArray* results = - [NSMutableArray arrayWithCapacity:responses.count]; + std::vector results; + results.reserve(responses.count); const model::SnapshotVersion commitVersion = ToCommitVersion(response); for (GCFSWriteResult* proto in responses) { - [results addObject:[serializer_ decodedMutationResult:proto - commitVersion:commitVersion]]; + results.push_back([serializer_ decodedMutationResult:proto + commitVersion:commitVersion]); }; return results; } @@ -300,27 +300,6 @@ bool IsLoggingEnabled() { return [serializer_ decodedMaybeDocumentFromBatch:response]; } -// WriteStreamDelegate - -void WriteStreamDelegate::NotifyDelegateOnOpen() { - [delegate_ writeStreamDidOpen]; -} - -void WriteStreamDelegate::NotifyDelegateOnHandshakeComplete() { - [delegate_ writeStreamDidCompleteHandshake]; -} - -void WriteStreamDelegate::NotifyDelegateOnCommit( - const SnapshotVersion& commit_version, - NSArray* results) { - [delegate_ writeStreamDidReceiveResponseWithVersion:commit_version - mutationResults:results]; -} - -void WriteStreamDelegate::NotifyDelegateOnClose(const Status& status) { - [delegate_ writeStreamWasInterruptedWithError:status]; -} - } // namespace bridge } // namespace remote } // namespace firestore diff --git a/Firestore/core/src/firebase/firestore/remote/remote_store.h b/Firestore/core/src/firebase/firestore/remote/remote_store.h index 4d9180fe365..6d0d0bfcf6f 100644 --- a/Firestore/core/src/firebase/firestore/remote/remote_store.h +++ b/Firestore/core/src/firebase/firestore/remote/remote_store.h @@ -25,6 +25,7 @@ #include #include +#include #include "Firestore/core/src/firebase/firestore/model/document_key_set.h" #include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" @@ -34,10 +35,12 @@ #include "Firestore/core/src/firebase/firestore/remote/remote_event.h" #include "Firestore/core/src/firebase/firestore/remote/watch_change.h" #include "Firestore/core/src/firebase/firestore/remote/watch_stream.h" +#include "Firestore/core/src/firebase/firestore/remote/write_stream.h" #include "Firestore/core/src/firebase/firestore/util/async_queue.h" #include "Firestore/core/src/firebase/firestore/util/status.h" @class FSTLocalStore; +@class FSTMutationBatch; @class FSTMutationBatchResult; @class FSTQueryData; @@ -104,7 +107,9 @@ namespace firebase { namespace firestore { namespace remote { -class RemoteStore : public TargetMetadataProvider, public WatchStreamCallback { +class RemoteStore : public TargetMetadataProvider, + public WatchStreamCallback, + public WriteStreamCallback { public: RemoteStore(FSTLocalStore* local_store, Datastore* datastore, @@ -112,6 +117,7 @@ class RemoteStore : public TargetMetadataProvider, public WatchStreamCallback { std::function online_state_handler); // TODO(varconst): remove the getters and setters + id sync_engine() { return sync_engine_; } @@ -134,6 +140,13 @@ class RemoteStore : public TargetMetadataProvider, public WatchStreamCallback { WatchStream& watch_stream() { return *watch_stream_; } + WriteStream& write_stream() { + return *write_stream_; + } + + std::vector& write_pipeline() { + return write_pipeline_; + } /** Listens to the target identified by the given `FSTQueryData`. */ void Listen(FSTQueryData* query_data); @@ -151,6 +164,13 @@ class RemoteStore : public TargetMetadataProvider, public WatchStreamCallback { const model::SnapshotVersion& snapshot_version) override; void OnWatchStreamClose(const util::Status& status) override; + void OnWriteStreamOpen() override; + void OnWriteStreamHandshakeComplete() override; + void OnWriteStreamClose(const util::Status& status) override; + void OnWriteStreamMutationResult( + model::SnapshotVersion commit_version, + std::vector mutation_results) override; + // TODO(varconst): make the following methods private. bool CanUseNetwork() const; @@ -165,6 +185,22 @@ class RemoteStore : public TargetMetadataProvider, public WatchStreamCallback { void CleanUpWatchStreamState(); + /** + * Attempts to fill our write pipeline with writes from the `FSTLocalStore`. + * + * Called internally to bootstrap or refill the write pipeline and by + * `FSTSyncEngine` whenever there are new mutations to process. + * + * Starts the write stream if necessary. + */ + void FillWritePipeline(); + + /** + * Queues additional writes to be sent to the write stream, sending them + * immediately if the write stream is established. + */ + void AddToWritePipeline(FSTMutationBatch* batch); + private: void SendWatchRequest(FSTQueryData* query_data); void SendUnwatchRequest(model::TargetId target_id); @@ -178,6 +214,23 @@ class RemoteStore : public TargetMetadataProvider, public WatchStreamCallback { /** Process a target error and passes the error along to `SyncEngine`. */ void ProcessTargetError(const WatchTargetChange& change); + /** + * Returns true if we can add to the write pipeline (i.e. it is not full and + * the network is enabled). + */ + bool CanAddToWritePipeline() const; + + void StartWriteStream(); + + /** + * Returns true if the network is enabled, the write stream has not yet been + * started and there are pending writes. + */ + bool ShouldStartWriteStream() const; + + void HandleHandshakeError(const util::Status& status); + void HandleWriteError(const util::Status& status); + id sync_engine_ = nil; /** @@ -206,7 +259,27 @@ class RemoteStore : public TargetMetadataProvider, public WatchStreamCallback { bool is_network_enabled_ = false; std::shared_ptr watch_stream_; + std::shared_ptr write_stream_; std::unique_ptr watch_change_aggregator_; + + /** + * A list of up to `kMaxPendingWrites` writes that we have fetched from the + * `LocalStore` via `FillWritePipeline` and have or will send to the write + * stream. + * + * Whenever `write_pipeline_` is not empty, the `RemoteStore` will attempt to + * start or restart the write stream. When the stream is established, the + * writes in the pipeline will be sent in order. + * + * Writes remain in `write_pipeline_` until they are acknowledged by the + * backend and thus will automatically be re-sent if the stream is interrupted + * / restarted before they're acknowledged. + * + * Write responses from the backend are linked to their originating request + * purely based on order, and so we can just remove writes from the front of + * the `write_pipeline_` as we receive responses. + */ + std::vector write_pipeline_; }; } // namespace remote diff --git a/Firestore/core/src/firebase/firestore/remote/remote_store.mm b/Firestore/core/src/firebase/firestore/remote/remote_store.mm index f40a5e7b31d..3779086aeba 100644 --- a/Firestore/core/src/firebase/firestore/remote/remote_store.mm +++ b/Firestore/core/src/firebase/firestore/remote/remote_store.mm @@ -20,16 +20,20 @@ #import "Firestore/Source/Local/FSTLocalStore.h" #import "Firestore/Source/Local/FSTQueryData.h" +#import "Firestore/Source/Model/FSTMutationBatch.h" +#include "Firestore/core/src/firebase/firestore/model/mutation_batch.h" #include "Firestore/core/src/firebase/firestore/util/error_apple.h" #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" +#include "Firestore/core/src/firebase/firestore/util/log.h" #include "absl/memory/memory.h" +using firebase::firestore::model::BatchId; using firebase::firestore::model::DocumentKeySet; using firebase::firestore::model::OnlineState; using firebase::firestore::model::SnapshotVersion; -using firebase::firestore::model::DocumentKeySet; using firebase::firestore::model::TargetId; +using firebase::firestore::model::kBatchIdUnknown; using firebase::firestore::remote::Datastore; using firebase::firestore::remote::WatchStream; using firebase::firestore::remote::DocumentWatchChange; @@ -48,6 +52,12 @@ namespace firestore { namespace remote { +/** + * The maximum number of pending writes to allow. + * TODO(b/35853402): Negotiate this value with the backend. + */ +constexpr int kMaxPendingWrites = 10; + RemoteStore::RemoteStore( FSTLocalStore* local_store, Datastore* datastore, @@ -57,8 +67,11 @@ online_state_tracker_{worker_queue, std::move(online_state_handler)} { // Create streams (but note they're not started yet) watch_stream_ = datastore->CreateWatchStream(this); + write_stream_ = datastore->CreateWriteStream(this); } +// Watch Stream + void RemoteStore::Listen(FSTQueryData* query_data) { TargetId targetKey = query_data.targetID; HARD_ASSERT(listen_targets_.find(targetKey) == listen_targets_.end(), @@ -96,15 +109,6 @@ } } -FSTQueryData* RemoteStore::GetQueryDataForTarget(TargetId target_id) const { - auto found = listen_targets_.find(target_id); - return found != listen_targets_.end() ? found->second : nil; -} - -DocumentKeySet RemoteStore::GetRemoteKeysForTarget(TargetId target_id) const { - return [sync_engine_ remoteKeysForTarget:target_id]; -} - void RemoteStore::SendWatchRequest(FSTQueryData* query_data) { // We need to increment the the expected number of pending responses we're due // from watch so we wait for the ack to process any messages from this target. @@ -120,6 +124,11 @@ watch_stream_->UnwatchTargetId(target_id); } +bool RemoteStore::ShouldStartWatchStream() const { + return CanUseNetwork() && !watch_stream_->IsStarted() && + !listen_targets_.empty(); +} + void RemoteStore::StartWatchStream() { HARD_ASSERT(ShouldStartWatchStream(), "StartWatchStream called when ShouldStartWatchStream is false."); @@ -129,17 +138,6 @@ online_state_tracker_.HandleWatchStreamStart(); } -bool RemoteStore::ShouldStartWatchStream() const { - return CanUseNetwork() && !watch_stream_->IsStarted() && - !listen_targets_.empty(); -} - -bool RemoteStore::CanUseNetwork() const { - // PORTING NOTE: This method exists mostly because web also has to take into - // account primary vs. secondary state. - return is_network_enabled_; -} - void RemoteStore::CleanUpWatchStreamState() { watch_change_aggregator_.reset(); } @@ -291,6 +289,185 @@ } } +// Write Stream + +void RemoteStore::FillWritePipeline() { + BatchId last_batch_id_retrieved = write_pipeline_.empty() + ? kBatchIdUnknown + : write_pipeline_.back().batchID; + while (CanAddToWritePipeline()) { + FSTMutationBatch* batch = + [local_store_ nextMutationBatchAfterBatchID:last_batch_id_retrieved]; + if (!batch) { + if (write_pipeline_.empty()) { + write_stream_->MarkIdle(); + } + break; + } + AddToWritePipeline(batch); + last_batch_id_retrieved = batch.batchID; + } + + if (ShouldStartWriteStream()) { + StartWriteStream(); + } +} + +bool RemoteStore::CanAddToWritePipeline() const { + return CanUseNetwork() && write_pipeline_.size() < kMaxPendingWrites; +} + +void RemoteStore::AddToWritePipeline(FSTMutationBatch* batch) { + HARD_ASSERT(CanAddToWritePipeline(), + "AddToWritePipeline called when pipeline is full"); + + write_pipeline_.push_back(batch); + + if (write_stream_->IsOpen() && write_stream_->handshake_complete()) { + write_stream_->WriteMutations(batch.mutations); + } +} + +bool RemoteStore::ShouldStartWriteStream() const { + return CanUseNetwork() && !write_stream_->IsStarted() && + !write_pipeline_.empty(); +} + +void RemoteStore::StartWriteStream() { + HARD_ASSERT(ShouldStartWriteStream(), "StartWriteStream called when " + "ShouldStartWriteStream is false."); + write_stream_->Start(); +} + +void RemoteStore::OnWriteStreamOpen() { + write_stream_->WriteHandshake(); +} + +void RemoteStore::OnWriteStreamHandshakeComplete() { + // Record the stream token. + [local_store_ setLastStreamToken:write_stream_->GetLastStreamToken()]; + + // Send the write pipeline now that the stream is established. + for (FSTMutationBatch* write : write_pipeline_) { + write_stream_->WriteMutations(write.mutations); + } +} + +void RemoteStore::OnWriteStreamMutationResult( + SnapshotVersion commit_version, + std::vector mutation_results) { + // This is a response to a write containing mutations and should be correlated + // to the first write in our write pipeline. + HARD_ASSERT(!write_pipeline_.empty(), "Got result for empty write pipeline"); + + FSTMutationBatch* batch = write_pipeline_.front(); + write_pipeline_.erase(write_pipeline_.begin()); + + FSTMutationBatchResult* batchResult = [FSTMutationBatchResult + resultWithBatch:batch + commitVersion:commit_version + mutationResults:std::move(mutation_results) + streamToken:write_stream_->GetLastStreamToken()]; + [sync_engine_ applySuccessfulWriteWithResult:batchResult]; + + // It's possible that with the completion of this mutation another slot has + // freed up. + FillWritePipeline(); +} + +void RemoteStore::OnWriteStreamClose(const Status& status) { + if (status.ok()) { + // Graceful stop (due to Stop() or idle timeout). Make sure that's + // desirable. + HARD_ASSERT(!ShouldStartWriteStream(), + "Write stream was stopped gracefully while still needed."); + } + + // If the write stream closed due to an error, invoke the error callbacks if + // there are pending writes. + if (!status.ok() && !write_pipeline_.empty()) { + // TODO(varconst): handle UNAUTHENTICATED status, see + // go/firestore-client-errors + if (write_stream_->handshake_complete()) { + // This error affects the actual writes. + HandleWriteError(status); + } else { + // If there was an error before the handshake finished, it's possible that + // the server is unable to process the stream token we're sending. + // (Perhaps it's too old?) + HandleHandshakeError(status); + } + } + + // The write stream might have been started by refilling the write pipeline + // for failed writes + if (ShouldStartWriteStream()) { + StartWriteStream(); + } +} + +void RemoteStore::HandleHandshakeError(const Status& status) { + HARD_ASSERT(!status.ok(), "Handling write error with status OK."); + + // Reset the token if it's a permanent error, signaling the write stream is + // no longer valid. Note that the handshake does not count as a write: see + // comments on `Datastore::IsPermanentWriteError` for details. + if (Datastore::IsPermanentError(status)) { + NSString* token = + [write_stream_->GetLastStreamToken() base64EncodedStringWithOptions:0]; + LOG_DEBUG("RemoteStore %s error before completed handshake; resetting " + "stream token %s: " + "error code: '%s', details: '%s'", + this, token, status.code(), status.error_message()); + write_stream_->SetLastStreamToken(nil); + [local_store_ setLastStreamToken:nil]; + } else { + // Some other error, don't reset stream token. Our stream logic will just + // retry with exponential backoff. + } +} + +void RemoteStore::HandleWriteError(const Status& status) { + HARD_ASSERT(!status.ok(), "Handling write error with status OK."); + + // Only handle permanent errors here. If it's transient, just let the retry + // logic kick in. + if (!Datastore::IsPermanentWriteError(status)) { + return; + } + + // If this was a permanent error, the request itself was the problem so it's + // not going to succeed if we resend it. + FSTMutationBatch* batch = write_pipeline_.front(); + write_pipeline_.erase(write_pipeline_.begin()); + + // In this case it's also unlikely that the server itself is melting + // down--this was just a bad request so inhibit backoff on the next restart. + write_stream_->InhibitBackoff(); + + [sync_engine_ rejectFailedWriteWithBatchID:batch.batchID + error:util::MakeNSError(status)]; + + // It's possible that with the completion of this mutation another slot has + // freed up. + FillWritePipeline(); +} + +bool RemoteStore::CanUseNetwork() const { + // PORTING NOTE: This method exists mostly because web also has to take into + // account primary vs. secondary state. + return is_network_enabled_; +} + +DocumentKeySet RemoteStore::GetRemoteKeysForTarget(TargetId target_id) const { + return [sync_engine_ remoteKeysForTarget:target_id]; +} + +FSTQueryData* RemoteStore::GetQueryDataForTarget(TargetId target_id) const { + auto found = listen_targets_.find(target_id); + return found != listen_targets_.end() ? found->second : nil; +} + } // namespace remote } // namespace firestore } // namespace firebase diff --git a/Firestore/core/src/firebase/firestore/remote/write_stream.h b/Firestore/core/src/firebase/firestore/remote/write_stream.h index 8e34e3eac25..8e85bcf1693 100644 --- a/Firestore/core/src/firebase/firestore/remote/write_stream.h +++ b/Firestore/core/src/firebase/firestore/remote/write_stream.h @@ -24,7 +24,9 @@ #import #include #include +#include +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" #include "Firestore/core/src/firebase/firestore/remote/grpc_connection.h" #include "Firestore/core/src/firebase/firestore/remote/remote_objc_bridge.h" #include "Firestore/core/src/firebase/firestore/remote/stream.h" @@ -37,10 +39,46 @@ #import "Firestore/Source/Model/FSTMutation.h" #import "Firestore/Source/Remote/FSTSerializerBeta.h" +@class FSTMutationResult; + namespace firebase { namespace firestore { namespace remote { +class WriteStreamCallback { + public: + /** + * Called by the `WriteStream` when it is ready to accept outbound request + * messages. + */ + virtual void OnWriteStreamOpen() = 0; + + /** + * Called by the `WriteStream` upon a successful handshake response from the + * server, which is the receiver's cue to send any pending writes. + */ + virtual void OnWriteStreamHandshakeComplete() = 0; + + /** + * Called by the `WriteStream` upon receiving a StreamingWriteResponse from + * the server that contains mutation results. + */ + virtual void OnWriteStreamMutationResult( + model::SnapshotVersion commit_version, + std::vector results) = 0; + + /** + * Called when the `WriteStream`'s underlying RPC is interrupted for whatever + * reason, usually because of an error, but possibly due to an idle timeout. + * The status passed to this method may be "ok", in which case the stream was + * closed without attributable fault. + * + * NOTE: This will not be called after `Stop` is called on the stream. See + * "Starting and Stopping" on `Stream` for details. + */ + virtual void OnWriteStreamClose(const util::Status& status) = 0; +}; + /** * A Stream that implements the Write RPC. * @@ -65,7 +103,7 @@ class WriteStream : public Stream { auth::CredentialsProvider* credentials_provider, FSTSerializerBeta* serializer, GrpcConnection* grpc_connection, - id delegate); + WriteStreamCallback* callback); void SetLastStreamToken(NSData* token); /** @@ -115,7 +153,7 @@ class WriteStream : public Stream { } bridge::WriteStreamSerializer serializer_bridge_; - bridge::WriteStreamDelegate delegate_bridge_; + WriteStreamCallback* callback_ = nullptr; bool handshake_complete_ = false; }; diff --git a/Firestore/core/src/firebase/firestore/remote/write_stream.mm b/Firestore/core/src/firebase/firestore/remote/write_stream.mm index 17f46a53e04..db778509fda 100644 --- a/Firestore/core/src/firebase/firestore/remote/write_stream.mm +++ b/Firestore/core/src/firebase/firestore/remote/write_stream.mm @@ -36,11 +36,11 @@ CredentialsProvider* credentials_provider, FSTSerializerBeta* serializer, GrpcConnection* grpc_connection, - id delegate) + WriteStreamCallback* callback) : Stream{async_queue, credentials_provider, grpc_connection, TimerId::WriteStreamConnectionBackoff, TimerId::WriteStreamIdle}, serializer_bridge_{serializer}, - delegate_bridge_{delegate} { + callback_{NOT_NULL(callback)} { } void WriteStream::SetLastStreamToken(NSData* token) { @@ -97,11 +97,11 @@ } void WriteStream::NotifyStreamOpen() { - delegate_bridge_.NotifyDelegateOnOpen(); + callback_->OnWriteStreamOpen(); } void WriteStream::NotifyStreamClose(const Status& status) { - delegate_bridge_.NotifyDelegateOnClose(status); + callback_->OnWriteStreamClose(status); // Delegate's logic might depend on whether handshake was completed, so only // reset it after notifying. handshake_complete_ = false; @@ -124,14 +124,14 @@ if (!handshake_complete()) { // The first response is the handshake response handshake_complete_ = true; - delegate_bridge_.NotifyDelegateOnHandshakeComplete(); + callback_->OnWriteStreamHandshakeComplete(); } else { // A successful first write response means the stream is healthy. // Note that we could consider a successful handshake healthy, however, the // write itself might be causing an error we want to back off from. backoff_.Reset(); - delegate_bridge_.NotifyDelegateOnCommit( + callback_->OnWriteStreamMutationResult( serializer_bridge_.ToCommitVersion(response), serializer_bridge_.ToMutationResults(response)); } From e8b0c1f462aab01083955cf7545c6267b3199304 Mon Sep 17 00:00:00 2001 From: Konstantin Varlamov Date: Thu, 7 Feb 2019 15:28:16 -0500 Subject: [PATCH 24/27] C++ migration: make all methods of `FSTRemoteStore` delegate to C++ (#2337) --- Firestore/Source/Remote/FSTRemoteStore.mm | 71 ++-------- .../firebase/firestore/remote/remote_store.h | 126 ++++++++++-------- .../firebase/firestore/remote/remote_store.mm | 84 +++++++++++- 3 files changed, 158 insertions(+), 123 deletions(-) diff --git a/Firestore/Source/Remote/FSTRemoteStore.mm b/Firestore/Source/Remote/FSTRemoteStore.mm index 9d86bd9ee29..0e3ca8f57c2 100644 --- a/Firestore/Source/Remote/FSTRemoteStore.mm +++ b/Firestore/Source/Remote/FSTRemoteStore.mm @@ -31,6 +31,7 @@ #include "Firestore/core/src/firebase/firestore/auth/user.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/model/document_key_set.h" #include "Firestore/core/src/firebase/firestore/model/mutation_batch.h" #include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" #include "Firestore/core/src/firebase/firestore/remote/online_state_tracker.h" @@ -52,7 +53,6 @@ using firebase::firestore::model::DocumentKeySet; using firebase::firestore::model::OnlineState; using firebase::firestore::model::SnapshotVersion; -using firebase::firestore::model::DocumentKeySet; using firebase::firestore::model::TargetId; using firebase::firestore::remote::Datastore; using firebase::firestore::remote::WatchStream; @@ -75,9 +75,6 @@ #pragma mark - FSTRemoteStore @implementation FSTRemoteStore { - /** The client-side proxy for interacting with the backend. */ - std::shared_ptr _datastore; - std::unique_ptr _remoteStore; } @@ -86,12 +83,8 @@ - (instancetype)initWithLocalStore:(FSTLocalStore *)localStore workerQueue:(AsyncQueue *)queue onlineStateHandler:(std::function)onlineStateHandler { if (self = [super init]) { - _datastore = std::move(datastore); - _datastore->Start(); - - _remoteStore = absl::make_unique(localStore, _datastore.get(), queue, + _remoteStore = absl::make_unique(localStore, std::move(datastore), queue, std::move(onlineStateHandler)); - _remoteStore->set_is_network_enabled(false); } return self; } @@ -101,75 +94,27 @@ - (void)setSyncEngine:(id)syncEngine { } - (void)start { - // For now, all setup is handled by enableNetwork(). We might expand on this in the future. - [self enableNetwork]; + _remoteStore->Start(); } #pragma mark Online/Offline state - (void)enableNetwork { - _remoteStore->set_is_network_enabled(true); - - if (_remoteStore->CanUseNetwork()) { - // Load any saved stream token from persistent storage - _remoteStore->write_stream().SetLastStreamToken([_remoteStore->local_store() lastStreamToken]); - - if (_remoteStore->ShouldStartWatchStream()) { - _remoteStore->StartWatchStream(); - } else { - _remoteStore->online_state_tracker().UpdateState(OnlineState::Unknown); - } - - // This will start the write stream if necessary. - [self fillWritePipeline]; - } + _remoteStore->EnableNetwork(); } - (void)disableNetwork { - _remoteStore->set_is_network_enabled(false); - [self disableNetworkInternal]; - - // Set the OnlineState to Offline so get()s return from cache, etc. - _remoteStore->online_state_tracker().UpdateState(OnlineState::Offline); -} - -/** Disables the network, setting the OnlineState to the specified targetOnlineState. */ -- (void)disableNetworkInternal { - _remoteStore->watch_stream().Stop(); - _remoteStore->write_stream().Stop(); - - if (!_remoteStore->write_pipeline().empty()) { - LOG_DEBUG("Stopping write stream with %s pending writes", - _remoteStore->write_pipeline().size()); - _remoteStore->write_pipeline().clear(); - } - - _remoteStore->CleanUpWatchStreamState(); + _remoteStore->DisableNetwork(); } #pragma mark Shutdown - (void)shutdown { - LOG_DEBUG("FSTRemoteStore %s shutting down", (__bridge void *)self); - _remoteStore->set_is_network_enabled(false); - [self disableNetworkInternal]; - // Set the OnlineState to Unknown (rather than Offline) to avoid potentially triggering - // spurious listener events with cached data, etc. - _remoteStore->online_state_tracker().UpdateState(OnlineState::Unknown); - _datastore->Shutdown(); + _remoteStore->Shutdown(); } - (void)credentialDidChange { - if (_remoteStore->CanUseNetwork()) { - // Tear down and re-create our network streams. This will ensure we get a fresh auth token - // for the new user and re-fill the write pipeline with new mutations from the LocalStore - // (since mutations are per-user). - LOG_DEBUG("FSTRemoteStore %s restarting streams for new credential", (__bridge void *)self); - _remoteStore->set_is_network_enabled(false); - [self disableNetworkInternal]; - _remoteStore->online_state_tracker().UpdateState(OnlineState::Unknown); - [self enableNetwork]; - } + _remoteStore->HandleCredentialChange(); } #pragma mark Watch Stream @@ -201,7 +146,7 @@ - (void)addBatchToWritePipeline:(FSTMutationBatch *)batch { } - (FSTTransaction *)transaction { - return [FSTTransaction transactionWithDatastore:_datastore.get()]; + return _remoteStore->CreateTransaction(); } @end diff --git a/Firestore/core/src/firebase/firestore/remote/remote_store.h b/Firestore/core/src/firebase/firestore/remote/remote_store.h index 6d0d0bfcf6f..3d447ae2255 100644 --- a/Firestore/core/src/firebase/firestore/remote/remote_store.h +++ b/Firestore/core/src/firebase/firestore/remote/remote_store.h @@ -43,6 +43,7 @@ @class FSTMutationBatch; @class FSTMutationBatchResult; @class FSTQueryData; +@class FSTTransaction; NS_ASSUME_NONNULL_BEGIN @@ -112,41 +113,46 @@ class RemoteStore : public TargetMetadataProvider, public WriteStreamCallback { public: RemoteStore(FSTLocalStore* local_store, - Datastore* datastore, + std::shared_ptr datastore, util::AsyncQueue* worker_queue, std::function online_state_handler); - // TODO(varconst): remove the getters and setters - - id sync_engine() { - return sync_engine_; - } void set_sync_engine(id sync_engine) { sync_engine_ = sync_engine; } - FSTLocalStore* local_store() { - return local_store_; - } + /** + * Starts up the remote store, creating streams, restoring state from + * `FSTLocalStore`, etc. + */ + void Start(); - OnlineStateTracker& online_state_tracker() { - return online_state_tracker_; - } + /** + * Shuts down the remote store, tearing down connections and otherwise + * cleaning up. + */ + void Shutdown(); - void set_is_network_enabled(bool value) { - is_network_enabled_ = value; - } + /** + * Temporarily disables the network. The network can be re-enabled using + * 'EnableNetwork'. + */ + void DisableNetwork(); - WatchStream& watch_stream() { - return *watch_stream_; - } - WriteStream& write_stream() { - return *write_stream_; - } + /** + * Re-enables the network. Only to be called as the counterpart to + * 'DisableNetwork'. + */ + void EnableNetwork(); - std::vector& write_pipeline() { - return write_pipeline_; - } + /** + * Tells the `RemoteStore` that the currently authenticated user has changed. + * + * In response the remote store tears down streams and clears up any tracked + * operations that should not persist across users. Restarts the streams if + * appropriate. + */ + void HandleCredentialChange(); /** Listens to the target identified by the given `FSTQueryData`. */ void Listen(FSTQueryData* query_data); @@ -154,6 +160,25 @@ class RemoteStore : public TargetMetadataProvider, /** Stops listening to the target with the given target ID. */ void StopListening(model::TargetId target_id); + /** + * Attempts to fill our write pipeline with writes from the `FSTLocalStore`. + * + * Called internally to bootstrap or refill the write pipeline and by + * `FSTSyncEngine` whenever there are new mutations to process. + * + * Starts the write stream if necessary. + */ + void FillWritePipeline(); + + /** + * Queues additional writes to be sent to the write stream, sending them + * immediately if the write stream is established. + */ + void AddToWritePipeline(FSTMutationBatch* batch); + + /** Returns a new transaction backed by this remote store. */ + FSTTransaction* CreateTransaction(); + model::DocumentKeySet GetRemoteKeysForTarget( model::TargetId target_id) const override; FSTQueryData* GetQueryDataForTarget(model::TargetId target_id) const override; @@ -171,37 +196,9 @@ class RemoteStore : public TargetMetadataProvider, model::SnapshotVersion commit_version, std::vector mutation_results) override; - // TODO(varconst): make the following methods private. - - bool CanUseNetwork() const; - - void StartWatchStream(); - - /** - * Returns true if the network is enabled, the watch stream has not yet been - * started and there are active watch targets. - */ - bool ShouldStartWatchStream() const; - - void CleanUpWatchStreamState(); - - /** - * Attempts to fill our write pipeline with writes from the `FSTLocalStore`. - * - * Called internally to bootstrap or refill the write pipeline and by - * `FSTSyncEngine` whenever there are new mutations to process. - * - * Starts the write stream if necessary. - */ - void FillWritePipeline(); - - /** - * Queues additional writes to be sent to the write stream, sending them - * immediately if the write stream is established. - */ - void AddToWritePipeline(FSTMutationBatch* batch); - private: + void DisableNetworkInternal(); + void SendWatchRequest(FSTQueryData* query_data); void SendUnwatchRequest(model::TargetId target_id); @@ -231,14 +228,29 @@ class RemoteStore : public TargetMetadataProvider, void HandleHandshakeError(const util::Status& status); void HandleWriteError(const util::Status& status); + bool CanUseNetwork() const; + + void StartWatchStream(); + + /** + * Returns true if the network is enabled, the watch stream has not yet been + * started and there are active watch targets. + */ + bool ShouldStartWatchStream() const; + + void CleanUpWatchStreamState(); + id sync_engine_ = nil; /** * The local store, used to fill the write pipeline with outbound mutations - * and resolve existence filter mismatches. Immutable after initialization. + * and resolve existence filter mismatches. */ FSTLocalStore* local_store_ = nil; + /** The client-side proxy for interacting with the backend. */ + std::shared_ptr datastore_; + /** * A mapping of watched targets that the client cares about tracking and the * user has explicitly called a 'listen' for this target. @@ -253,8 +265,8 @@ class RemoteStore : public TargetMetadataProvider, OnlineStateTracker online_state_tracker_; /** - * Set to true by `EnableNetwork` and false by `DisableNetworkInternal` and - * indicates the user-preferred network state. + * Set to true by `EnableNetwork` and false by `DisableNetwork` and indicates + * the user-preferred network state. */ bool is_network_enabled_ = false; diff --git a/Firestore/core/src/firebase/firestore/remote/remote_store.mm b/Firestore/core/src/firebase/firestore/remote/remote_store.mm index 3779086aeba..5c28a7b25b7 100644 --- a/Firestore/core/src/firebase/firestore/remote/remote_store.mm +++ b/Firestore/core/src/firebase/firestore/remote/remote_store.mm @@ -18,6 +18,7 @@ #include +#import "Firestore/Source/Core/FSTTransaction.h" #import "Firestore/Source/Local/FSTLocalStore.h" #import "Firestore/Source/Local/FSTQueryData.h" #import "Firestore/Source/Model/FSTMutationBatch.h" @@ -60,14 +61,74 @@ RemoteStore::RemoteStore( FSTLocalStore* local_store, - Datastore* datastore, + std::shared_ptr datastore, AsyncQueue* worker_queue, std::function online_state_handler) : local_store_{local_store}, + datastore_{std::move(datastore)}, online_state_tracker_{worker_queue, std::move(online_state_handler)} { + datastore_->Start(); + // Create streams (but note they're not started yet) - watch_stream_ = datastore->CreateWatchStream(this); - write_stream_ = datastore->CreateWriteStream(this); + watch_stream_ = datastore_->CreateWatchStream(this); + write_stream_ = datastore_->CreateWriteStream(this); +} + +void RemoteStore::Start() { + // For now, all setup is handled by `EnableNetwork`. We might expand on this + // in the future. + EnableNetwork(); +} + +void RemoteStore::EnableNetwork() { + is_network_enabled_ = true; + + if (CanUseNetwork()) { + // Load any saved stream token from persistent storage + write_stream_->SetLastStreamToken([local_store_ lastStreamToken]); + + if (ShouldStartWatchStream()) { + StartWatchStream(); + } else { + online_state_tracker_.UpdateState(OnlineState::Unknown); + } + + // This will start the write stream if necessary. + FillWritePipeline(); + } +} + +void RemoteStore::DisableNetwork() { + is_network_enabled_ = false; + DisableNetworkInternal(); + + // Set the OnlineState to Offline so get()s return from cache, etc. + online_state_tracker_.UpdateState(OnlineState::Offline); +} + +void RemoteStore::DisableNetworkInternal() { + watch_stream_->Stop(); + write_stream_->Stop(); + + if (!write_pipeline_.empty()) { + LOG_DEBUG("Stopping write stream with %s pending writes", + write_pipeline_.size()); + write_pipeline_.clear(); + } + + CleanUpWatchStreamState(); +} + +void RemoteStore::Shutdown() { + LOG_DEBUG("RemoteStore %s shutting down", this); + is_network_enabled_ = false; + DisableNetworkInternal(); + + // Set the `OnlineState` to `Unknown` (rather than `Offline`) to avoid + // potentially triggering spurious listener events with cached data, etc. + online_state_tracker_.UpdateState(OnlineState::Unknown); + + datastore_->Shutdown(); } // Watch Stream @@ -459,6 +520,10 @@ return is_network_enabled_; } +FSTTransaction* RemoteStore::CreateTransaction() { + return [FSTTransaction transactionWithDatastore:datastore_.get()]; +} + DocumentKeySet RemoteStore::GetRemoteKeysForTarget(TargetId target_id) const { return [sync_engine_ remoteKeysForTarget:target_id]; } @@ -468,6 +533,19 @@ return found != listen_targets_.end() ? found->second : nil; } +void RemoteStore::HandleCredentialChange() { + if (CanUseNetwork()) { + // Tear down and re-create our network streams. This will ensure we get a + // fresh auth token for the new user and re-fill the write pipeline with new + // mutations from the `FSTLocalStore` (since mutations are per-user). + LOG_DEBUG("RemoteStore %s restarting streams for new credential", this); + is_network_enabled_ = false; + DisableNetworkInternal(); + online_state_tracker_.UpdateState(OnlineState::Unknown); + EnableNetwork(); + } +} + } // namespace remote } // namespace firestore } // namespace firebase From 10f50bf748c6ad0e02670f9abe933ddd91ad7478 Mon Sep 17 00:00:00 2001 From: Chuan Ren Date: Thu, 7 Feb 2019 14:34:42 -0800 Subject: [PATCH 25/27] Add NS_ASSUME_NONNULL_NOTATION for game center sign in (#2359) --- .../AuthProviders/GameCenter/FIRGameCenterAuthCredential.m | 4 ++++ .../AuthProviders/GameCenter/FIRGameCenterAuthProvider.m | 6 +++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Firebase/Auth/Source/AuthProviders/GameCenter/FIRGameCenterAuthCredential.m b/Firebase/Auth/Source/AuthProviders/GameCenter/FIRGameCenterAuthCredential.m index b98aec19cf1..91a4b684e2f 100644 --- a/Firebase/Auth/Source/AuthProviders/GameCenter/FIRGameCenterAuthCredential.m +++ b/Firebase/Auth/Source/AuthProviders/GameCenter/FIRGameCenterAuthCredential.m @@ -21,6 +21,8 @@ #import "FIRGameCenterAuthProvider.h" #import "FIRVerifyAssertionRequest.h" +NS_ASSUME_NONNULL_BEGIN + @implementation FIRGameCenterAuthCredential - (nullable instancetype)initWithProvider:(NSString *)provider { @@ -84,3 +86,5 @@ - (void)encodeWithCoder:(NSCoder *)aCoder { } @end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/AuthProviders/GameCenter/FIRGameCenterAuthProvider.m b/Firebase/Auth/Source/AuthProviders/GameCenter/FIRGameCenterAuthProvider.m index f037b0cb6cb..af8e7e6f74d 100644 --- a/Firebase/Auth/Source/AuthProviders/GameCenter/FIRGameCenterAuthProvider.m +++ b/Firebase/Auth/Source/AuthProviders/GameCenter/FIRGameCenterAuthProvider.m @@ -22,6 +22,8 @@ #import "FIRAuthExceptionUtils.h" #import "FIRGameCenterAuthCredential.h" +NS_ASSUME_NONNULL_BEGIN + @implementation FIRGameCenterAuthProvider - (instancetype)init { @@ -37,7 +39,7 @@ + (void)getCredentialWithCompletion:(FIRGameCenterCredentialCallback)completion checking whether the APP that consuming our SDK has linked GameKit.framework. If not, a `GameKitNotLinkedError` will be raised. **/ - GKLocalPlayer *optionalLocalPlayer = [[NSClassFromString(@"GKLocalPlayer") alloc] init]; + GKLocalPlayer * _Nullable optionalLocalPlayer = [[NSClassFromString(@"GKLocalPlayer") alloc] init]; if (!optionalLocalPlayer) { if (completion) { @@ -82,3 +84,5 @@ + (void)getCredentialWithCompletion:(FIRGameCenterCredentialCallback)completion } @end + +NS_ASSUME_NONNULL_END From 0074c79bc50a934716d2ad1b46c2a22bb53a5097 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Thu, 7 Feb 2019 15:47:06 -0800 Subject: [PATCH 26/27] Update CI to use CocoaPods 1.6.0 (#2360) --- Example/Firebase.xcodeproj/project.pbxproj | 16 ++----- Gemfile.lock | 52 +++++++++++----------- 2 files changed, 30 insertions(+), 38 deletions(-) diff --git a/Example/Firebase.xcodeproj/project.pbxproj b/Example/Firebase.xcodeproj/project.pbxproj index 829c7cdfad8..41a982e6abf 100644 --- a/Example/Firebase.xcodeproj/project.pbxproj +++ b/Example/Firebase.xcodeproj/project.pbxproj @@ -5760,7 +5760,7 @@ DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 4ANB9W7R3P; GCC_C_LANGUAGE_STANDARD = gnu11; - INFOPLIST_FILE = $SRCROOT/DynamicLinks/FDLBuilderTestAppObjC/Info.plist; + INFOPLIST_FILE = "$SRCROOT/DynamicLinks/FDLBuilderTestAppObjC/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 11.4; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; MTL_ENABLE_DEBUG_INFO = YES; @@ -5794,7 +5794,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 4ANB9W7R3P; GCC_C_LANGUAGE_STANDARD = gnu11; - INFOPLIST_FILE = $SRCROOT/DynamicLinks/FDLBuilderTestAppObjC/Info.plist; + INFOPLIST_FILE = "$SRCROOT/DynamicLinks/FDLBuilderTestAppObjC/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 11.4; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; MTL_ENABLE_DEBUG_INFO = NO; @@ -6458,6 +6458,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; + BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -6482,11 +6483,6 @@ INFOPLIST_FILE = "$(SRCROOT)/Auth/App/iOS/Auth-Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; MTL_ENABLE_DEBUG_INFO = YES; - OTHER_LDFLAGS = ( - "$(inherited)", - "-framework", - FirebaseAuth, - ); PRODUCT_BUNDLE_IDENTIFIER = "com.google.Auth-Example-tvOS"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = appletvos; @@ -6501,6 +6497,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; + BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -6526,11 +6523,6 @@ INFOPLIST_FILE = "$(SRCROOT)/Auth/App/iOS/Auth-Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; MTL_ENABLE_DEBUG_INFO = NO; - OTHER_LDFLAGS = ( - "$(inherited)", - "-framework", - FirebaseAuth, - ); PRODUCT_BUNDLE_IDENTIFIER = "com.google.Auth-Example-tvOS"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = appletvos; diff --git a/Gemfile.lock b/Gemfile.lock index 4a6b22f88d3..7ad1392a28e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,82 +1,82 @@ GIT remote: https://github.com/CocoaPods/CocoaPods.git - revision: 10b69dbd9b991442646944f118f569d855652b26 + revision: 984af0b1c31000b30455b276bcac21d424003f40 specs: - cocoapods (1.5.3) + cocoapods (1.6.0) activesupport (>= 4.0.2, < 5) claide (>= 1.0.2, < 2.0) - cocoapods-core (= 1.5.3) + cocoapods-core (= 1.6.0) cocoapods-deintegrate (>= 1.0.2, < 2.0) - cocoapods-downloader (>= 1.2.1, < 2.0) + cocoapods-downloader (>= 1.2.2, < 2.0) cocoapods-plugins (>= 1.0.0, < 2.0) cocoapods-search (>= 1.0.0, < 2.0) cocoapods-stats (>= 1.0.0, < 2.0) - cocoapods-trunk (>= 1.3.0, < 2.0) + cocoapods-trunk (>= 1.3.1, < 2.0) cocoapods-try (>= 1.1.0, < 2.0) colored2 (~> 3.1) escape (~> 0.0.4) - fourflusher (~> 2.0.1) + fourflusher (>= 2.2.0, < 3.0) gh_inspector (~> 1.0) - molinillo (~> 0.6.5) + molinillo (~> 0.6.6) nap (~> 1.0) - ruby-macho (~> 1.2) - xcodeproj (>= 1.5.8, < 2.0) + ruby-macho (~> 1.3, >= 1.3.1) + xcodeproj (>= 1.8.0, < 2.0) GIT remote: https://github.com/CocoaPods/Core.git - revision: 577c69f38fdb56cbdb883b44681ca2b224cad746 + revision: 887e2804a091eec4bc43f9db8285d8e07905418d specs: - cocoapods-core (1.5.3) + cocoapods-core (1.6.0) activesupport (>= 4.0.2, < 6) fuzzy_match (~> 2.0.4) nap (~> 1.0) GIT remote: https://github.com/CocoaPods/Xcodeproj.git - revision: cadb238767d09942a1c5eba90a3112034438ba23 + revision: 47492ac78703be3e44d2bcc0c2343dcfa92d26f4 specs: - xcodeproj (1.5.9) + xcodeproj (1.8.0) CFPropertyList (>= 2.3.3, < 4.0) - atomos (~> 0.1.2) + atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) colored2 (~> 3.1) - nanaimo (~> 0.2.5) + nanaimo (~> 0.2.6) GEM remote: https://rubygems.org/ specs: CFPropertyList (3.0.0) - activesupport (4.2.10) + activesupport (4.2.11) i18n (~> 0.7) minitest (~> 5.1) thread_safe (~> 0.3, >= 0.3.4) tzinfo (~> 1.1) - atomos (0.1.2) + atomos (0.1.3) claide (1.0.2) cocoapods-deintegrate (1.0.2) - cocoapods-downloader (1.2.1) + cocoapods-downloader (1.2.2) cocoapods-plugins (1.0.0) nap cocoapods-search (1.0.0) - cocoapods-stats (1.0.0) - cocoapods-trunk (1.3.0) + cocoapods-stats (1.1.0) + cocoapods-trunk (1.3.1) nap (>= 0.8, < 2.0) netrc (~> 0.11) cocoapods-try (1.1.0) colored2 (3.1.2) - concurrent-ruby (1.0.5) + concurrent-ruby (1.1.4) escape (0.0.4) - fourflusher (2.0.1) + fourflusher (2.2.0) fuzzy_match (2.0.4) gh_inspector (1.1.3) i18n (0.9.5) concurrent-ruby (~> 1.0) minitest (5.11.3) - molinillo (0.6.5) - nanaimo (0.2.5) + molinillo (0.6.6) + nanaimo (0.2.6) nap (1.1.0) netrc (0.11.0) - ruby-macho (1.2.0) + ruby-macho (1.3.1) thread_safe (0.3.6) tzinfo (1.2.5) thread_safe (~> 0.1) @@ -90,4 +90,4 @@ DEPENDENCIES xcodeproj! BUNDLED WITH - 1.16.1 + 1.16.6 From a22b4cd08e7d116d3c905d899b0a63827dca8c6d Mon Sep 17 00:00:00 2001 From: Konstantin Varlamov Date: Thu, 7 Feb 2019 22:03:17 -0500 Subject: [PATCH 27/27] Pass FSTMutations using a vector (#2357) --- .../Tests/Integration/FSTDatastoreTests.mm | 4 +- .../Local/FSTLRUGarbageCollectorTests.mm | 10 ++-- .../Tests/Local/FSTLocalSerializerTests.mm | 7 ++- .../Example/Tests/Local/FSTLocalStoreTests.mm | 52 ++++++++--------- .../Tests/Local/FSTMutationQueueTests.mm | 55 ++++++++---------- .../Tests/SpecTests/FSTMockDatastore.h | 2 +- .../Tests/SpecTests/FSTMockDatastore.mm | 10 ++-- .../SpecTests/FSTSyncEngineTestDriver.mm | 7 ++- Firestore/Example/Tests/Util/FSTHelpers.h | 9 +++ Firestore/Source/API/FIRDocumentReference.mm | 2 +- Firestore/Source/API/FIRWriteBatch.mm | 37 ++++++++---- Firestore/Source/Core/FSTFirestoreClient.h | 3 +- Firestore/Source/Core/FSTFirestoreClient.mm | 9 +-- Firestore/Source/Core/FSTSyncEngine.h | 5 +- Firestore/Source/Core/FSTSyncEngine.mm | 4 +- Firestore/Source/Core/FSTTransaction.h | 6 -- Firestore/Source/Core/FSTTransaction.mm | 20 +++---- .../Source/Local/FSTLocalDocumentsView.mm | 2 +- Firestore/Source/Local/FSTLocalSerializer.mm | 10 ++-- Firestore/Source/Local/FSTLocalStore.h | 4 +- Firestore/Source/Local/FSTLocalStore.mm | 7 ++- Firestore/Source/Model/FSTMutationBatch.h | 5 +- Firestore/Source/Model/FSTMutationBatch.mm | 55 +++++++++++------- .../src/firebase/firestore/core/user_data.h | 4 +- .../src/firebase/firestore/core/user_data.mm | 56 +++++++++++-------- .../firestore/local/leveldb_mutation_queue.h | 5 +- .../firestore/local/leveldb_mutation_queue.mm | 11 ++-- .../firestore/local/memory_mutation_queue.h | 5 +- .../firestore/local/memory_mutation_queue.mm | 12 ++-- .../firebase/firestore/local/mutation_queue.h | 3 +- .../src/firebase/firestore/remote/datastore.h | 9 +-- .../firebase/firestore/remote/datastore.mm | 9 +-- .../firestore/remote/remote_objc_bridge.h | 6 +- .../firestore/remote/remote_objc_bridge.mm | 10 ++-- .../firebase/firestore/remote/write_stream.h | 2 +- .../firebase/firestore/remote/write_stream.mm | 2 +- .../firestore/remote/datastore_test.mm | 10 ++-- 37 files changed, 264 insertions(+), 205 deletions(-) diff --git a/Firestore/Example/Tests/Integration/FSTDatastoreTests.mm b/Firestore/Example/Tests/Integration/FSTDatastoreTests.mm index ec43e8ba7eb..243febbb8ab 100644 --- a/Firestore/Example/Tests/Integration/FSTDatastoreTests.mm +++ b/Firestore/Example/Tests/Integration/FSTDatastoreTests.mm @@ -207,7 +207,7 @@ - (void)tearDown { - (void)testCommit { XCTestExpectation *expectation = [self expectationWithDescription:@"commitWithCompletion"]; - _datastore->CommitMutations(@[], ^(NSError *_Nullable error) { + _datastore->CommitMutations({}, ^(NSError *_Nullable error) { XCTAssertNil(error, @"Failed to commit"); [expectation fulfill]; }); @@ -224,7 +224,7 @@ - (void)testStreamingWrite { FSTSetMutation *mutation = [self setMutation]; FSTMutationBatch *batch = [[FSTMutationBatch alloc] initWithBatchID:23 localWriteTime:[FIRTimestamp timestamp] - mutations:@[ mutation ]]; + mutations:{mutation}]; _testWorkerQueue->Enqueue([=] { [_remoteStore addBatchToWritePipeline:batch]; // The added batch won't be written immediately because write stream wasn't yet open -- diff --git a/Firestore/Example/Tests/Local/FSTLRUGarbageCollectorTests.mm b/Firestore/Example/Tests/Local/FSTLRUGarbageCollectorTests.mm index 3a1f5b78876..c691c675f9b 100644 --- a/Firestore/Example/Tests/Local/FSTLRUGarbageCollectorTests.mm +++ b/Firestore/Example/Tests/Local/FSTLRUGarbageCollectorTests.mm @@ -20,6 +20,8 @@ #include #include +#include +#include #import "FIRTimestamp.h" #import "Firestore/Example/Tests/Util/FSTHelpers.h" @@ -412,7 +414,7 @@ - (void)testRemoveOrphanedDocuments { // as any documents with pending mutations. std::unordered_set expectedRetained; // we add two mutations later, for now track them in an array. - NSMutableArray *mutations = [NSMutableArray arrayWithCapacity:2]; + std::vector mutations; // Add a target and add two documents to it. The documents are expected to be // retained, since their membership in the target keeps them alive. @@ -426,7 +428,7 @@ - (void)testRemoveOrphanedDocuments { FSTDocument *doc2 = [self cacheADocumentInTransaction]; [self addDocument:doc2.key toTarget:queryData.targetID]; expectedRetained.insert(doc2.key); - [mutations addObject:[self mutationForDocument:doc2.key]]; + mutations.push_back([self mutationForDocument:doc2.key]); }); // Add a second query and register a third document on it @@ -440,7 +442,7 @@ - (void)testRemoveOrphanedDocuments { // cache another document and prepare a mutation on it. _persistence.run("queue a mutation", [&]() { FSTDocument *doc4 = [self cacheADocumentInTransaction]; - [mutations addObject:[self mutationForDocument:doc4.key]]; + mutations.push_back([self mutationForDocument:doc4.key]); expectedRetained.insert(doc4.key); }); @@ -448,7 +450,7 @@ - (void)testRemoveOrphanedDocuments { // serve to keep the mutated documents from being GC'd while the mutations are outstanding. _persistence.run("actually register the mutations", [&]() { FIRTimestamp *writeTime = [FIRTimestamp timestamp]; - _mutationQueue->AddMutationBatch(writeTime, mutations); + _mutationQueue->AddMutationBatch(writeTime, std::move(mutations)); }); // Mark 5 documents eligible for GC. This simulates documents that were mutated then ack'd. diff --git a/Firestore/Example/Tests/Local/FSTLocalSerializerTests.mm b/Firestore/Example/Tests/Local/FSTLocalSerializerTests.mm index 538f6c61e86..32a0a55a6f9 100644 --- a/Firestore/Example/Tests/Local/FSTLocalSerializerTests.mm +++ b/Firestore/Example/Tests/Local/FSTLocalSerializerTests.mm @@ -19,6 +19,9 @@ #import #import +#include +#include + #import "Firestore/Protos/objc/firestore/local/MaybeDocument.pbobjc.h" #import "Firestore/Protos/objc/firestore/local/Mutation.pbobjc.h" #import "Firestore/Protos/objc/firestore/local/Target.pbobjc.h" @@ -89,7 +92,7 @@ - (void)testEncodesMutationBatch { FIRTimestamp *writeTime = [FIRTimestamp timestamp]; FSTMutationBatch *model = [[FSTMutationBatch alloc] initWithBatchID:42 localWriteTime:writeTime - mutations:@[ set, patch, del ]]; + mutations:{set, patch, del}]; GCFSWrite *setProto = [GCFSWrite message]; setProto.update.name = @"projects/p/databases/d/documents/foo/bar"; @@ -123,7 +126,7 @@ - (void)testEncodesMutationBatch { FSTMutationBatch *decoded = [self.serializer decodedMutationBatch:batchProto]; XCTAssertEqual(decoded.batchID, model.batchID); XCTAssertEqualObjects(decoded.localWriteTime, model.localWriteTime); - XCTAssertEqualObjects(decoded.mutations, model.mutations); + FSTAssertEqualVectors(decoded.mutations, model.mutations); XCTAssertEqual([decoded keys], [model keys]); } diff --git a/Firestore/Example/Tests/Local/FSTLocalStoreTests.mm b/Firestore/Example/Tests/Local/FSTLocalStoreTests.mm index 026db12ad05..da8a8b0b4c9 100644 --- a/Firestore/Example/Tests/Local/FSTLocalStoreTests.mm +++ b/Firestore/Example/Tests/Local/FSTLocalStoreTests.mm @@ -19,6 +19,7 @@ #import #import +#include #include #import "Firestore/Source/Core/FSTQuery.h" @@ -124,15 +125,16 @@ - (BOOL)isTestBaseClass { } - (void)writeMutation:(FSTMutation *)mutation { - [self writeMutations:@[ mutation ]]; + [self writeMutations:{mutation}]; } -- (void)writeMutations:(NSArray *)mutations { - FSTLocalWriteResult *result = [self.localStore locallyWriteMutations:mutations]; +- (void)writeMutations:(std::vector &&)mutations { + auto mutationsCopy = mutations; + FSTLocalWriteResult *result = [self.localStore locallyWriteMutations:std::move(mutationsCopy)]; XCTAssertNotNil(result); [self.batches addObject:[[FSTMutationBatch alloc] initWithBatchID:result.batchID localWriteTime:[FIRTimestamp timestamp] - mutations:mutations]]; + mutations:std::move(mutations)]]; _lastChanges = result.changes; } @@ -147,7 +149,7 @@ - (void)notifyLocalViewChanges:(FSTLocalViewChanges *)changes { - (void)acknowledgeMutationWithVersion:(FSTTestSnapshotVersion)documentVersion { FSTMutationBatch *batch = [self.batches firstObject]; [self.batches removeObjectAtIndex:0]; - XCTAssertEqual(batch.mutations.count, 1, @"Acknowledging more than one mutation not supported."); + XCTAssertEqual(batch.mutations.size(), 1, @"Acknowledging more than one mutation not supported."); SnapshotVersion version = testutil::Version(documentVersion); FSTMutationResult *mutationResult = [[FSTMutationResult alloc] initWithVersion:version transformResults:nil]; @@ -227,7 +229,7 @@ - (void)testMutationBatchKeys { FSTMutation *set2 = FSTTestSetMutation(@"bar/baz", @{@"bar" : @"baz"}); FSTMutationBatch *batch = [[FSTMutationBatch alloc] initWithBatchID:1 localWriteTime:[FIRTimestamp timestamp] - mutations:@[ set1, set2 ]]; + mutations:{set1, set2}]; DocumentKeySet keys = [batch keys]; XCTAssertEqual(keys.size(), 2u); } @@ -619,10 +621,10 @@ - (void)testHandlesSetMutationThenPatchMutationThenDocumentThenAckThenAck { - (void)testHandlesSetMutationAndPatchMutationTogether { if ([self isTestBaseClass]) return; - [self writeMutations:@[ + [self writeMutations:{ FSTTestSetMutation(@"foo/bar", @{@"foo" : @"old"}), - FSTTestPatchMutation("foo/bar", @{@"foo" : @"bar"}, {}) - ]]; + FSTTestPatchMutation("foo/bar", @{@"foo" : @"bar"}, {}) + }]; FSTAssertChanged( @[ FSTTestDoc("foo/bar", 0, @{@"foo" : @"bar"}, FSTDocumentStateLocalMutations) ]); @@ -649,11 +651,11 @@ - (void)testHandlesSetMutationThenPatchMutationThenReject { - (void)testHandlesSetMutationsAndPatchMutationOfJustOneTogether { if ([self isTestBaseClass]) return; - [self writeMutations:@[ + [self writeMutations:{ FSTTestSetMutation(@"foo/bar", @{@"foo" : @"old"}), - FSTTestSetMutation(@"bar/baz", @{@"bar" : @"baz"}), - FSTTestPatchMutation("foo/bar", @{@"foo" : @"bar"}, {}) - ]]; + FSTTestSetMutation(@"bar/baz", @{@"bar" : @"baz"}), + FSTTestPatchMutation("foo/bar", @{@"foo" : @"bar"}, {}) + }]; FSTAssertChanged((@[ FSTTestDoc("bar/baz", 0, @{@"bar" : @"baz"}, FSTDocumentStateLocalMutations), @@ -843,11 +845,11 @@ - (void)testThrowsAwayDocumentsWithUnknownTargetIDsImmediately { - (void)testCanExecuteDocumentQueries { if ([self isTestBaseClass]) return; - [self.localStore locallyWriteMutations:@[ + [self.localStore locallyWriteMutations:{ FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"}), - FSTTestSetMutation(@"foo/baz", @{@"foo" : @"baz"}), - FSTTestSetMutation(@"foo/bar/Foo/Bar", @{@"Foo" : @"Bar"}) - ]]; + FSTTestSetMutation(@"foo/baz", @{@"foo" : @"baz"}), + FSTTestSetMutation(@"foo/bar/Foo/Bar", @{@"Foo" : @"Bar"}) + }]; FSTQuery *query = FSTTestQuery("foo/bar"); DocumentMap docs = [self.localStore executeQuery:query]; XCTAssertEqualObjects(docMapToArray(docs), @[ FSTTestDoc("foo/bar", 0, @{@"foo" : @"bar"}, @@ -857,13 +859,13 @@ - (void)testCanExecuteDocumentQueries { - (void)testCanExecuteCollectionQueries { if ([self isTestBaseClass]) return; - [self.localStore locallyWriteMutations:@[ + [self.localStore locallyWriteMutations:{ FSTTestSetMutation(@"fo/bar", @{@"fo" : @"bar"}), - FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"}), - FSTTestSetMutation(@"foo/baz", @{@"foo" : @"baz"}), - FSTTestSetMutation(@"foo/bar/Foo/Bar", @{@"Foo" : @"Bar"}), - FSTTestSetMutation(@"fooo/blah", @{@"fooo" : @"blah"}) - ]]; + FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"}), + FSTTestSetMutation(@"foo/baz", @{@"foo" : @"baz"}), + FSTTestSetMutation(@"foo/bar/Foo/Bar", @{@"Foo" : @"Bar"}), + FSTTestSetMutation(@"fooo/blah", @{@"fooo" : @"blah"}) + }]; FSTQuery *query = FSTTestQuery("foo"); DocumentMap docs = [self.localStore executeQuery:query]; XCTAssertEqualObjects( @@ -887,7 +889,7 @@ - (void)testCanExecuteMixedCollectionQueries { FSTTestDoc("foo/bar", 20, @{@"a" : @"b"}, FSTDocumentStateSynced), {2}, {})]; - [self.localStore locallyWriteMutations:@[ FSTTestSetMutation(@"foo/bonk", @{@"a" : @"b"}) ]]; + [self.localStore locallyWriteMutations:{ FSTTestSetMutation(@"foo/bonk", @{@"a" : @"b"}) }]; DocumentMap docs = [self.localStore executeQuery:query]; XCTAssertEqualObjects(docMapToArray(docs), (@[ @@ -942,7 +944,7 @@ - (void)testRemoteDocumentKeysForTarget { applyRemoteEvent:FSTTestAddedRemoteEvent( FSTTestDoc("foo/bar", 20, @{@"a" : @"b"}, FSTDocumentStateSynced), {2})]; - [self.localStore locallyWriteMutations:@[ FSTTestSetMutation(@"foo/bonk", @{@"a" : @"b"}) ]]; + [self.localStore locallyWriteMutations:{ FSTTestSetMutation(@"foo/bonk", @{@"a" : @"b"}) }]; DocumentKeySet keys = [self.localStore remoteDocumentKeysForTarget:2]; DocumentKeySet expected{testutil::Key("foo/bar"), testutil::Key("foo/baz")}; diff --git a/Firestore/Example/Tests/Local/FSTMutationQueueTests.mm b/Firestore/Example/Tests/Local/FSTMutationQueueTests.mm index c975bf70cd8..c3fcc6f6131 100644 --- a/Firestore/Example/Tests/Local/FSTMutationQueueTests.mm +++ b/Firestore/Example/Tests/Local/FSTMutationQueueTests.mm @@ -19,6 +19,7 @@ #import #include +#include #include #import "Firestore/Source/Core/FSTQuery.h" @@ -50,14 +51,6 @@ - (void)tearDown { [super tearDown]; } -- (void)assertVector:(const std::vector &)actual - matchesExpected:(const std::vector &)expected { - XCTAssertEqual(actual.size(), expected.size(), @"Vector length mismatch"); - for (int i = 0; i < expected.size(); i++) { - XCTAssertEqualObjects(actual[i], expected[i]); - } -} - /** * Xcode will run tests from any class that extends XCTestCase, but this doesn't work for * FSTMutationQueueTests since it is incomplete without the implementations supplied by its @@ -208,7 +201,7 @@ - (void)testAllMutationBatchesAffectingDocumentKey { NSMutableArray *batches = [NSMutableArray array]; for (FSTMutation *mutation in mutations) { FSTMutationBatch *batch = - self.mutationQueue->AddMutationBatch([FIRTimestamp timestamp], @[ mutation ]); + self.mutationQueue->AddMutationBatch([FIRTimestamp timestamp], { mutation }); [batches addObject:batch]; } @@ -216,7 +209,7 @@ - (void)testAllMutationBatchesAffectingDocumentKey { std::vector matches = self.mutationQueue->AllMutationBatchesAffectingDocumentKey(testutil::Key("foo/bar")); - [self assertVector:matches matchesExpected:expected]; + FSTAssertEqualVectors(matches, expected); }); } @@ -235,7 +228,7 @@ - (void)testAllMutationBatchesAffectingDocumentKeys { NSMutableArray *batches = [NSMutableArray array]; for (FSTMutation *mutation in mutations) { FSTMutationBatch *batch = - self.mutationQueue->AddMutationBatch([FIRTimestamp timestamp], @[ mutation ]); + self.mutationQueue->AddMutationBatch([FIRTimestamp timestamp], { mutation }); [batches addObject:batch]; } @@ -248,7 +241,7 @@ - (void)testAllMutationBatchesAffectingDocumentKeys { std::vector matches = self.mutationQueue->AllMutationBatchesAffectingDocumentKeys(keys); - [self assertVector:matches matchesExpected:expected]; + FSTAssertEqualVectors(matches, expected); }); } @@ -256,21 +249,21 @@ - (void)testAllMutationBatchesAffectingDocumentKeys_handlesOverlap { if ([self isTestBaseClass]) return; self.persistence.run("testAllMutationBatchesAffectingDocumentKeys_handlesOverlap", [&]() { - NSArray *group1 = @[ - FSTTestSetMutation(@"foo/bar", @{@"a" : @1}), - FSTTestSetMutation(@"foo/baz", @{@"a" : @1}), - ]; + std::vector group1 = { + FSTTestSetMutation(@"foo/bar", @{@"a" : @1}), + FSTTestSetMutation(@"foo/baz", @{@"a" : @1}), + }; FSTMutationBatch *batch1 = - self.mutationQueue->AddMutationBatch([FIRTimestamp timestamp], group1); + self.mutationQueue->AddMutationBatch([FIRTimestamp timestamp], std::move(group1)); - NSArray *group2 = @[ FSTTestSetMutation(@"food/bar", @{@"a" : @1}) ]; - self.mutationQueue->AddMutationBatch([FIRTimestamp timestamp], group2); + std::vector group2 = {FSTTestSetMutation(@"food/bar", @{@"a" : @1})}; + self.mutationQueue->AddMutationBatch([FIRTimestamp timestamp], std::move(group2)); - NSArray *group3 = @[ - FSTTestSetMutation(@"foo/bar", @{@"b" : @1}), - ]; + std::vector group3 = { + FSTTestSetMutation(@"foo/bar", @{@"b" : @1}), + }; FSTMutationBatch *batch3 = - self.mutationQueue->AddMutationBatch([FIRTimestamp timestamp], group3); + self.mutationQueue->AddMutationBatch([FIRTimestamp timestamp], std::move(group3)); DocumentKeySet keys{ Key("foo/bar"), @@ -281,7 +274,7 @@ - (void)testAllMutationBatchesAffectingDocumentKeys_handlesOverlap { std::vector matches = self.mutationQueue->AllMutationBatchesAffectingDocumentKeys(keys); - [self assertVector:matches matchesExpected:expected]; + FSTAssertEqualVectors(matches, expected); }); } @@ -300,7 +293,7 @@ - (void)testAllMutationBatchesAffectingQuery { NSMutableArray *batches = [NSMutableArray array]; for (FSTMutation *mutation in mutations) { FSTMutationBatch *batch = - self.mutationQueue->AddMutationBatch([FIRTimestamp timestamp], @[ mutation ]); + self.mutationQueue->AddMutationBatch([FIRTimestamp timestamp], { mutation }); [batches addObject:batch]; } @@ -309,7 +302,7 @@ - (void)testAllMutationBatchesAffectingQuery { std::vector matches = self.mutationQueue->AllMutationBatchesAffectingQuery(query); - [self assertVector:matches matchesExpected:expected]; + FSTAssertEqualVectors(matches, expected); }); } @@ -327,7 +320,7 @@ - (void)testRemoveMutationBatches { std::vector found; found = self.mutationQueue->AllMutationBatches(); - [self assertVector:found matchesExpected:batches]; + FSTAssertEqualVectors(found, batches); XCTAssertEqual(found.size(), 9); self.mutationQueue->RemoveMutationBatch(batches[0]); @@ -337,7 +330,7 @@ - (void)testRemoveMutationBatches { XCTAssertEqual([self batchCount], 6); found = self.mutationQueue->AllMutationBatches(); - [self assertVector:found matchesExpected:batches]; + FSTAssertEqualVectors(found, batches); XCTAssertEqual(found.size(), 6); self.mutationQueue->RemoveMutationBatch(batches[0]); @@ -345,7 +338,7 @@ - (void)testRemoveMutationBatches { XCTAssertEqual([self batchCount], 5); found = self.mutationQueue->AllMutationBatches(); - [self assertVector:found matchesExpected:batches]; + FSTAssertEqualVectors(found, batches); XCTAssertEqual(found.size(), 5); self.mutationQueue->RemoveMutationBatch(batches[0]); @@ -357,7 +350,7 @@ - (void)testRemoveMutationBatches { XCTAssertEqual([self batchCount], 3); found = self.mutationQueue->AllMutationBatches(); - [self assertVector:found matchesExpected:batches]; + FSTAssertEqualVectors(found, batches); XCTAssertEqual(found.size(), 3); XCTAssertFalse(self.mutationQueue->IsEmpty()); @@ -404,7 +397,7 @@ - (FSTMutationBatch *)addMutationBatchWithKey:(NSString *)key { FSTSetMutation *mutation = FSTTestSetMutation(key, @{@"a" : @1}); FSTMutationBatch *batch = - self.mutationQueue->AddMutationBatch([FIRTimestamp timestamp], @[ mutation ]); + self.mutationQueue->AddMutationBatch([FIRTimestamp timestamp], { mutation }); return batch; } diff --git a/Firestore/Example/Tests/SpecTests/FSTMockDatastore.h b/Firestore/Example/Tests/SpecTests/FSTMockDatastore.h index c9555cd9455..cc1e4335d59 100644 --- a/Firestore/Example/Tests/SpecTests/FSTMockDatastore.h +++ b/Firestore/Example/Tests/SpecTests/FSTMockDatastore.h @@ -78,7 +78,7 @@ class MockDatastore : public Datastore { /** * Returns the next write that was "sent to the backend", failing if there are no queued sent */ - NSArray* NextSentWrite(); + std::vector NextSentWrite(); /** Returns the number of writes that have been sent to the backend but not waited on yet. */ int WritesSent() const; diff --git a/Firestore/Example/Tests/SpecTests/FSTMockDatastore.mm b/Firestore/Example/Tests/SpecTests/FSTMockDatastore.mm index 49bf0b71375..ab25c01e567 100644 --- a/Firestore/Example/Tests/SpecTests/FSTMockDatastore.mm +++ b/Firestore/Example/Tests/SpecTests/FSTMockDatastore.mm @@ -196,7 +196,7 @@ void WriteHandshake() override { callback_->OnWriteStreamHandshakeComplete(); } - void WriteMutations(NSArray* mutations) override { + void WriteMutations(const std::vector& mutations) override { datastore_->IncrementWriteStreamRequests(); sent_mutations_.push(mutations); } @@ -215,10 +215,10 @@ void FailStream(const Status& error) { /** * Returns the next write that was "sent to the backend", failing if there are no queued sent */ - NSArray* NextSentWrite() { + std::vector NextSentWrite() { HARD_ASSERT(!sent_mutations_.empty(), "Writes need to happen before you can call NextSentWrite."); - NSArray* result = std::move(sent_mutations_.front()); + std::vector result = std::move(sent_mutations_.front()); sent_mutations_.pop(); return result; } @@ -233,7 +233,7 @@ int sent_mutations_count() const { private: bool open_ = false; - std::queue*> sent_mutations_; + std::queue> sent_mutations_; MockDatastore* datastore_ = nullptr; WriteStreamCallback* callback_ = nullptr; }; @@ -281,7 +281,7 @@ int sent_mutations_count() const { return watch_stream_->IsOpen(); } -NSArray* MockDatastore::NextSentWrite() { +std::vector MockDatastore::NextSentWrite() { return write_stream_->NextSentWrite(); } diff --git a/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.mm b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.mm index 4f196b15033..2d54a833ca3 100644 --- a/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.mm +++ b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.mm @@ -22,6 +22,7 @@ #include #include #include +#include #import "Firestore/Source/Core/FSTEventManager.h" #import "Firestore/Source/Core/FSTQuery.h" @@ -227,9 +228,9 @@ - (void)shutdown { } - (void)validateNextWriteSent:(FSTMutation *)expectedWrite { - NSArray *request = _datastore->NextSentWrite(); + std::vector request = _datastore->NextSentWrite(); // Make sure the write went through the pipe like we expected it to. - HARD_ASSERT(request.count == 1, "Only single mutation requests are supported at the moment"); + HARD_ASSERT(request.size() == 1, "Only single mutation requests are supported at the moment"); FSTMutation *actualWrite = request[0]; HARD_ASSERT([actualWrite isEqual:expectedWrite], "Mock datastore received write %s but first outstanding mutation was %s", actualWrite, @@ -356,7 +357,7 @@ - (void)writeUserMutation:(FSTMutation *)mutation { [[self currentOutstandingWrites] addObject:write]; LOG_DEBUG("sending a user write."); _workerQueue->EnqueueBlocking([=] { - [self.syncEngine writeMutations:@[ mutation ] + [self.syncEngine writeMutations:{mutation} completion:^(NSError *_Nullable error) { LOG_DEBUG("A callback was called with error: %s", error); write.done = YES; diff --git a/Firestore/Example/Tests/Util/FSTHelpers.h b/Firestore/Example/Tests/Util/FSTHelpers.h index 6e6f57158ab..8f9f4fff6fb 100644 --- a/Firestore/Example/Tests/Util/FSTHelpers.h +++ b/Firestore/Example/Tests/Util/FSTHelpers.h @@ -141,6 +141,15 @@ inline NSString *FSTRemoveExceptionPrefix(NSString *exception) { XCTAssertTrue(didThrow, ##__VA_ARGS__); \ } while (0) +// Helper to compare vectors containing Objective-C objects. +#define FSTAssertEqualVectors(v1, v2) \ + do { \ + XCTAssertEqual(v1.size(), v2.size(), @"Vector length mismatch"); \ + for (size_t i = 0; i < v1.size(); i++) { \ + XCTAssertEqualObjects(v1[i], v2[i]); \ + } \ + } while (0) + /** * An implementation of `TargetMetadataProvider` that provides controlled access to the * `TargetMetadataProvider` callbacks. Any target accessed via these callbacks must be diff --git a/Firestore/Source/API/FIRDocumentReference.mm b/Firestore/Source/API/FIRDocumentReference.mm index 0222843c718..82f915f2d71 100644 --- a/Firestore/Source/API/FIRDocumentReference.mm +++ b/Firestore/Source/API/FIRDocumentReference.mm @@ -174,7 +174,7 @@ - (void)deleteDocument { - (void)deleteDocumentWithCompletion:(nullable void (^)(NSError *_Nullable error))completion { FSTDeleteMutation *mutation = [[FSTDeleteMutation alloc] initWithKey:self.key precondition:Precondition::None()]; - return [self.firestore.client writeMutations:@[ mutation ] completion:completion]; + return [self.firestore.client writeMutations:{mutation} completion:completion]; } - (void)getDocumentWithCompletion:(void (^)(FIRDocumentSnapshot *_Nullable document, diff --git a/Firestore/Source/API/FIRWriteBatch.mm b/Firestore/Source/API/FIRWriteBatch.mm index 8ce58d3077e..484b8b0f4e8 100644 --- a/Firestore/Source/API/FIRWriteBatch.mm +++ b/Firestore/Source/API/FIRWriteBatch.mm @@ -16,7 +16,9 @@ #import "FIRWriteBatch.h" +#include #include +#include #import "Firestore/Source/API/FIRDocumentReference+Internal.h" #import "Firestore/Source/API/FIRFirestore+Internal.h" @@ -41,7 +43,6 @@ @interface FIRWriteBatch () - (instancetype)initWithFirestore:(FIRFirestore *)firestore NS_DESIGNATED_INITIALIZER; @property(nonatomic, strong, readonly) FIRFirestore *firestore; -@property(nonatomic, strong, readonly) NSMutableArray *mutations; @property(nonatomic, assign) BOOL committed; @end @@ -54,13 +55,14 @@ + (instancetype)writeBatchWithFirestore:(FIRFirestore *)firestore { @end -@implementation FIRWriteBatch +@implementation FIRWriteBatch { + std::vector _mutations; +} - (instancetype)initWithFirestore:(FIRFirestore *)firestore { self = [super init]; if (self) { _firestore = firestore; - _mutations = [NSMutableArray array]; } return self; } @@ -75,10 +77,13 @@ - (FIRWriteBatch *)setData:(NSDictionary *)data merge:(BOOL)merge { [self verifyNotCommitted]; [self validateReference:document]; + ParsedSetData parsed = merge ? [self.firestore.dataConverter parsedMergeData:data fieldMask:nil] : [self.firestore.dataConverter parsedSetData:data]; - [self.mutations - addObjectsFromArray:std::move(parsed).ToMutations(document.key, Precondition::None())]; + std::vector append_mutations = + std::move(parsed).ToMutations(document.key, Precondition::None()); + std::move(append_mutations.begin(), append_mutations.end(), std::back_inserter(_mutations)); + return self; } @@ -87,9 +92,12 @@ - (FIRWriteBatch *)setData:(NSDictionary *)data mergeFields:(NSArray *)mergeFields { [self verifyNotCommitted]; [self validateReference:document]; + ParsedSetData parsed = [self.firestore.dataConverter parsedMergeData:data fieldMask:mergeFields]; - [self.mutations - addObjectsFromArray:std::move(parsed).ToMutations(document.key, Precondition::None())]; + std::vector append_mutations = + std::move(parsed).ToMutations(document.key, Precondition::None()); + std::move(append_mutations.begin(), append_mutations.end(), std::back_inserter(_mutations)); + return self; } @@ -97,17 +105,22 @@ - (FIRWriteBatch *)updateData:(NSDictionary *)fields forDocument:(FIRDocumentReference *)document { [self verifyNotCommitted]; [self validateReference:document]; + ParsedUpdateData parsed = [self.firestore.dataConverter parsedUpdateData:fields]; - [self.mutations - addObjectsFromArray:std::move(parsed).ToMutations(document.key, Precondition::Exists(true))]; + std::vector append_mutations = + std::move(parsed).ToMutations(document.key, Precondition::Exists(true)); + std::move(append_mutations.begin(), append_mutations.end(), std::back_inserter(_mutations)); + return self; } - (FIRWriteBatch *)deleteDocument:(FIRDocumentReference *)document { [self verifyNotCommitted]; [self validateReference:document]; - [self.mutations addObject:[[FSTDeleteMutation alloc] initWithKey:document.key - precondition:Precondition::None()]]; + + _mutations.push_back([[FSTDeleteMutation alloc] initWithKey:document.key + precondition:Precondition::None()]); + ; return self; } @@ -118,7 +131,7 @@ - (void)commit { - (void)commitWithCompletion:(nullable void (^)(NSError *_Nullable error))completion { [self verifyNotCommitted]; self.committed = TRUE; - [self.firestore.client writeMutations:self.mutations completion:completion]; + [self.firestore.client writeMutations:std::move(_mutations) completion:completion]; } - (void)verifyNotCommitted { diff --git a/Firestore/Source/Core/FSTFirestoreClient.h b/Firestore/Source/Core/FSTFirestoreClient.h index 1d2e3abe66c..233e7c2d027 100644 --- a/Firestore/Source/Core/FSTFirestoreClient.h +++ b/Firestore/Source/Core/FSTFirestoreClient.h @@ -17,6 +17,7 @@ #import #include +#include #import "Firestore/Source/Core/FSTTypes.h" #import "Firestore/Source/Core/FSTViewSnapshot.h" @@ -100,7 +101,7 @@ NS_ASSUME_NONNULL_BEGIN NSError *_Nullable error))completion; /** Write mutations. completion will be notified when it's written to the backend. */ -- (void)writeMutations:(NSArray *)mutations +- (void)writeMutations:(std::vector &&)mutations completion:(nullable FSTVoidErrorBlock)completion; /** Tries to execute the transaction in updateBlock up to retries times. */ diff --git a/Firestore/Source/Core/FSTFirestoreClient.mm b/Firestore/Source/Core/FSTFirestoreClient.mm index 4de9a6a39f2..5e720f7c90b 100644 --- a/Firestore/Source/Core/FSTFirestoreClient.mm +++ b/Firestore/Source/Core/FSTFirestoreClient.mm @@ -386,15 +386,16 @@ - (void)getDocumentsFromLocalCache:(FIRQuery *)query }); } -- (void)writeMutations:(NSArray *)mutations +- (void)writeMutations:(std::vector &&)mutations completion:(nullable FSTVoidErrorBlock)completion { - _workerQueue->Enqueue([self, mutations, completion] { - if (mutations.count == 0) { + // TODO(c++14): move `mutations` into lambda (C++14). + _workerQueue->Enqueue([self, mutations, completion]() mutable { + if (mutations.empty()) { if (completion) { self->_userExecutor->Execute([=] { completion(nil); }); } } else { - [self.syncEngine writeMutations:mutations + [self.syncEngine writeMutations:std::move(mutations) completion:^(NSError *error) { // Dispatch the result back onto the user dispatch queue. if (completion) { diff --git a/Firestore/Source/Core/FSTSyncEngine.h b/Firestore/Source/Core/FSTSyncEngine.h index c99b06616f4..85e56b2b02b 100644 --- a/Firestore/Source/Core/FSTSyncEngine.h +++ b/Firestore/Source/Core/FSTSyncEngine.h @@ -16,6 +16,8 @@ #import +#include + #import "Firestore/Source/Core/FSTTypes.h" #import "Firestore/Source/Remote/FSTRemoteStore.h" @@ -90,7 +92,8 @@ NS_ASSUME_NONNULL_BEGIN * write caused. The provided completion block will be called once the write has been acked or * rejected by the backend (or failed locally for any other reason). */ -- (void)writeMutations:(NSArray *)mutations completion:(FSTVoidErrorBlock)completion; +- (void)writeMutations:(std::vector &&)mutations + completion:(FSTVoidErrorBlock)completion; /** * Runs the given transaction block up to retries times and then calls completion. diff --git a/Firestore/Source/Core/FSTSyncEngine.mm b/Firestore/Source/Core/FSTSyncEngine.mm index 86794d0fec5..576b2b1c710 100644 --- a/Firestore/Source/Core/FSTSyncEngine.mm +++ b/Firestore/Source/Core/FSTSyncEngine.mm @@ -247,11 +247,11 @@ - (void)stopListeningToQuery:(FSTQuery *)query { [self removeAndCleanupQuery:queryView]; } -- (void)writeMutations:(NSArray *)mutations +- (void)writeMutations:(std::vector &&)mutations completion:(FSTVoidErrorBlock)completion { [self assertDelegateExistsForSelector:_cmd]; - FSTLocalWriteResult *result = [self.localStore locallyWriteMutations:mutations]; + FSTLocalWriteResult *result = [self.localStore locallyWriteMutations:std::move(mutations)]; [self addMutationCompletionBlock:completion batchID:result.batchID]; [self emitNewSnapshotsAndNotifyLocalStoreWithChanges:result.changes remoteEvent:absl::nullopt]; diff --git a/Firestore/Source/Core/FSTTransaction.h b/Firestore/Source/Core/FSTTransaction.h index 352fa7a4f55..64e10607776 100644 --- a/Firestore/Source/Core/FSTTransaction.h +++ b/Firestore/Source/Core/FSTTransaction.h @@ -18,16 +18,10 @@ #include -#import "Firestore/Source/Core/FSTTypes.h" - #include "Firestore/core/src/firebase/firestore/core/user_data.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" #include "Firestore/core/src/firebase/firestore/remote/datastore.h" -@class FSTMaybeDocument; -@class FSTObjectValue; -@class FSTParsedUpdateData; - NS_ASSUME_NONNULL_BEGIN #pragma mark - FSTTransaction diff --git a/Firestore/Source/Core/FSTTransaction.mm b/Firestore/Source/Core/FSTTransaction.mm index 2ee220705cf..34a6c531f3f 100644 --- a/Firestore/Source/Core/FSTTransaction.mm +++ b/Firestore/Source/Core/FSTTransaction.mm @@ -16,17 +16,16 @@ #import "Firestore/Source/Core/FSTTransaction.h" +#include #include #include #include #import "FIRFirestoreErrors.h" -#import "Firestore/Source/API/FSTUserDataConverter.h" #import "Firestore/Source/Model/FSTDocument.h" #import "Firestore/Source/Model/FSTMutation.h" #import "Firestore/Source/Util/FSTUsageValidation.h" -#include "Firestore/core/src/firebase/firestore/model/document_key.h" #include "Firestore/core/src/firebase/firestore/model/document_key_set.h" #include "Firestore/core/src/firebase/firestore/model/precondition.h" #include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" @@ -46,7 +45,6 @@ #pragma mark - FSTTransaction @interface FSTTransaction () -@property(nonatomic, strong, readonly) NSMutableArray *mutations; @property(nonatomic, assign) BOOL commitCalled; /** * An error that may have occurred as a consequence of a write. If set, needs to be raised in the @@ -57,6 +55,7 @@ @interface FSTTransaction () @implementation FSTTransaction { Datastore *_datastore; + std::vector _mutations; std::map _readVersions; } @@ -68,7 +67,6 @@ - (instancetype)initWithDatastore:(Datastore *)datastore { self = [super init]; if (self) { _datastore = datastore; - _mutations = [NSMutableArray array]; _commitCalled = NO; } return self; @@ -113,7 +111,7 @@ - (BOOL)recordVersionForDocument:(FSTMaybeDocument *)doc error:(NSError **)error - (void)lookupDocumentsForKeys:(const std::vector &)keys completion:(FSTVoidMaybeDocumentArrayErrorBlock)completion { [self ensureCommitNotCalled]; - if (self.mutations.count) { + if (!_mutations.empty()) { FSTThrowInvalidUsage(@"FIRIllegalStateException", @"All reads in a transaction must be done before any writes."); } @@ -135,9 +133,9 @@ - (void)lookupDocumentsForKeys:(const std::vector &)keys } /** Stores mutations to be written when commitWithCompletion is called. */ -- (void)writeMutations:(NSArray *)mutations { +- (void)writeMutations:(const std::vector &)mutations { [self ensureCommitNotCalled]; - [self.mutations addObjectsFromArray:mutations]; + std::move(mutations.begin(), mutations.end(), std::back_inserter(_mutations)); } /** @@ -201,9 +199,9 @@ - (void)updateData:(ParsedUpdateData &&)data forDocument:(const DocumentKey &)ke } - (void)deleteDocument:(const DocumentKey &)key { - [self writeMutations:@[ [[FSTDeleteMutation alloc] + [self writeMutations:{[[FSTDeleteMutation alloc] initWithKey:key - precondition:[self preconditionForDocumentKey:key]] ]]; + precondition:[self preconditionForDocumentKey:key]]}]; // Since the delete will be applied before all following writes, we need to ensure that the // precondition for the next write will be exists without timestamp. _readVersions[key] = SnapshotVersion::None(); @@ -226,7 +224,7 @@ - (void)commitWithCompletion:(FSTVoidErrorBlock)completion { unwritten = unwritten.insert(kv.first); }; // For each mutation, note that the doc was written. - for (FSTMutation *mutation in self.mutations) { + for (FSTMutation *mutation : _mutations) { unwritten = unwritten.erase(mutation.key); } if (!unwritten.empty()) { @@ -239,7 +237,7 @@ - (void)commitWithCompletion:(FSTVoidErrorBlock)completion { @"written in that transaction." }]); } else { - _datastore->CommitMutations(self.mutations, ^(NSError *_Nullable error) { + _datastore->CommitMutations(_mutations, ^(NSError *_Nullable error) { if (error) { completion(error); } else { diff --git a/Firestore/Source/Local/FSTLocalDocumentsView.mm b/Firestore/Source/Local/FSTLocalDocumentsView.mm index eb11d3ca0ab..a357f5b5f4f 100644 --- a/Firestore/Source/Local/FSTLocalDocumentsView.mm +++ b/Firestore/Source/Local/FSTLocalDocumentsView.mm @@ -166,7 +166,7 @@ - (DocumentMap)documentsMatchingCollectionQuery:(FSTQuery *)query { _mutationQueue->AllMutationBatchesAffectingQuery(query); for (FSTMutationBatch *batch : matchingBatches) { - for (FSTMutation *mutation in batch.mutations) { + for (FSTMutation *mutation : [batch mutations]) { // Only process documents belonging to the collection. if (!query.path.IsImmediateParentOf(mutation.key.path())) { continue; diff --git a/Firestore/Source/Local/FSTLocalSerializer.mm b/Firestore/Source/Local/FSTLocalSerializer.mm index 5ad70e43c47..528af368266 100644 --- a/Firestore/Source/Local/FSTLocalSerializer.mm +++ b/Firestore/Source/Local/FSTLocalSerializer.mm @@ -17,6 +17,8 @@ #import "Firestore/Source/Local/FSTLocalSerializer.h" #include +#include +#include #import "FIRTimestamp.h" #import "Firestore/Protos/objc/firestore/local/MaybeDocument.pbobjc.h" @@ -182,7 +184,7 @@ - (FSTPBWriteBatch *)encodedMutationBatch:(FSTMutationBatch *)batch { encodedTimestamp:Timestamp{batch.localWriteTime.seconds, batch.localWriteTime.nanoseconds}]; NSMutableArray *writes = proto.writesArray; - for (FSTMutation *mutation in batch.mutations) { + for (FSTMutation *mutation : [batch mutations]) { [writes addObject:[remoteSerializer encodedMutation:mutation]]; } return proto; @@ -192,9 +194,9 @@ - (FSTMutationBatch *)decodedMutationBatch:(FSTPBWriteBatch *)batch { FSTSerializerBeta *remoteSerializer = self.remoteSerializer; int batchID = batch.batchId; - NSMutableArray *mutations = [NSMutableArray array]; + std::vector mutations; for (GCFSWrite *write in batch.writesArray) { - [mutations addObject:[remoteSerializer decodedMutation:write]]; + mutations.push_back([remoteSerializer decodedMutation:write]); } Timestamp localWriteTime = [remoteSerializer decodedTimestamp:batch.localWriteTime]; @@ -203,7 +205,7 @@ - (FSTMutationBatch *)decodedMutationBatch:(FSTPBWriteBatch *)batch { initWithBatchID:batchID localWriteTime:[FIRTimestamp timestampWithSeconds:localWriteTime.seconds() nanoseconds:localWriteTime.nanoseconds()] - mutations:mutations]; + mutations:std::move(mutations)]; } - (FSTPBTarget *)encodedQueryData:(FSTQueryData *)queryData { diff --git a/Firestore/Source/Local/FSTLocalStore.h b/Firestore/Source/Local/FSTLocalStore.h index 21f05b68e4c..8d696fe9c95 100644 --- a/Firestore/Source/Local/FSTLocalStore.h +++ b/Firestore/Source/Local/FSTLocalStore.h @@ -16,6 +16,8 @@ #import +#include + #import "Firestore/Source/Local/FSTLRUGarbageCollector.h" #include "Firestore/core/src/firebase/firestore/auth/user.h" @@ -103,7 +105,7 @@ NS_ASSUME_NONNULL_BEGIN (const firebase::firestore::auth::User &)user; /** Accepts locally generated Mutations and commits them to storage. */ -- (FSTLocalWriteResult *)locallyWriteMutations:(NSArray *)mutations; +- (FSTLocalWriteResult *)locallyWriteMutations:(std::vector &&)mutations; /** Returns the current value of a document with a given key, or nil if not found. */ - (nullable FSTMaybeDocument *)readDocument:(const firebase::firestore::model::DocumentKey &)key; diff --git a/Firestore/Source/Local/FSTLocalStore.mm b/Firestore/Source/Local/FSTLocalStore.mm index 22d9aa283e0..eabe3c58080 100644 --- a/Firestore/Source/Local/FSTLocalStore.mm +++ b/Firestore/Source/Local/FSTLocalStore.mm @@ -152,7 +152,7 @@ - (MaybeDocumentMap)userDidChange:(const User &)user { DocumentKeySet changedKeys; for (const std::vector &batches : {oldBatches, newBatches}) { for (FSTMutationBatch *batch : batches) { - for (FSTMutation *mutation in batch.mutations) { + for (FSTMutation *mutation : [batch mutations]) { changedKeys = changedKeys.insert(mutation.key); } } @@ -163,10 +163,11 @@ - (MaybeDocumentMap)userDidChange:(const User &)user { }); } -- (FSTLocalWriteResult *)locallyWriteMutations:(NSArray *)mutations { +- (FSTLocalWriteResult *)locallyWriteMutations:(std::vector &&)mutations { return self.persistence.run("Locally write mutations", [&]() -> FSTLocalWriteResult * { FIRTimestamp *localWriteTime = [FIRTimestamp timestamp]; - FSTMutationBatch *batch = _mutationQueue->AddMutationBatch(localWriteTime, mutations); + FSTMutationBatch *batch = + _mutationQueue->AddMutationBatch(localWriteTime, std::move(mutations)); DocumentKeySet keys = [batch keys]; MaybeDocumentMap changedDocuments = [self.localDocuments documentsForKeys:keys]; return [FSTLocalWriteResult resultForBatchID:batch.batchID changes:std::move(changedDocuments)]; diff --git a/Firestore/Source/Model/FSTMutationBatch.h b/Firestore/Source/Model/FSTMutationBatch.h index ac8212db47a..64c55dafe96 100644 --- a/Firestore/Source/Model/FSTMutationBatch.h +++ b/Firestore/Source/Model/FSTMutationBatch.h @@ -53,7 +53,7 @@ NS_ASSUME_NONNULL_BEGIN /** Initializes a mutation batch with the given batchID, localWriteTime, and mutations. */ - (instancetype)initWithBatchID:(firebase::firestore::model::BatchId)batchID localWriteTime:(FIRTimestamp *)localWriteTime - mutations:(NSArray *)mutations NS_DESIGNATED_INITIALIZER; + mutations:(std::vector &&)mutations NS_DESIGNATED_INITIALIZER; - (id)init NS_UNAVAILABLE; @@ -83,9 +83,10 @@ NS_ASSUME_NONNULL_BEGIN /** Returns the set of unique keys referenced by all mutations in the batch. */ - (firebase::firestore::model::DocumentKeySet)keys; +- (const std::vector &)mutations; + @property(nonatomic, assign, readonly) firebase::firestore::model::BatchId batchID; @property(nonatomic, strong, readonly) FIRTimestamp *localWriteTime; -@property(nonatomic, strong, readonly) NSArray *mutations; @end diff --git a/Firestore/Source/Model/FSTMutationBatch.mm b/Firestore/Source/Model/FSTMutationBatch.mm index c76c5e3fd2f..2074a79030e 100644 --- a/Firestore/Source/Model/FSTMutationBatch.mm +++ b/Firestore/Source/Model/FSTMutationBatch.mm @@ -16,6 +16,7 @@ #import "Firestore/Source/Model/FSTMutationBatch.h" +#include #include #import "FIRTimestamp.h" @@ -24,6 +25,7 @@ #import "Firestore/Source/Model/FSTMutation.h" #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" +#include "Firestore/core/src/firebase/firestore/util/hashing.h" using firebase::firestore::model::BatchId; using firebase::firestore::model::DocumentKey; @@ -31,24 +33,31 @@ using firebase::firestore::model::DocumentKeySet; using firebase::firestore::model::DocumentVersionMap; using firebase::firestore::model::SnapshotVersion; +using firebase::firestore::util::Hash; NS_ASSUME_NONNULL_BEGIN -@implementation FSTMutationBatch +@implementation FSTMutationBatch { + std::vector _mutations; +} - (instancetype)initWithBatchID:(BatchId)batchID localWriteTime:(FIRTimestamp *)localWriteTime - mutations:(NSArray *)mutations { - HARD_ASSERT(mutations.count != 0, "Cannot create an empty mutation batch"); + mutations:(std::vector &&)mutations { + HARD_ASSERT(!mutations.empty(), "Cannot create an empty mutation batch"); self = [super init]; if (self) { _batchID = batchID; _localWriteTime = localWriteTime; - _mutations = mutations; + _mutations = std::move(mutations); } return self; } +- (const std::vector &)mutations { + return _mutations; +} + - (BOOL)isEqual:(id)other { if (self == other) { return YES; @@ -59,19 +68,27 @@ - (BOOL)isEqual:(id)other { FSTMutationBatch *otherBatch = (FSTMutationBatch *)other; return self.batchID == otherBatch.batchID && [self.localWriteTime isEqual:otherBatch.localWriteTime] && - [self.mutations isEqual:otherBatch.mutations]; + std::equal(_mutations.begin(), _mutations.end(), otherBatch.mutations.begin(), + [](FSTMutation *lhs, FSTMutation *rhs) { return [lhs isEqual:rhs]; }); } - (NSUInteger)hash { NSUInteger result = (NSUInteger)self.batchID; result = result * 31 + self.localWriteTime.hash; - result = result * 31 + self.mutations.hash; + for (FSTMutation *mutation : _mutations) { + result = result * 31 + [mutation hash]; + } return result; } - (NSString *)description { + // TODO(varconst): quick-and-dirty-way to create a readable description. + NSMutableArray *mutationsCopy = [NSMutableArray array]; + for (FSTMutation *mutation : _mutations) { + [mutationsCopy addObject:mutation]; + } return [NSString stringWithFormat:@"", - self.batchID, self.localWriteTime, self.mutations]; + self.batchID, self.localWriteTime, mutationsCopy]; } - (FSTMaybeDocument *_Nullable)applyToRemoteDocument:(FSTMaybeDocument *_Nullable)maybeDoc @@ -82,12 +99,12 @@ - (FSTMaybeDocument *_Nullable)applyToRemoteDocument:(FSTMaybeDocument *_Nullabl "applyTo: key %s doesn't match maybeDoc key %s", documentKey.ToString(), maybeDoc.key.ToString()); - HARD_ASSERT(mutationBatchResult.mutationResults.size() == self.mutations.count, - "Mismatch between mutations length (%s) and results length (%s)", - self.mutations.count, mutationBatchResult.mutationResults.size()); + HARD_ASSERT(mutationBatchResult.mutationResults.size() == _mutations.size(), + "Mismatch between mutations length (%s) and results length (%s)", _mutations.size(), + mutationBatchResult.mutationResults.size()); - for (NSUInteger i = 0; i < self.mutations.count; i++) { - FSTMutation *mutation = self.mutations[i]; + for (size_t i = 0; i < _mutations.size(); i++) { + FSTMutation *mutation = _mutations[i]; FSTMutationResult *mutationResult = mutationBatchResult.mutationResults[i]; if (mutation.key == documentKey) { maybeDoc = [mutation applyToRemoteDocument:maybeDoc mutationResult:mutationResult]; @@ -103,8 +120,7 @@ - (FSTMaybeDocument *_Nullable)applyToLocalDocument:(FSTMaybeDocument *_Nullable maybeDoc.key.ToString()); FSTMaybeDocument *baseDoc = maybeDoc; - for (NSUInteger i = 0; i < self.mutations.count; i++) { - FSTMutation *mutation = self.mutations[i]; + for (FSTMutation *mutation : _mutations) { if (mutation.key == documentKey) { maybeDoc = [mutation applyToLocalDocument:maybeDoc baseDocument:baseDoc @@ -114,10 +130,9 @@ - (FSTMaybeDocument *_Nullable)applyToLocalDocument:(FSTMaybeDocument *_Nullable return maybeDoc; } -// TODO(klimt): This could use NSMutableDictionary instead. - (DocumentKeySet)keys { DocumentKeySet set; - for (FSTMutation *mutation in self.mutations) { + for (FSTMutation *mutation : _mutations) { set = set.insert(mutation.key); } return set; @@ -172,13 +187,13 @@ + (instancetype)resultWithBatch:(FSTMutationBatch *)batch commitVersion:(SnapshotVersion)commitVersion mutationResults:(std::vector)mutationResults streamToken:(nullable NSData *)streamToken { - HARD_ASSERT(batch.mutations.count == mutationResults.size(), - "Mutations sent %s must equal results received %s", batch.mutations.count, + HARD_ASSERT(batch.mutations.size() == mutationResults.size(), + "Mutations sent %s must equal results received %s", batch.mutations.size(), mutationResults.size()); DocumentVersionMap docVersions; - NSArray *mutations = batch.mutations; - for (NSUInteger i = 0; i < mutations.count; i++) { + std::vector mutations = batch.mutations; + for (size_t i = 0; i < mutations.size(); i++) { absl::optional version = mutationResults[i].version; if (!version) { // deletes don't have a version, so we substitute the commitVersion diff --git a/Firestore/core/src/firebase/firestore/core/user_data.h b/Firestore/core/src/firebase/firestore/core/user_data.h index cdb059af5c0..d3c3bf94b1e 100644 --- a/Firestore/core/src/firebase/firestore/core/user_data.h +++ b/Firestore/core/src/firebase/firestore/core/user_data.h @@ -266,7 +266,7 @@ class ParsedSetData { * * This method consumes the values stored in the ParsedSetData */ - NSArray* ToMutations( + std::vector ToMutations( const model::DocumentKey& key, const model::Precondition& precondition) &&; @@ -299,7 +299,7 @@ class ParsedUpdateData { * * This method consumes the values stored in the ParsedUpdateData */ - NSArray* ToMutations( + std::vector ToMutations( const model::DocumentKey& key, const model::Precondition& precondition) &&; diff --git a/Firestore/core/src/firebase/firestore/core/user_data.mm b/Firestore/core/src/firebase/firestore/core/user_data.mm index 525c0ee212d..237e49f234c 100644 --- a/Firestore/core/src/firebase/firestore/core/user_data.mm +++ b/Firestore/core/src/firebase/firestore/core/user_data.mm @@ -210,25 +210,30 @@ patch_{true} { } -NSArray* ParsedSetData::ToMutations( +std::vector ParsedSetData::ToMutations( const DocumentKey& key, const Precondition& precondition) && { - NSMutableArray* mutations = [NSMutableArray array]; + std::vector mutations; if (patch_) { - [mutations - addObject:[[FSTPatchMutation alloc] initWithKey:key - fieldMask:std::move(field_mask_) - value:data_ - precondition:precondition]]; + FSTMutation* mutation = + [[FSTPatchMutation alloc] initWithKey:key + fieldMask:std::move(field_mask_) + value:data_ + precondition:precondition]; + mutations.push_back(mutation); } else { - [mutations addObject:[[FSTSetMutation alloc] initWithKey:key - value:data_ - precondition:precondition]]; + FSTMutation* mutation = [[FSTSetMutation alloc] initWithKey:key + value:data_ + precondition:precondition]; + mutations.push_back(mutation); } + if (!field_transforms_.empty()) { - [mutations - addObject:[[FSTTransformMutation alloc] initWithKey:key - fieldTransforms:field_transforms_]]; + FSTMutation* mutation = + [[FSTTransformMutation alloc] initWithKey:key + fieldTransforms:field_transforms_]; + mutations.push_back(mutation); } + return mutations; } @@ -243,19 +248,24 @@ field_transforms_{std::move(field_transforms)} { } -NSArray* ParsedUpdateData::ToMutations( +std::vector ParsedUpdateData::ToMutations( const DocumentKey& key, const Precondition& precondition) && { - NSMutableArray* mutations = [NSMutableArray array]; - [mutations - addObject:[[FSTPatchMutation alloc] initWithKey:key - fieldMask:std::move(field_mask_) - value:data_ - precondition:precondition]]; + std::vector mutations; + + FSTMutation* mutation = + [[FSTPatchMutation alloc] initWithKey:key + fieldMask:std::move(field_mask_) + value:data_ + precondition:precondition]; + mutations.push_back(mutation); + if (!field_transforms_.empty()) { - [mutations addObject:[[FSTTransformMutation alloc] - initWithKey:key - fieldTransforms:std::move(field_transforms_)]]; + FSTMutation* mutation = + [[FSTTransformMutation alloc] initWithKey:key + fieldTransforms:std::move(field_transforms_)]; + mutations.push_back(mutation); } + return mutations; } diff --git a/Firestore/core/src/firebase/firestore/local/leveldb_mutation_queue.h b/Firestore/core/src/firebase/firestore/local/leveldb_mutation_queue.h index ef205ef276b..c8b00e572ed 100644 --- a/Firestore/core/src/firebase/firestore/local/leveldb_mutation_queue.h +++ b/Firestore/core/src/firebase/firestore/local/leveldb_mutation_queue.h @@ -70,8 +70,9 @@ class LevelDbMutationQueue : public MutationQueue { void AcknowledgeBatch(FSTMutationBatch* batch, NSData* _Nullable stream_token) override; - FSTMutationBatch* AddMutationBatch(FIRTimestamp* local_write_time, - NSArray* mutations) override; + FSTMutationBatch* AddMutationBatch( + FIRTimestamp* local_write_time, + std::vector&& mutations) override; void RemoveMutationBatch(FSTMutationBatch* batch) override; diff --git a/Firestore/core/src/firebase/firestore/local/leveldb_mutation_queue.mm b/Firestore/core/src/firebase/firestore/local/leveldb_mutation_queue.mm index b020c563dab..9592cd724c6 100644 --- a/Firestore/core/src/firebase/firestore/local/leveldb_mutation_queue.mm +++ b/Firestore/core/src/firebase/firestore/local/leveldb_mutation_queue.mm @@ -17,6 +17,7 @@ #include "Firestore/core/src/firebase/firestore/local/leveldb_mutation_queue.h" #include +#include #import "Firestore/Protos/objc/firestore/local/Mutation.pbobjc.h" #import "Firestore/Source/Core/FSTQuery.h" @@ -154,14 +155,14 @@ BatchId LoadNextBatchIdFromDb(DB* db) { } FSTMutationBatch* LevelDbMutationQueue::AddMutationBatch( - FIRTimestamp* local_write_time, NSArray* mutations) { + FIRTimestamp* local_write_time, std::vector&& mutations) { BatchId batch_id = next_batch_id_; next_batch_id_++; FSTMutationBatch* batch = [[FSTMutationBatch alloc] initWithBatchID:batch_id localWriteTime:local_write_time - mutations:mutations]; + mutations:std::move(mutations)]; std::string key = mutation_batch_key(batch_id); db_.currentTransaction->Put(key, [serializer_ encodedMutationBatch:batch]); @@ -171,7 +172,7 @@ BatchId LoadNextBatchIdFromDb(DB* db) { // buffer (and the parser will see all default values). std::string empty_buffer; - for (FSTMutation* mutation in mutations) { + for (FSTMutation* mutation : [batch mutations]) { key = LevelDbDocumentMutationKey::Key(user_id_, mutation.key, batch_id); db_.currentTransaction->Put(key, empty_buffer); } @@ -197,7 +198,7 @@ BatchId LoadNextBatchIdFromDb(DB* db) { db_.currentTransaction->Delete(key); - for (FSTMutation* mutation in batch.mutations) { + for (FSTMutation* mutation : [batch mutations]) { key = LevelDbDocumentMutationKey::Key(user_id_, mutation.key, batch_id); db_.currentTransaction->Delete(key); [db_.referenceDelegate removeMutationReference:mutation.key]; @@ -468,4 +469,4 @@ BatchId LoadNextBatchIdFromDb(DB* db) { } // namespace firestore } // namespace firebase -NS_ASSUME_NONNULL_END \ No newline at end of file +NS_ASSUME_NONNULL_END diff --git a/Firestore/core/src/firebase/firestore/local/memory_mutation_queue.h b/Firestore/core/src/firebase/firestore/local/memory_mutation_queue.h index a47a2899576..cf863d23092 100644 --- a/Firestore/core/src/firebase/firestore/local/memory_mutation_queue.h +++ b/Firestore/core/src/firebase/firestore/local/memory_mutation_queue.h @@ -58,8 +58,9 @@ class MemoryMutationQueue : public MutationQueue { void AcknowledgeBatch(FSTMutationBatch* batch, NSData* _Nullable stream_token) override; - FSTMutationBatch* AddMutationBatch(FIRTimestamp* local_write_time, - NSArray* mutations) override; + FSTMutationBatch* AddMutationBatch( + FIRTimestamp* local_write_time, + std::vector&& mutations) override; void RemoveMutationBatch(FSTMutationBatch* batch) override; diff --git a/Firestore/core/src/firebase/firestore/local/memory_mutation_queue.mm b/Firestore/core/src/firebase/firestore/local/memory_mutation_queue.mm index 5979976d4fb..9f5d9bd89f2 100644 --- a/Firestore/core/src/firebase/firestore/local/memory_mutation_queue.mm +++ b/Firestore/core/src/firebase/firestore/local/memory_mutation_queue.mm @@ -16,6 +16,8 @@ #include "Firestore/core/src/firebase/firestore/local/memory_mutation_queue.h" +#include + #import "Firestore/Protos/objc/firestore/local/Mutation.pbobjc.h" #import "Firestore/Source/Core/FSTQuery.h" #import "Firestore/Source/Local/FSTLocalSerializer.h" @@ -72,8 +74,8 @@ } FSTMutationBatch* MemoryMutationQueue::AddMutationBatch( - FIRTimestamp* local_write_time, NSArray* mutations) { - HARD_ASSERT(mutations.count > 0, "Mutation batches should not be empty"); + FIRTimestamp* local_write_time, std::vector&& mutations) { + HARD_ASSERT(!mutations.empty(), "Mutation batches should not be empty"); BatchId batch_id = next_batch_id_; next_batch_id_++; @@ -87,11 +89,11 @@ FSTMutationBatch* batch = [[FSTMutationBatch alloc] initWithBatchID:batch_id localWriteTime:local_write_time - mutations:mutations]; + mutations:std::move(mutations)]; queue_.push_back(batch); // Track references by document key. - for (FSTMutation* mutation in batch.mutations) { + for (FSTMutation* mutation : [batch mutations]) { batches_by_document_key_ = batches_by_document_key_.insert( DocumentReference{mutation.key, batch_id}); } @@ -109,7 +111,7 @@ queue_.erase(queue_.begin()); // Remove entries from the index too. - for (FSTMutation* mutation in batch.mutations) { + for (FSTMutation* mutation : [batch mutations]) { const DocumentKey& key = mutation.key; [persistence_.referenceDelegate removeMutationReference:key]; diff --git a/Firestore/core/src/firebase/firestore/local/mutation_queue.h b/Firestore/core/src/firebase/firestore/local/mutation_queue.h index 784a7e55f56..3e66a7c082d 100644 --- a/Firestore/core/src/firebase/firestore/local/mutation_queue.h +++ b/Firestore/core/src/firebase/firestore/local/mutation_queue.h @@ -61,7 +61,8 @@ class MutationQueue { /** Creates a new mutation batch and adds it to this mutation queue. */ virtual FSTMutationBatch* AddMutationBatch( - FIRTimestamp* local_write_time, NSArray* mutations) = 0; + FIRTimestamp* local_write_time, + std::vector&& mutations) = 0; /** * Removes the given mutation batch from the queue. This is useful in two diff --git a/Firestore/core/src/firebase/firestore/remote/datastore.h b/Firestore/core/src/firebase/firestore/remote/datastore.h index 1c9c592977a..35938eff336 100644 --- a/Firestore/core/src/firebase/firestore/remote/datastore.h +++ b/Firestore/core/src/firebase/firestore/remote/datastore.h @@ -92,7 +92,7 @@ class Datastore : public std::enable_shared_from_this { virtual std::shared_ptr CreateWriteStream( WriteStreamCallback* callback); - void CommitMutations(NSArray* mutations, + void CommitMutations(const std::vector& mutations, FSTVoidErrorBlock completion); void LookupDocuments(const std::vector& keys, FSTVoidMaybeDocumentArrayErrorBlock completion); @@ -155,9 +155,10 @@ class Datastore : public std::enable_shared_from_this { private: void PollGrpcQueue(); - void CommitMutationsWithCredentials(const auth::Token& token, - NSArray* mutations, - FSTVoidErrorBlock completion); + void CommitMutationsWithCredentials( + const auth::Token& token, + const std::vector& mutations, + FSTVoidErrorBlock completion); void OnCommitMutationsResponse(const util::StatusOr& result, FSTVoidErrorBlock completion); diff --git a/Firestore/core/src/firebase/firestore/remote/datastore.mm b/Firestore/core/src/firebase/firestore/remote/datastore.mm index d7f955abed8..1037a075011 100644 --- a/Firestore/core/src/firebase/firestore/remote/datastore.mm +++ b/Firestore/core/src/firebase/firestore/remote/datastore.mm @@ -164,7 +164,7 @@ void LogGrpcCallFinished(absl::string_view rpc_name, &grpc_connection_, callback); } -void Datastore::CommitMutations(NSArray* mutations, +void Datastore::CommitMutations(const std::vector& mutations, FSTVoidErrorBlock completion) { ResumeRpcWithCredentials( [this, mutations, completion](const StatusOr& maybe_credentials) { @@ -177,9 +177,10 @@ void LogGrpcCallFinished(absl::string_view rpc_name, }); } -void Datastore::CommitMutationsWithCredentials(const Token& token, - NSArray* mutations, - FSTVoidErrorBlock completion) { +void Datastore::CommitMutationsWithCredentials( + const Token& token, + const std::vector& mutations, + FSTVoidErrorBlock completion) { grpc::ByteBuffer message = serializer_bridge_.ToByteBuffer( serializer_bridge_.CreateCommitRequest(mutations)); diff --git a/Firestore/core/src/firebase/firestore/remote/remote_objc_bridge.h b/Firestore/core/src/firebase/firestore/remote/remote_objc_bridge.h index 63f23e03781..7330dbe04c0 100644 --- a/Firestore/core/src/firebase/firestore/remote/remote_objc_bridge.h +++ b/Firestore/core/src/firebase/firestore/remote/remote_objc_bridge.h @@ -110,9 +110,9 @@ class WriteStreamSerializer { GCFSWriteRequest* CreateHandshake() const; GCFSWriteRequest* CreateWriteMutationsRequest( - NSArray* mutations) const; + const std::vector& mutations) const; GCFSWriteRequest* CreateEmptyMutationsList() { - return CreateWriteMutationsRequest(@[]); + return CreateWriteMutationsRequest({}); } static grpc::ByteBuffer ToByteBuffer(GCFSWriteRequest* request); @@ -146,7 +146,7 @@ class DatastoreSerializer { explicit DatastoreSerializer(const core::DatabaseInfo& database_info); GCFSCommitRequest* CreateCommitRequest( - NSArray* mutations) const; + const std::vector& mutations) const; static grpc::ByteBuffer ToByteBuffer(GCFSCommitRequest* request); GCFSBatchGetDocumentsRequest* CreateLookupRequest( diff --git a/Firestore/core/src/firebase/firestore/remote/remote_objc_bridge.mm b/Firestore/core/src/firebase/firestore/remote/remote_objc_bridge.mm index c2f0882b807..169d135b148 100644 --- a/Firestore/core/src/firebase/firestore/remote/remote_objc_bridge.mm +++ b/Firestore/core/src/firebase/firestore/remote/remote_objc_bridge.mm @@ -179,10 +179,10 @@ bool IsLoggingEnabled() { } GCFSWriteRequest* WriteStreamSerializer::CreateWriteMutationsRequest( - NSArray* mutations) const { + const std::vector& mutations) const { NSMutableArray* protos = - [NSMutableArray arrayWithCapacity:mutations.count]; - for (FSTMutation* mutation in mutations) { + [NSMutableArray arrayWithCapacity:mutations.size()]; + for (FSTMutation* mutation : mutations) { [protos addObject:[serializer_ encodedMutation:mutation]]; }; @@ -238,12 +238,12 @@ bool IsLoggingEnabled() { } GCFSCommitRequest* DatastoreSerializer::CreateCommitRequest( - NSArray* mutations) const { + const std::vector& mutations) const { GCFSCommitRequest* request = [GCFSCommitRequest message]; request.database = [serializer_ encodedDatabaseID]; NSMutableArray* mutationProtos = [NSMutableArray array]; - for (FSTMutation* mutation in mutations) { + for (FSTMutation* mutation : mutations) { [mutationProtos addObject:[serializer_ encodedMutation:mutation]]; } request.writesArray = mutationProtos; diff --git a/Firestore/core/src/firebase/firestore/remote/write_stream.h b/Firestore/core/src/firebase/firestore/remote/write_stream.h index 8e85bcf1693..bfa75304ab5 100644 --- a/Firestore/core/src/firebase/firestore/remote/write_stream.h +++ b/Firestore/core/src/firebase/firestore/remote/write_stream.h @@ -131,7 +131,7 @@ class WriteStream : public Stream { virtual void WriteHandshake(); /** Sends a group of mutations to the Firestore backend to apply. */ - virtual void WriteMutations(NSArray* mutations); + virtual void WriteMutations(const std::vector& mutations); protected: // For tests only diff --git a/Firestore/core/src/firebase/firestore/remote/write_stream.mm b/Firestore/core/src/firebase/firestore/remote/write_stream.mm index db778509fda..7e0513a5b47 100644 --- a/Firestore/core/src/firebase/firestore/remote/write_stream.mm +++ b/Firestore/core/src/firebase/firestore/remote/write_stream.mm @@ -65,7 +65,7 @@ // stream token on the handshake, ignoring any stream token we might have. } -void WriteStream::WriteMutations(NSArray* mutations) { +void WriteStream::WriteMutations(const std::vector& mutations) { EnsureOnQueue(); HARD_ASSERT(IsOpen(), "Writing mutations requires an opened stream"); HARD_ASSERT(handshake_complete(), diff --git a/Firestore/core/test/firebase/firestore/remote/datastore_test.mm b/Firestore/core/test/firebase/firestore/remote/datastore_test.mm index b8d376959c2..847865f49f5 100644 --- a/Firestore/core/test/firebase/firestore/remote/datastore_test.mm +++ b/Firestore/core/test/firebase/firestore/remote/datastore_test.mm @@ -174,7 +174,7 @@ void ForceFinishAnyTypeOrder( TEST_F(DatastoreTest, CommitMutationsSuccess) { __block bool done = false; __block NSError* resulting_error = nullptr; - datastore->CommitMutations(@[], ^(NSError* _Nullable error) { + datastore->CommitMutations({}, ^(NSError* _Nullable error) { done = true; resulting_error = error; }); @@ -246,7 +246,7 @@ void ForceFinishAnyTypeOrder( TEST_F(DatastoreTest, CommitMutationsError) { __block bool done = false; __block NSError* resulting_error = nullptr; - datastore->CommitMutations(@[], ^(NSError* _Nullable error) { + datastore->CommitMutations({}, ^(NSError* _Nullable error) { done = true; resulting_error = error; }); @@ -307,7 +307,7 @@ void ForceFinishAnyTypeOrder( credentials.FailGetToken(); __block NSError* resulting_error = nullptr; - datastore->CommitMutations(@[], ^(NSError* _Nullable error) { + datastore->CommitMutations({}, ^(NSError* _Nullable error) { resulting_error = error; }); worker_queue.EnqueueBlocking([] {}); @@ -330,7 +330,7 @@ void ForceFinishAnyTypeOrder( credentials.DelayGetToken(); worker_queue.EnqueueBlocking([&] { - datastore->CommitMutations(@[], ^(NSError* _Nullable error) { + datastore->CommitMutations({}, ^(NSError* _Nullable error) { FAIL() << "Callback shouldn't be invoked"; }); }); @@ -343,7 +343,7 @@ void ForceFinishAnyTypeOrder( credentials.DelayGetToken(); worker_queue.EnqueueBlocking([&] { - datastore->CommitMutations(@[], ^(NSError* _Nullable error) { + datastore->CommitMutations({}, ^(NSError* _Nullable error) { FAIL() << "Callback shouldn't be invoked"; }); });