2 * Copyright 2017 Google
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 #import "FIRMessagingRmq2PersistentStore.h"
21 #import "FIRMessagingConstants.h"
22 #import "FIRMessagingDefines.h"
23 #import "FIRMessagingLogger.h"
24 #import "FIRMessagingPersistentSyncMessage.h"
25 #import "FIRMessagingUtilities.h"
26 #import "NSError+FIRMessaging.h"
27 #import "Protos/GtalkCore.pbobjc.h"
29 #ifndef _FIRMessagingRmqLogAndExit
30 #define _FIRMessagingRmqLogAndExit(stmt, return_value) \
32 [self logErrorAndFinalizeStatement:stmt]; \
33 return return_value; \
37 typedef enum : NSUInteger {
38 FIRMessagingRmqDirectoryUnknown,
39 FIRMessagingRmqDirectoryDocuments,
40 FIRMessagingRmqDirectoryApplicationSupport,
41 } FIRMessagingRmqDirectory;
43 static NSString *const kFCMRmqStoreTag = @"FIRMessagingRmqStore:";
46 NSString *const kTableOutgoingRmqMessages = @"outgoingRmqMessages";
47 NSString *const kTableLastRmqId = @"lastrmqid";
48 NSString *const kOldTableS2DRmqIds = @"s2dRmqIds";
49 NSString *const kTableS2DRmqIds = @"s2dRmqIds_1";
51 // Used to prevent de-duping of sync messages received both via APNS and MCS.
52 NSString *const kTableSyncMessages = @"incomingSyncMessages";
54 static NSString *const kTablePrefix = @"";
57 static NSString *const kCreateTableOutgoingRmqMessages =
58 @"create TABLE IF NOT EXISTS %@%@ "
59 @"(_id INTEGER PRIMARY KEY, "
65 static NSString *const kCreateTableLastRmqId =
66 @"create TABLE IF NOT EXISTS %@%@ "
67 @"(_id INTEGER PRIMARY KEY, "
70 static NSString *const kCreateTableS2DRmqIds =
71 @"create TABLE IF NOT EXISTS %@%@ "
72 @"(_id INTEGER PRIMARY KEY, "
75 static NSString *const kCreateTableSyncMessages =
76 @"create TABLE IF NOT EXISTS %@%@ "
77 @"(_id INTEGER PRIMARY KEY, "
79 @"expiration_ts INTEGER, "
80 @"apns_recv INTEGER, "
83 static NSString *const kDropTableCommand =
84 @"drop TABLE if exists %@%@";
87 static NSString *const kRmqIdColumn = @"rmq_id";
88 static NSString *const kDataColumn = @"data";
89 static NSString *const kProtobufTagColumn = @"type";
90 static NSString *const kIdColumn = @"_id";
92 static NSString *const kOutgoingRmqMessagesColumns = @"rmq_id, type, data";
94 // Sync message columns
95 static NSString *const kSyncMessagesColumns = @"rmq_id, expiration_ts, apns_recv, mcs_recv";
96 // Message time expiration in seconds since 1970
97 static NSString *const kSyncMessageExpirationTimestampColumn = @"expiration_ts";
98 static NSString *const kSyncMessageAPNSReceivedColumn = @"apns_recv";
99 static NSString *const kSyncMessageMCSReceivedColumn = @"mcs_recv";
101 // table data handlers
102 typedef void(^FCMOutgoingRmqMessagesTableHandler)(int64_t rmqId, int8_t tag, NSData *data);
104 // Utility to create an NSString from a sqlite3 result code
105 NSString * _Nonnull FIRMessagingStringFromSQLiteResult(int result) {
106 #pragma clang diagnostic push
107 #pragma clang diagnostic ignored "-Wunguarded-availability"
108 const char *errorStr = sqlite3_errstr(result);
110 NSString *errorString = [NSString stringWithFormat:@"%d - %s", result, errorStr];
114 @interface FIRMessagingRmq2PersistentStore () {
118 @property(nonatomic, readwrite, strong) NSString *databaseName;
119 @property(nonatomic, readwrite, assign) FIRMessagingRmqDirectory currentDirectory;
123 @implementation FIRMessagingRmq2PersistentStore
125 - (instancetype)initWithDatabaseName:(NSString *)databaseName {
128 _databaseName = [databaseName copy];
129 BOOL didMoveToApplicationSupport =
130 [self moveToApplicationSupportSubDirectory:kFIRMessagingApplicationSupportSubDirectory];
132 _currentDirectory = didMoveToApplicationSupport
133 ? FIRMessagingRmqDirectoryApplicationSupport
134 : FIRMessagingRmqDirectoryDocuments;
136 [self openDatabase:_databaseName];
142 sqlite3_close(_database);
145 - (BOOL)moveToApplicationSupportSubDirectory:(NSString *)subDirectoryName {
146 NSArray *directoryPaths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory,
147 NSUserDomainMask, YES);
148 NSString *applicationSupportDirPath = directoryPaths.lastObject;
149 NSArray *components = @[applicationSupportDirPath, subDirectoryName];
150 NSString *subDirectoryPath = [NSString pathWithComponents:components];
151 BOOL hasSubDirectory;
153 if (![[NSFileManager defaultManager] fileExistsAtPath:subDirectoryPath
154 isDirectory:&hasSubDirectory]) {
155 // Cannot move to non-existent directory
159 if ([self doesFileExistInDirectory:FIRMessagingRmqDirectoryDocuments]) {
160 NSString *oldPlistPath = [[self class] pathForDatabase:self.databaseName
161 inDirectory:FIRMessagingRmqDirectoryDocuments];
162 NSString *newPlistPath = [[self class]
163 pathForDatabase:self.databaseName
164 inDirectory:FIRMessagingRmqDirectoryApplicationSupport];
166 if ([self doesFileExistInDirectory:FIRMessagingRmqDirectoryApplicationSupport]) {
167 // File exists in both Documents and ApplicationSupport, delete the one in Documents
168 NSError *deleteError;
169 if (![[NSFileManager defaultManager] removeItemAtPath:oldPlistPath error:&deleteError]) {
170 FIRMessagingLoggerError(kFIRMessagingMessageCodeRmq2PersistentStore000,
171 @"Failed to delete old copy of %@.sqlite in Documents %@",
172 self.databaseName, deleteError);
177 if (![[NSFileManager defaultManager] moveItemAtPath:oldPlistPath
180 FIRMessagingLoggerError(kFIRMessagingMessageCodeRmq2PersistentStore001,
181 @"Failed to move file %@ from %@ to %@. Error: %@", self.databaseName,
182 oldPlistPath, newPlistPath, moveError);
186 // We moved the file if it existed, otherwise we didn't need to do anything
190 - (BOOL)doesFileExistInDirectory:(FIRMessagingRmqDirectory)directory {
191 NSString *path = [[self class] pathForDatabase:self.databaseName inDirectory:directory];
192 return [[NSFileManager defaultManager] fileExistsAtPath:path];
195 + (NSString *)pathForDatabase:(NSString *)dbName inDirectory:(FIRMessagingRmqDirectory)directory {
198 NSString *dbNameWithExtension = [NSString stringWithFormat:@"%@.sqlite", dbName];
199 NSString *errorMessage;
202 case FIRMessagingRmqDirectoryDocuments:
203 paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
204 components = @[paths.lastObject, dbNameWithExtension];
207 case FIRMessagingRmqDirectoryApplicationSupport:
208 paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory,
213 kFIRMessagingApplicationSupportSubDirectory,
219 errorMessage = [NSString stringWithFormat:@"Invalid directory type %lu",
220 (unsigned long)directory];
221 FIRMessagingLoggerError(kFIRMessagingMessageCodeRmq2PersistentStoreInvalidRmqDirectory,
224 NSAssert(NO, errorMessage);
228 return [NSString pathWithComponents:components];
231 - (void)createTableWithName:(NSString *)tableName command:(NSString *)command {
233 NSString *createDatabase = [NSString stringWithFormat:command, kTablePrefix, tableName];
234 if (sqlite3_exec(_database, [createDatabase UTF8String], NULL, NULL, &error) != SQLITE_OK) {
235 // remove db before failing
236 [self removeDatabase];
237 NSString *errorMessage = [NSString stringWithFormat:@"Couldn't create table: %@ %@",
238 kCreateTableOutgoingRmqMessages,
239 [NSString stringWithCString:error encoding:NSUTF8StringEncoding]];
240 FIRMessagingLoggerError(kFIRMessagingMessageCodeRmq2PersistentStoreErrorCreatingTable,
243 NSAssert(NO, errorMessage);
247 - (void)dropTableWithName:(NSString *)tableName {
249 NSString *dropTableSQL = [NSString stringWithFormat:kDropTableCommand, kTablePrefix, tableName];
250 if (sqlite3_exec(_database, [dropTableSQL UTF8String], NULL, NULL, &error) != SQLITE_OK) {
251 FIRMessagingLoggerError(kFIRMessagingMessageCodeRmq2PersistentStore002,
252 @"Failed to remove table %@", tableName);
256 - (void)removeDatabase {
257 NSString *path = [[self class] pathForDatabase:self.databaseName
258 inDirectory:self.currentDirectory];
259 [[NSFileManager defaultManager] removeItemAtPath:path error:nil];
262 + (void)removeDatabase:(NSString *)dbName {
263 NSString *documentsDirPath = [self pathForDatabase:dbName
264 inDirectory:FIRMessagingRmqDirectoryDocuments];
265 NSString *applicationSupportDirPath =
266 [self pathForDatabase:dbName inDirectory:FIRMessagingRmqDirectoryApplicationSupport];
267 [[NSFileManager defaultManager] removeItemAtPath:documentsDirPath error:nil];
268 [[NSFileManager defaultManager] removeItemAtPath:applicationSupportDirPath error:nil];
271 - (void)openDatabase:(NSString *)dbName {
272 NSFileManager *fileManager = [NSFileManager defaultManager];
273 NSString *path = [[self class] pathForDatabase:dbName inDirectory:self.currentDirectory];
275 BOOL didOpenDatabase = YES;
276 if (![fileManager fileExistsAtPath:path]) {
277 // We've to separate between different versions here because of backwards compatbility issues.
278 int result = sqlite3_open([path UTF8String], &_database);
279 if (result != SQLITE_OK) {
280 NSString *errorString = FIRMessagingStringFromSQLiteResult(result);
281 NSString *errorMessage =
282 [NSString stringWithFormat:@"Could not open existing RMQ database at path %@, error: %@",
285 FIRMessagingLoggerError(kFIRMessagingMessageCodeRmq2PersistentStoreErrorOpeningDatabase,
288 NSAssert(NO, errorMessage);
291 [self createTableWithName:kTableOutgoingRmqMessages
292 command:kCreateTableOutgoingRmqMessages];
294 [self createTableWithName:kTableLastRmqId command:kCreateTableLastRmqId];
295 [self createTableWithName:kTableS2DRmqIds command:kCreateTableS2DRmqIds];
297 // Calling sqlite3_open should create the database, since the file doesn't exist.
298 int result = sqlite3_open([path UTF8String], &_database);
299 if (result != SQLITE_OK) {
300 NSString *errorString = FIRMessagingStringFromSQLiteResult(result);
301 NSString *errorMessage =
302 [NSString stringWithFormat:@"Could not create RMQ database at path %@, error: %@",
305 FIRMessagingLoggerError(kFIRMessagingMessageCodeRmq2PersistentStoreErrorCreatingDatabase,
308 NSAssert(NO, errorMessage);
309 didOpenDatabase = NO;
311 [self updateDbWithStringRmqID];
315 if (didOpenDatabase) {
316 [self createTableWithName:kTableSyncMessages command:kCreateTableSyncMessages];
320 - (void)updateDbWithStringRmqID {
321 [self createTableWithName:kTableS2DRmqIds command:kCreateTableS2DRmqIds];
322 [self dropTableWithName:kOldTableS2DRmqIds];
325 #pragma mark - Insert
327 - (BOOL)saveUnackedS2dMessageWithRmqId:(NSString *)rmqId {
328 NSString *insertFormat = @"INSERT INTO %@ (%@) VALUES (?)";
329 NSString *insertSQL = [NSString stringWithFormat:insertFormat,
332 sqlite3_stmt *insert_statement;
333 if (sqlite3_prepare_v2(_database, [insertSQL UTF8String], -1, &insert_statement, NULL)
335 _FIRMessagingRmqLogAndExit(insert_statement, NO);
337 if (sqlite3_bind_text(insert_statement,
341 SQLITE_STATIC) != SQLITE_OK) {
342 _FIRMessagingRmqLogAndExit(insert_statement, NO);
344 if (sqlite3_step(insert_statement) != SQLITE_DONE) {
345 _FIRMessagingRmqLogAndExit(insert_statement, NO);
347 sqlite3_finalize(insert_statement);
351 - (BOOL)saveMessageWithRmqId:(int64_t)rmqId
354 error:(NSError **)error {
355 NSString *insertFormat = @"INSERT INTO %@ (%@, %@, %@) VALUES (?, ?, ?)";
356 NSString *insertSQL = [NSString stringWithFormat:insertFormat,
357 kTableOutgoingRmqMessages, // table
358 kRmqIdColumn, kProtobufTagColumn, kDataColumn /* columns */];
359 sqlite3_stmt *insert_statement;
360 if (sqlite3_prepare_v2(_database, [insertSQL UTF8String], -1, &insert_statement, NULL)
363 *error = [NSError errorWithDomain:[NSString stringWithFormat:@"%s", sqlite3_errmsg(_database)]
364 code:sqlite3_errcode(_database)
367 _FIRMessagingRmqLogAndExit(insert_statement, NO);
369 if (sqlite3_bind_int64(insert_statement, 1, rmqId) != SQLITE_OK) {
370 _FIRMessagingRmqLogAndExit(insert_statement, NO);
372 if (sqlite3_bind_int(insert_statement, 2, tag) != SQLITE_OK) {
373 _FIRMessagingRmqLogAndExit(insert_statement, NO);
375 if (sqlite3_bind_blob(insert_statement, 3, [data bytes], (int)[data length], NULL) != SQLITE_OK) {
376 _FIRMessagingRmqLogAndExit(insert_statement, NO);
378 if (sqlite3_step(insert_statement) != SQLITE_DONE) {
379 _FIRMessagingRmqLogAndExit(insert_statement, NO);
382 sqlite3_finalize(insert_statement);
386 - (int)deleteMessagesFromTable:(NSString *)tableName
387 withRmqIds:(NSArray *)rmqIds {
388 _FIRMessagingDevAssert([tableName isEqualToString:kTableOutgoingRmqMessages] ||
389 [tableName isEqualToString:kTableLastRmqId] ||
390 [tableName isEqualToString:kTableS2DRmqIds] ||
391 [tableName isEqualToString:kTableSyncMessages],
392 @"%@: Invalid Table Name %@", kFCMRmqStoreTag, tableName);
394 BOOL isRmqIDString = NO;
395 // RmqID is a string only for outgoing messages
396 if ([tableName isEqualToString:kTableS2DRmqIds] ||
397 [tableName isEqualToString:kTableSyncMessages]) {
401 NSMutableString *delete = [NSMutableString stringWithFormat:@"DELETE FROM %@ WHERE ", tableName];
403 NSString *toDeleteArgument = [NSString stringWithFormat:@"%@ = ? OR ", kRmqIdColumn];
405 int toDelete = (int)[rmqIds count];
409 int maxBatchSize = 100;
412 while (start < toDelete) {
414 // construct the WHERE argument
415 int end = MIN(start + maxBatchSize, toDelete);
416 NSMutableString *whereArgument = [NSMutableString string];
417 for (int i = start; i < end; i++) {
418 [whereArgument appendString:toDeleteArgument];
420 // remove the last * OR * from argument
421 NSRange range = NSMakeRange([whereArgument length] -4, 4);
422 [whereArgument deleteCharactersInRange:range];
423 NSString *deleteQuery = [NSString stringWithFormat:@"%@ %@", delete, whereArgument];
427 sqlite3_stmt *delete_statement;
428 if (sqlite3_prepare_v2(_database, [deleteQuery UTF8String],
429 -1, &delete_statement, NULL) != SQLITE_OK) {
430 _FIRMessagingRmqLogAndExit(delete_statement, 0);
435 int placeholderIndex = 1; // placeholders in sqlite3 start with 1
436 for (NSString *rmqId in rmqIds) { // objectAtIndex: is O(n) -- would make it slow
437 if (rmqIndex < start) {
440 } else if (rmqIndex >= end) {
444 if (sqlite3_bind_text(delete_statement,
448 SQLITE_STATIC) != SQLITE_OK) {
449 FIRMessagingLoggerDebug(kFIRMessagingMessageCodeRmq2PersistentStore003,
450 @"Failed to bind rmqID %@", rmqId);
454 int64_t rmqIdValue = [rmqId longLongValue];
455 sqlite3_bind_int64(delete_statement, placeholderIndex, rmqIdValue);
461 if (sqlite3_step(delete_statement) != SQLITE_DONE) {
462 _FIRMessagingRmqLogAndExit(delete_statement, deleteCount);
464 sqlite3_finalize(delete_statement);
465 deleteCount += sqlite3_changes(_database);
469 // if we are here all of our sqlite queries should have succeeded
470 FIRMessagingLoggerDebug(kFIRMessagingMessageCodeRmq2PersistentStore004,
471 @"%@ Trying to delete %d s2D ID's, successfully deleted %d",
472 kFCMRmqStoreTag, toDelete, deleteCount);
478 - (int64_t)queryHighestRmqId {
479 NSString *queryFormat = @"SELECT %@ FROM %@ ORDER BY %@ DESC LIMIT %d";
480 NSString *query = [NSString stringWithFormat:queryFormat,
481 kRmqIdColumn, // column
482 kTableOutgoingRmqMessages, // table
483 kRmqIdColumn, // order by column
486 sqlite3_stmt *statement;
487 int64_t highestRmqId = 0;
488 if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &statement, NULL) != SQLITE_OK) {
489 _FIRMessagingRmqLogAndExit(statement, highestRmqId);
491 if (sqlite3_step(statement) == SQLITE_ROW) {
492 highestRmqId = sqlite3_column_int64(statement, 0);
494 sqlite3_finalize(statement);
498 - (int64_t)queryLastRmqId {
499 NSString *queryFormat = @"SELECT %@ FROM %@ ORDER BY %@ DESC LIMIT %d";
500 NSString *query = [NSString stringWithFormat:queryFormat,
501 kRmqIdColumn, // column
502 kTableLastRmqId, // table
503 kRmqIdColumn, // order by column
506 sqlite3_stmt *statement;
507 int64_t lastRmqId = 0;
508 if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &statement, NULL) != SQLITE_OK) {
509 _FIRMessagingRmqLogAndExit(statement, lastRmqId);
511 if (sqlite3_step(statement) == SQLITE_ROW) {
512 lastRmqId = sqlite3_column_int64(statement, 0);
514 sqlite3_finalize(statement);
518 - (BOOL)updateLastOutgoingRmqId:(int64_t)rmqID {
519 NSString *queryFormat = @"INSERT OR REPLACE INTO %@ (%@, %@) VALUES (?, ?)";
520 NSString *query = [NSString stringWithFormat:queryFormat,
521 kTableLastRmqId, // table
522 kIdColumn, kRmqIdColumn]; // columns
523 sqlite3_stmt *statement;
524 if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &statement, NULL) != SQLITE_OK) {
525 _FIRMessagingRmqLogAndExit(statement, NO);
527 if (sqlite3_bind_int(statement, 1, 1) != SQLITE_OK) {
528 _FIRMessagingRmqLogAndExit(statement, NO);
530 if (sqlite3_bind_int64(statement, 2, rmqID) != SQLITE_OK) {
531 _FIRMessagingRmqLogAndExit(statement, NO);
533 if (sqlite3_step(statement) != SQLITE_DONE) {
534 _FIRMessagingRmqLogAndExit(statement, NO);
536 sqlite3_finalize(statement);
540 - (NSArray *)unackedS2dRmqIds {
541 NSString *queryFormat = @"SELECT %@ FROM %@ ORDER BY %@ ASC";
542 NSString *query = [NSString stringWithFormat:queryFormat,
546 sqlite3_stmt *statement;
547 if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &statement, NULL) != SQLITE_OK) {
548 FIRMessagingLoggerDebug(kFIRMessagingMessageCodeRmq2PersistentStore005,
549 @"%@: Could not find s2d ids", kFCMRmqStoreTag);
550 _FIRMessagingRmqLogAndExit(statement, @[]);
552 NSMutableArray *rmqIDArray = [NSMutableArray array];
553 while (sqlite3_step(statement) == SQLITE_ROW) {
554 const char *rmqID = (char *)sqlite3_column_text(statement, 0);
555 [rmqIDArray addObject:[NSString stringWithUTF8String:rmqID]];
557 sqlite3_finalize(statement);
563 - (void)scanOutgoingRmqMessagesWithHandler:(FCMOutgoingRmqMessagesTableHandler)handler {
564 static NSString *queryFormat = @"SELECT %@ FROM %@ WHERE %@ != 0 ORDER BY %@ ASC";
565 NSString *query = [NSString stringWithFormat:queryFormat,
566 kOutgoingRmqMessagesColumns, // select (rmq_id, type, data)
567 kTableOutgoingRmqMessages, // from table
568 kRmqIdColumn, // where
569 kRmqIdColumn]; // order by
570 sqlite3_stmt *statement;
571 if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &statement, NULL) != SQLITE_OK) {
573 sqlite3_finalize(statement);
576 // can query sqlite3 for this but this is fine
577 const int rmqIdColumnNumber = 0;
578 const int typeColumnNumber = 1;
579 const int dataColumnNumber = 2;
580 while (sqlite3_step(statement) == SQLITE_ROW) {
581 int64_t rmqId = sqlite3_column_int64(statement, rmqIdColumnNumber);
582 int8_t type = sqlite3_column_int(statement, typeColumnNumber);
583 const void *bytes = sqlite3_column_blob(statement, dataColumnNumber);
584 int length = sqlite3_column_bytes(statement, dataColumnNumber);
585 _FIRMessagingDevAssert(bytes != NULL,
586 @"%@ Message with no data being stored in Rmq",
588 NSData *data = [NSData dataWithBytes:bytes length:length];
589 handler(rmqId, type, data);
591 sqlite3_finalize(statement);
594 #pragma mark - Sync Messages
596 - (FIRMessagingPersistentSyncMessage *)querySyncMessageWithRmqID:(NSString *)rmqID {
597 _FIRMessagingDevAssert([rmqID length], @"Invalid rmqID key %@ to search in SYNC_RMQ", rmqID);
599 NSString *queryFormat = @"SELECT %@ FROM %@ WHERE %@ = '%@'";
600 NSString *query = [NSString stringWithFormat:queryFormat,
601 kSyncMessagesColumns, // SELECT (rmq_id, expiration_ts, apns_recv, mcs_recv)
602 kTableSyncMessages, // FROM sync_rmq
603 kRmqIdColumn, // WHERE rmq_id
607 if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &stmt, NULL) != SQLITE_OK) {
609 sqlite3_finalize(stmt);
613 const int rmqIDColumn = 0;
614 const int expirationTimestampColumn = 1;
615 const int apnsReceivedColumn = 2;
616 const int mcsReceivedColumn = 3;
619 FIRMessagingPersistentSyncMessage *persistentMessage;
621 while (sqlite3_step(stmt) == SQLITE_ROW) {
623 [NSString stringWithUTF8String:(char *)sqlite3_column_text(stmt, rmqIDColumn)];
624 int64_t expirationTimestamp = sqlite3_column_int64(stmt, expirationTimestampColumn);
625 BOOL apnsReceived = sqlite3_column_int(stmt, apnsReceivedColumn);
626 BOOL mcsReceived = sqlite3_column_int(stmt, mcsReceivedColumn);
628 // create a new persistent message
630 [[FIRMessagingPersistentSyncMessage alloc] initWithRMQID:rmqID expirationTime:expirationTimestamp];
631 persistentMessage.apnsReceived = apnsReceived;
632 persistentMessage.mcsReceived = mcsReceived;
636 sqlite3_finalize(stmt);
638 _FIRMessagingDevAssert(count <= 1, @"Found multiple messages in %@ with same RMQ ID", kTableSyncMessages);
639 return persistentMessage;
642 - (BOOL)deleteSyncMessageWithRmqID:(NSString *)rmqID {
643 _FIRMessagingDevAssert([rmqID length], @"Invalid rmqID key %@ to delete in SYNC_RMQ", rmqID);
644 return [self deleteMessagesFromTable:kTableSyncMessages withRmqIds:@[rmqID]] > 0;
647 - (int)deleteExpiredOrFinishedSyncMessages:(NSError *__autoreleasing *)error {
648 int64_t now = FIRMessagingCurrentTimestampInSeconds();
649 NSString *deleteSQL = @"DELETE FROM %@ "
650 @"WHERE %@ < %lld OR " // expirationTime < now
651 @"(%@ = 1 AND %@ = 1)"; // apns_received = 1 AND mcs_received = 1
652 NSString *query = [NSString stringWithFormat:deleteSQL,
654 kSyncMessageExpirationTimestampColumn,
656 kSyncMessageAPNSReceivedColumn,
657 kSyncMessageMCSReceivedColumn];
659 NSString *errorReason = @"Failed to save delete expired sync messages from store.";
662 if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &stmt, NULL) != SQLITE_OK) {
664 *error = [NSError fcm_errorWithCode:sqlite3_errcode(_database)
665 userInfo:@{ @"error" : errorReason }];
667 _FIRMessagingRmqLogAndExit(stmt, 0);
670 if (sqlite3_step(stmt) != SQLITE_DONE) {
672 *error = [NSError fcm_errorWithCode:sqlite3_errcode(_database)
673 userInfo:@{ @"error" : errorReason }];
675 _FIRMessagingRmqLogAndExit(stmt, 0);
678 sqlite3_finalize(stmt);
679 int deleteCount = sqlite3_changes(_database);
683 - (BOOL)saveSyncMessageWithRmqID:(NSString *)rmqID
684 expirationTime:(int64_t)expirationTime
685 apnsReceived:(BOOL)apnsReceived
686 mcsReceived:(BOOL)mcsReceived
687 error:(NSError **)error {
688 _FIRMessagingDevAssert([rmqID length], @"Invalid nil message to persist to SYNC_RMQ");
690 NSString *insertFormat = @"INSERT INTO %@ (%@, %@, %@, %@) VALUES (?, ?, ?, ?)";
691 NSString *insertSQL = [NSString stringWithFormat:insertFormat,
692 kTableSyncMessages, // Table name
693 kRmqIdColumn, // rmq_id
694 kSyncMessageExpirationTimestampColumn, // expiration_ts
695 kSyncMessageAPNSReceivedColumn, // apns_recv
696 kSyncMessageMCSReceivedColumn /* mcs_recv */];
700 if (sqlite3_prepare_v2(_database, [insertSQL UTF8String], -1, &stmt, NULL) != SQLITE_OK) {
702 *error = [NSError fcm_errorWithCode:sqlite3_errcode(_database)
703 userInfo:@{ @"error" : @"Failed to save sync message to store." }];
705 _FIRMessagingRmqLogAndExit(stmt, NO);
708 if (sqlite3_bind_text(stmt, 1, [rmqID UTF8String], (int)[rmqID length], NULL) != SQLITE_OK) {
709 _FIRMessagingRmqLogAndExit(stmt, NO);
712 if (sqlite3_bind_int64(stmt, 2, expirationTime) != SQLITE_OK) {
713 _FIRMessagingRmqLogAndExit(stmt, NO);
716 if (sqlite3_bind_int(stmt, 3, apnsReceived ? 1 : 0) != SQLITE_OK) {
717 _FIRMessagingRmqLogAndExit(stmt, NO);
720 if (sqlite3_bind_int(stmt, 4, mcsReceived ? 1 : 0) != SQLITE_OK) {
721 _FIRMessagingRmqLogAndExit(stmt, NO);
724 if (sqlite3_step(stmt) != SQLITE_DONE) {
725 _FIRMessagingRmqLogAndExit(stmt, NO);
728 sqlite3_finalize(stmt);
732 - (BOOL)updateSyncMessageViaAPNSWithRmqID:(NSString *)rmqID
733 error:(NSError **)error {
734 return [self updateSyncMessageWithRmqID:rmqID
735 column:kSyncMessageAPNSReceivedColumn
740 - (BOOL)updateSyncMessageViaMCSWithRmqID:(NSString *)rmqID
741 error:(NSError *__autoreleasing *)error {
742 return [self updateSyncMessageWithRmqID:rmqID
743 column:kSyncMessageMCSReceivedColumn
748 - (BOOL)updateSyncMessageWithRmqID:(NSString *)rmqID
749 column:(NSString *)column
751 error:(NSError **)error {
752 _FIRMessagingDevAssert([column isEqualToString:kSyncMessageAPNSReceivedColumn] ||
753 [column isEqualToString:kSyncMessageMCSReceivedColumn],
754 @"Invalid column name %@ for SYNC_RMQ", column);
755 NSString *queryFormat = @"UPDATE %@ " // Table name
756 @"SET %@ = %d " // column=value
757 @"WHERE %@ = ?"; // condition
758 NSString *query = [NSString stringWithFormat:queryFormat,
765 if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &stmt, NULL) != SQLITE_OK) {
767 *error = [NSError fcm_errorWithCode:sqlite3_errcode(_database)
768 userInfo:@{ @"error" : @"Failed to update sync message"}];
770 _FIRMessagingRmqLogAndExit(stmt, NO);
773 if (sqlite3_bind_text(stmt, 1, [rmqID UTF8String], (int)[rmqID length], NULL) != SQLITE_OK) {
774 _FIRMessagingRmqLogAndExit(stmt, NO);
777 if (sqlite3_step(stmt) != SQLITE_DONE) {
778 _FIRMessagingRmqLogAndExit(stmt, NO);
781 sqlite3_finalize(stmt);
786 #pragma mark - Private
788 - (NSString *)lastErrorMessage {
789 return [NSString stringWithFormat:@"%s", sqlite3_errmsg(_database)];
792 - (int)lastErrorCode {
793 return sqlite3_errcode(_database);
797 FIRMessagingLoggerError(kFIRMessagingMessageCodeRmq2PersistentStore006,
798 @"%@ error: code (%d) message: %@", kFCMRmqStoreTag, [self lastErrorCode],
799 [self lastErrorMessage]);
802 - (void)logErrorAndFinalizeStatement:(sqlite3_stmt *)stmt {
804 sqlite3_finalize(stmt);