aboutsummaryrefslogtreecommitdiffhomepage
path: root/Firebase/Messaging/FIRMessagingRmq2PersistentStore.m
diff options
context:
space:
mode:
authorGravatar Paul Beusterien <paulbeusterien@google.com>2017-05-15 12:27:07 -0700
committerGravatar Paul Beusterien <paulbeusterien@google.com>2017-05-15 12:27:07 -0700
commit98ba64449a632518bd2b86fe8d927f4a960d3ddc (patch)
tree131d9c4272fa6179fcda6c5a33fcb3b1bd57ad2e /Firebase/Messaging/FIRMessagingRmq2PersistentStore.m
parent32461366c9e204a527ca05e6e9b9404a2454ac51 (diff)
Initial
Diffstat (limited to 'Firebase/Messaging/FIRMessagingRmq2PersistentStore.m')
-rw-r--r--Firebase/Messaging/FIRMessagingRmq2PersistentStore.m770
1 files changed, 770 insertions, 0 deletions
diff --git a/Firebase/Messaging/FIRMessagingRmq2PersistentStore.m b/Firebase/Messaging/FIRMessagingRmq2PersistentStore.m
new file mode 100644
index 0000000..9edac40
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingRmq2PersistentStore.m
@@ -0,0 +1,770 @@
+/*
+ * 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 "FIRMessagingRmq2PersistentStore.h"
+
+#import "sqlite3.h"
+
+#import "Protos/GtalkCore.pbobjc.h"
+
+#import "FIRMessagingConstants.h"
+#import "FIRMessagingDefines.h"
+#import "FIRMessagingLogger.h"
+#import "FIRMessagingPersistentSyncMessage.h"
+#import "FIRMessagingUtilities.h"
+#import "NSError+FIRMessaging.h"
+
+#ifndef _FIRMessagingRmqLogAndExit
+#define _FIRMessagingRmqLogAndExit(stmt, return_value) \
+do { \
+[self logErrorAndFinalizeStatement:stmt]; \
+return return_value; \
+} while(0)
+#endif
+
+typedef enum : NSUInteger {
+ FIRMessagingRmqDirectoryUnknown,
+ FIRMessagingRmqDirectoryDocuments,
+ FIRMessagingRmqDirectoryApplicationSupport,
+} FIRMessagingRmqDirectory;
+
+static NSString *const kFCMRmqStoreTag = @"FIRMessagingRmqStore:";
+
+// table names
+NSString *const kTableOutgoingRmqMessages = @"outgoingRmqMessages";
+NSString *const kTableLastRmqId = @"lastrmqid";
+NSString *const kOldTableS2DRmqIds = @"s2dRmqIds";
+NSString *const kTableS2DRmqIds = @"s2dRmqIds_1";
+
+// Used to prevent de-duping of sync messages received both via APNS and MCS.
+NSString *const kTableSyncMessages = @"incomingSyncMessages";
+
+static NSString *const kTablePrefix = @"";
+
+// create tables
+static NSString *const kCreateTableOutgoingRmqMessages =
+ @"create TABLE IF NOT EXISTS %@%@ "
+ @"(_id INTEGER PRIMARY KEY, "
+ @"rmq_id INTEGER, "
+ @"type INTEGER, "
+ @"ts INTEGER, "
+ @"data BLOB)";
+
+static NSString *const kCreateTableLastRmqId =
+ @"create TABLE IF NOT EXISTS %@%@ "
+ @"(_id INTEGER PRIMARY KEY, "
+ @"rmq_id INTEGER)";
+
+static NSString *const kCreateTableS2DRmqIds =
+ @"create TABLE IF NOT EXISTS %@%@ "
+ @"(_id INTEGER PRIMARY KEY, "
+ @"rmq_id TEXT)";
+
+static NSString *const kCreateTableSyncMessages =
+ @"create TABLE IF NOT EXISTS %@%@ "
+ @"(_id INTEGER PRIMARY KEY, "
+ @"rmq_id TEXT, "
+ @"expiration_ts INTEGER, "
+ @"apns_recv INTEGER, "
+ @"mcs_recv INTEGER)";
+
+static NSString *const kDropTableCommand =
+ @"drop TABLE if exists %@%@";
+
+// table infos
+static NSString *const kRmqIdColumn = @"rmq_id";
+static NSString *const kDataColumn = @"data";
+static NSString *const kProtobufTagColumn = @"type";
+static NSString *const kIdColumn = @"_id";
+
+static NSString *const kOutgoingRmqMessagesColumns = @"rmq_id, type, data";
+
+// Sync message columns
+static NSString *const kSyncMessagesColumns = @"rmq_id, expiration_ts, apns_recv, mcs_recv";
+// Message time expiration in seconds since 1970
+static NSString *const kSyncMessageExpirationTimestampColumn = @"expiration_ts";
+static NSString *const kSyncMessageAPNSReceivedColumn = @"apns_recv";
+static NSString *const kSyncMessageMCSReceivedColumn = @"mcs_recv";
+
+// table data handlers
+typedef void(^FCMOutgoingRmqMessagesTableHandler)(int64_t rmqId, int8_t tag, NSData *data);
+
+@interface FIRMessagingRmq2PersistentStore () {
+ sqlite3 *_database;
+}
+
+@property(nonatomic, readwrite, strong) NSString *databaseName;
+@property(nonatomic, readwrite, assign) FIRMessagingRmqDirectory currentDirectory;
+
+@end
+
+@implementation FIRMessagingRmq2PersistentStore
+
+- (instancetype)initWithDatabaseName:(NSString *)databaseName {
+ self = [super init];
+ if (self) {
+ _databaseName = [databaseName copy];
+ BOOL didMoveToApplicationSupport =
+ [self moveToApplicationSupportSubDirectory:kFIRMessagingApplicationSupportSubDirectory];
+
+ _currentDirectory = didMoveToApplicationSupport
+ ? FIRMessagingRmqDirectoryApplicationSupport
+ : FIRMessagingRmqDirectoryDocuments;
+
+ [self openDatabase:_databaseName];
+ }
+ return self;
+}
+
+- (void)dealloc {
+ sqlite3_close(_database);
+}
+
+- (BOOL)moveToApplicationSupportSubDirectory:(NSString *)subDirectoryName {
+ NSArray *directoryPaths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory,
+ NSUserDomainMask, YES);
+ NSString *applicationSupportDirPath = directoryPaths.lastObject;
+ NSArray *components = @[applicationSupportDirPath, subDirectoryName];
+ NSString *subDirectoryPath = [NSString pathWithComponents:components];
+ BOOL hasSubDirectory;
+
+ if (![[NSFileManager defaultManager] fileExistsAtPath:subDirectoryPath
+ isDirectory:&hasSubDirectory]) {
+ // Cannot move to non-existent directory
+ return NO;
+ }
+
+ if ([self doesFileExistInDirectory:FIRMessagingRmqDirectoryDocuments]) {
+ NSString *oldPlistPath = [[self class] pathForDatabase:self.databaseName
+ inDirectory:FIRMessagingRmqDirectoryDocuments];
+ NSString *newPlistPath = [[self class]
+ pathForDatabase:self.databaseName
+ inDirectory:FIRMessagingRmqDirectoryApplicationSupport];
+
+ if ([self doesFileExistInDirectory:FIRMessagingRmqDirectoryApplicationSupport]) {
+ // File exists in both Documents and ApplicationSupport, delete the one in Documents
+ NSError *deleteError;
+ if (![[NSFileManager defaultManager] removeItemAtPath:oldPlistPath error:&deleteError]) {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeRmq2PersistentStore000,
+ @"Failed to delete old copy of %@.sqlite in Documents %@",
+ self.databaseName, deleteError);
+ }
+ return NO;
+ }
+ NSError *moveError;
+ if (![[NSFileManager defaultManager] moveItemAtPath:oldPlistPath
+ toPath:newPlistPath
+ error:&moveError]) {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeRmq2PersistentStore001,
+ @"Failed to move file %@ from %@ to %@. Error: %@", self.databaseName,
+ oldPlistPath, newPlistPath, moveError);
+ return NO;
+ }
+ }
+ // We moved the file if it existed, otherwise we didn't need to do anything
+ return YES;
+}
+
+- (BOOL)doesFileExistInDirectory:(FIRMessagingRmqDirectory)directory {
+ NSString *path = [[self class] pathForDatabase:self.databaseName inDirectory:directory];
+ return [[NSFileManager defaultManager] fileExistsAtPath:path];
+}
+
++ (NSString *)pathForDatabase:(NSString *)dbName inDirectory:(FIRMessagingRmqDirectory)directory {
+ NSArray *paths;
+ NSArray *components;
+ NSString *dbNameWithExtension = [NSString stringWithFormat:@"%@.sqlite", dbName];
+
+ switch (directory) {
+ case FIRMessagingRmqDirectoryDocuments:
+ paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
+ components = @[paths.lastObject, dbNameWithExtension];
+ break;
+
+ case FIRMessagingRmqDirectoryApplicationSupport:
+ paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory,
+ NSUserDomainMask,
+ YES);
+ components = @[
+ paths.lastObject,
+ kFIRMessagingApplicationSupportSubDirectory,
+ dbNameWithExtension
+ ];
+ break;
+
+ default:
+ FIRMessaging_FAIL(@"Invalid directory type %zd", directory);
+ break;
+ }
+
+ return [NSString pathWithComponents:components];
+}
+
+- (void)createTableWithName:(NSString *)tableName command:(NSString *)command {
+ char *error;
+ NSString *createDatabase = [NSString stringWithFormat:command, kTablePrefix, tableName];
+ if (sqlite3_exec(_database, [createDatabase UTF8String], NULL, NULL, &error) != SQLITE_OK) {
+ // remove db before failing
+ [self removeDatabase];
+ FIRMessaging_FAIL(@"Couldn't create table: %@ %@",
+ kCreateTableOutgoingRmqMessages,
+ [NSString stringWithCString:error encoding:NSUTF8StringEncoding]);
+ }
+}
+
+- (void)dropTableWithName:(NSString *)tableName {
+ char *error;
+ NSString *dropTableSQL = [NSString stringWithFormat:kDropTableCommand, kTablePrefix, tableName];
+ if (sqlite3_exec(_database, [dropTableSQL UTF8String], NULL, NULL, &error) != SQLITE_OK) {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeRmq2PersistentStore002,
+ @"Failed to remove table %@", tableName);
+ }
+}
+
+- (void)removeDatabase {
+ NSString *path = [[self class] pathForDatabase:self.databaseName
+ inDirectory:self.currentDirectory];
+ [[NSFileManager defaultManager] removeItemAtPath:path error:nil];
+}
+
++ (void)removeDatabase:(NSString *)dbName {
+ NSString *documentsDirPath = [self pathForDatabase:dbName
+ inDirectory:FIRMessagingRmqDirectoryDocuments];
+ NSString *applicationSupportDirPath =
+ [self pathForDatabase:dbName inDirectory:FIRMessagingRmqDirectoryApplicationSupport];
+ [[NSFileManager defaultManager] removeItemAtPath:documentsDirPath error:nil];
+ [[NSFileManager defaultManager] removeItemAtPath:applicationSupportDirPath error:nil];
+}
+
+- (void)openDatabase:(NSString *)dbName {
+ NSFileManager *fileManager = [NSFileManager defaultManager];
+ NSString *path = [[self class] pathForDatabase:dbName inDirectory:self.currentDirectory];
+
+ BOOL didOpenDatabase = YES;
+ if (![fileManager fileExistsAtPath:path]) {
+ // We've to separate between different versions here because of backwards compatbility issues.
+ if (sqlite3_open([path UTF8String], &_database) != SQLITE_OK) {
+ FIRMessaging_FAIL(@"%@ Could not open rmq database: %@", kFCMRmqStoreTag, path);
+ didOpenDatabase = NO;
+ return;
+ }
+ [self createTableWithName:kTableOutgoingRmqMessages
+ command:kCreateTableOutgoingRmqMessages];
+
+ [self createTableWithName:kTableLastRmqId command:kCreateTableLastRmqId];
+ [self createTableWithName:kTableS2DRmqIds command:kCreateTableS2DRmqIds];
+ } else {
+ if (sqlite3_open([path UTF8String], &_database) != SQLITE_OK) {
+ FIRMessaging_FAIL(@"%@ Could not open rmq database: %@", kFCMRmqStoreTag, path);
+ didOpenDatabase = NO;
+ } else {
+ [self updateDbWithStringRmqID];
+ }
+ }
+
+ if (didOpenDatabase) {
+ [self createTableWithName:kTableSyncMessages command:kCreateTableSyncMessages];
+ }
+}
+
+- (void)updateDbWithStringRmqID {
+ [self createTableWithName:kTableS2DRmqIds command:kCreateTableS2DRmqIds];
+ [self dropTableWithName:kOldTableS2DRmqIds];
+}
+
+#pragma mark - Insert
+
+- (BOOL)saveUnackedS2dMessageWithRmqId:(NSString *)rmqId {
+ NSString *insertFormat = @"INSERT INTO %@ (%@) VALUES (?)";
+ NSString *insertSQL = [NSString stringWithFormat:insertFormat,
+ kTableS2DRmqIds,
+ kRmqIdColumn];
+ sqlite3_stmt *insert_statement;
+ if (sqlite3_prepare_v2(_database, [insertSQL UTF8String], -1, &insert_statement, NULL)
+ != SQLITE_OK) {
+ _FIRMessagingRmqLogAndExit(insert_statement, NO);
+ }
+ if (sqlite3_bind_text(insert_statement,
+ 1,
+ [rmqId UTF8String],
+ (int)[rmqId length],
+ SQLITE_STATIC) != SQLITE_OK) {
+ _FIRMessagingRmqLogAndExit(insert_statement, NO);
+ }
+ if (sqlite3_step(insert_statement) != SQLITE_DONE) {
+ _FIRMessagingRmqLogAndExit(insert_statement, NO);
+ }
+ sqlite3_finalize(insert_statement);
+ return YES;
+}
+
+- (BOOL)saveMessageWithRmqId:(int64_t)rmqId
+ tag:(int8_t)tag
+ data:(NSData *)data
+ error:(NSError **)error {
+ NSString *insertFormat = @"INSERT INTO %@ (%@, %@, %@) VALUES (?, ?, ?)";
+ NSString *insertSQL = [NSString stringWithFormat:insertFormat,
+ kTableOutgoingRmqMessages, // table
+ kRmqIdColumn, kProtobufTagColumn, kDataColumn /* columns */];
+ sqlite3_stmt *insert_statement;
+ if (sqlite3_prepare_v2(_database, [insertSQL UTF8String], -1, &insert_statement, NULL)
+ != SQLITE_OK) {
+ if (error) {
+ *error = [NSError errorWithDomain:[NSString stringWithFormat:@"%s", sqlite3_errmsg(_database)]
+ code:sqlite3_errcode(_database)
+ userInfo:nil];
+ }
+ _FIRMessagingRmqLogAndExit(insert_statement, NO);
+ }
+ if (sqlite3_bind_int64(insert_statement, 1, rmqId) != SQLITE_OK) {
+ _FIRMessagingRmqLogAndExit(insert_statement, NO);
+ }
+ if (sqlite3_bind_int(insert_statement, 2, tag) != SQLITE_OK) {
+ _FIRMessagingRmqLogAndExit(insert_statement, NO);
+ }
+ if (sqlite3_bind_blob(insert_statement, 3, [data bytes], (int)[data length], NULL) != SQLITE_OK) {
+ _FIRMessagingRmqLogAndExit(insert_statement, NO);
+ }
+ if (sqlite3_step(insert_statement) != SQLITE_DONE) {
+ _FIRMessagingRmqLogAndExit(insert_statement, NO);
+ }
+
+ sqlite3_finalize(insert_statement);
+ return YES;
+}
+
+- (int)deleteMessagesFromTable:(NSString *)tableName
+ withRmqIds:(NSArray *)rmqIds {
+ _FIRMessagingDevAssert([tableName isEqualToString:kTableOutgoingRmqMessages] ||
+ [tableName isEqualToString:kTableLastRmqId] ||
+ [tableName isEqualToString:kTableS2DRmqIds] ||
+ [tableName isEqualToString:kTableSyncMessages],
+ @"%@: Invalid Table Name %@", kFCMRmqStoreTag, tableName);
+
+ BOOL isRmqIDString = NO;
+ // RmqID is a string only for outgoing messages
+ if ([tableName isEqualToString:kTableS2DRmqIds] ||
+ [tableName isEqualToString:kTableSyncMessages]) {
+ isRmqIDString = YES;
+ }
+
+ NSMutableString *delete = [NSMutableString stringWithFormat:@"DELETE FROM %@ WHERE ", tableName];
+
+ NSString *toDeleteArgument = [NSString stringWithFormat:@"%@ = ? OR ", kRmqIdColumn];
+
+ int toDelete = (int)[rmqIds count];
+ if (toDelete == 0) {
+ return 0;
+ }
+ int maxBatchSize = 100;
+ int start = 0;
+ int deleteCount = 0;
+ while (start < toDelete) {
+
+ // construct the WHERE argument
+ int end = MIN(start + maxBatchSize, toDelete);
+ NSMutableString *whereArgument = [NSMutableString string];
+ for (int i = start; i < end; i++) {
+ [whereArgument appendString:toDeleteArgument];
+ }
+ // remove the last * OR * from argument
+ NSRange range = NSMakeRange([whereArgument length] -4, 4);
+ [whereArgument deleteCharactersInRange:range];
+ NSString *deleteQuery = [NSString stringWithFormat:@"%@ %@", delete, whereArgument];
+
+
+ // sqlite update
+ sqlite3_stmt *delete_statement;
+ if (sqlite3_prepare_v2(_database, [deleteQuery UTF8String],
+ -1, &delete_statement, NULL) != SQLITE_OK) {
+ _FIRMessagingRmqLogAndExit(delete_statement, 0);
+ }
+
+ // bind values
+ int rmqIndex = 0;
+ int placeholderIndex = 1; // placeholders in sqlite3 start with 1
+ for (NSString *rmqId in rmqIds) { // objectAtIndex: is O(n) -- would make it slow
+ if (rmqIndex < start) {
+ rmqIndex++;
+ continue;
+ } else if (rmqIndex >= end) {
+ break;
+ } else {
+ if (isRmqIDString) {
+ if (sqlite3_bind_text(delete_statement,
+ placeholderIndex,
+ [rmqId UTF8String],
+ (int)[rmqId length],
+ SQLITE_STATIC) != SQLITE_OK) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeRmq2PersistentStore003,
+ @"Failed to bind rmqID %@", rmqId);
+ continue;
+ }
+ } else {
+ int64_t rmqIdValue = [rmqId longLongValue];
+ sqlite3_bind_int64(delete_statement, placeholderIndex, rmqIdValue);
+ }
+ placeholderIndex++;
+ }
+ rmqIndex++;
+ }
+ if (sqlite3_step(delete_statement) != SQLITE_DONE) {
+ _FIRMessagingRmqLogAndExit(delete_statement, deleteCount);
+ }
+ sqlite3_finalize(delete_statement);
+ deleteCount += sqlite3_changes(_database);
+ start = end;
+ }
+
+ // if we are here all of our sqlite queries should have succeeded
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeRmq2PersistentStore004,
+ @"%@ Trying to delete %d s2D ID's, successfully deleted %d",
+ kFCMRmqStoreTag, toDelete, deleteCount);
+ return deleteCount;
+}
+
+#pragma mark - Query
+
+- (int64_t)queryHighestRmqId {
+ NSString *queryFormat = @"SELECT %@ FROM %@ ORDER BY %@ DESC LIMIT %d";
+ NSString *query = [NSString stringWithFormat:queryFormat,
+ kRmqIdColumn, // column
+ kTableOutgoingRmqMessages, // table
+ kRmqIdColumn, // order by column
+ 1]; // limit
+
+ sqlite3_stmt *statement;
+ int64_t highestRmqId = 0;
+ if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &statement, NULL) != SQLITE_OK) {
+ _FIRMessagingRmqLogAndExit(statement, highestRmqId);
+ }
+ if (sqlite3_step(statement) == SQLITE_ROW) {
+ highestRmqId = sqlite3_column_int64(statement, 0);
+ }
+ sqlite3_finalize(statement);
+ return highestRmqId;
+}
+
+- (int64_t)queryLastRmqId {
+ NSString *queryFormat = @"SELECT %@ FROM %@ ORDER BY %@ DESC LIMIT %d";
+ NSString *query = [NSString stringWithFormat:queryFormat,
+ kRmqIdColumn, // column
+ kTableLastRmqId, // table
+ kRmqIdColumn, // order by column
+ 1]; // limit
+
+ sqlite3_stmt *statement;
+ int64_t lastRmqId = 0;
+ if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &statement, NULL) != SQLITE_OK) {
+ _FIRMessagingRmqLogAndExit(statement, lastRmqId);
+ }
+ if (sqlite3_step(statement) == SQLITE_ROW) {
+ lastRmqId = sqlite3_column_int64(statement, 0);
+ }
+ sqlite3_finalize(statement);
+ return lastRmqId;
+}
+
+- (BOOL)updateLastOutgoingRmqId:(int64_t)rmqID {
+ NSString *queryFormat = @"INSERT OR REPLACE INTO %@ (%@, %@) VALUES (?, ?)";
+ NSString *query = [NSString stringWithFormat:queryFormat,
+ kTableLastRmqId, // table
+ kIdColumn, kRmqIdColumn]; // columns
+ sqlite3_stmt *statement;
+ if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &statement, NULL) != SQLITE_OK) {
+ _FIRMessagingRmqLogAndExit(statement, NO);
+ }
+ if (sqlite3_bind_int(statement, 1, 1) != SQLITE_OK) {
+ _FIRMessagingRmqLogAndExit(statement, NO);
+ }
+ if (sqlite3_bind_int64(statement, 2, rmqID) != SQLITE_OK) {
+ _FIRMessagingRmqLogAndExit(statement, NO);
+ }
+ if (sqlite3_step(statement) != SQLITE_DONE) {
+ _FIRMessagingRmqLogAndExit(statement, NO);
+ }
+ sqlite3_finalize(statement);
+ return YES;
+}
+
+- (NSArray *)unackedS2dRmqIds {
+ NSString *queryFormat = @"SELECT %@ FROM %@ ORDER BY %@ ASC";
+ NSString *query = [NSString stringWithFormat:queryFormat,
+ kRmqIdColumn,
+ kTableS2DRmqIds,
+ kRmqIdColumn];
+ sqlite3_stmt *statement;
+ if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &statement, NULL) != SQLITE_OK) {
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeRmq2PersistentStore005,
+ @"%@: Could not find s2d ids", kFCMRmqStoreTag);
+ _FIRMessagingRmqLogAndExit(statement, @[]);
+ }
+ NSMutableArray *rmqIDArray = [NSMutableArray array];
+ while (sqlite3_step(statement) == SQLITE_ROW) {
+ const char *rmqID = (char *)sqlite3_column_text(statement, 0);
+ [rmqIDArray addObject:[NSString stringWithUTF8String:rmqID]];
+ }
+ sqlite3_finalize(statement);
+ return rmqIDArray;
+}
+
+#pragma mark - Scan
+
+- (void)scanOutgoingRmqMessagesWithHandler:(FCMOutgoingRmqMessagesTableHandler)handler {
+ static NSString *queryFormat = @"SELECT %@ FROM %@ WHERE %@ != 0 ORDER BY %@ ASC";
+ NSString *query = [NSString stringWithFormat:queryFormat,
+ kOutgoingRmqMessagesColumns, // select (rmq_id, type, data)
+ kTableOutgoingRmqMessages, // from table
+ kRmqIdColumn, // where
+ kRmqIdColumn]; // order by
+ sqlite3_stmt *statement;
+ if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &statement, NULL) != SQLITE_OK) {
+ [self logError];
+ sqlite3_finalize(statement);
+ return;
+ }
+ // can query sqlite3 for this but this is fine
+ const int rmqIdColumnNumber = 0;
+ const int typeColumnNumber = 1;
+ const int dataColumnNumber = 2;
+ while (sqlite3_step(statement) == SQLITE_ROW) {
+ int64_t rmqId = sqlite3_column_int64(statement, rmqIdColumnNumber);
+ int8_t type = sqlite3_column_int(statement, typeColumnNumber);
+ const void *bytes = sqlite3_column_blob(statement, dataColumnNumber);
+ int length = sqlite3_column_bytes(statement, dataColumnNumber);
+ _FIRMessagingDevAssert(bytes != NULL,
+ @"%@ Message with no data being stored in Rmq",
+ kFCMRmqStoreTag);
+ NSData *data = [NSData dataWithBytes:bytes length:length];
+ handler(rmqId, type, data);
+ }
+ sqlite3_finalize(statement);
+}
+
+#pragma mark - Sync Messages
+
+- (FIRMessagingPersistentSyncMessage *)querySyncMessageWithRmqID:(NSString *)rmqID {
+ _FIRMessagingDevAssert([rmqID length], @"Invalid rmqID key %@ to search in SYNC_RMQ", rmqID);
+
+ NSString *queryFormat = @"SELECT %@ FROM %@ WHERE %@ = '%@'";
+ NSString *query = [NSString stringWithFormat:queryFormat,
+ kSyncMessagesColumns, // SELECT (rmq_id, expiration_ts, apns_recv, mcs_recv)
+ kTableSyncMessages, // FROM sync_rmq
+ kRmqIdColumn, // WHERE rmq_id
+ rmqID];
+
+ sqlite3_stmt *stmt;
+ if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &stmt, NULL) != SQLITE_OK) {
+ [self logError];
+ sqlite3_finalize(stmt);
+ return nil;
+ }
+
+ const int rmqIDColumn = 0;
+ const int expirationTimestampColumn = 1;
+ const int apnsReceivedColumn = 2;
+ const int mcsReceivedColumn = 3;
+
+ int count = 0;
+ FIRMessagingPersistentSyncMessage *persistentMessage;
+
+ while (sqlite3_step(stmt) == SQLITE_ROW) {
+ NSString *rmqID =
+ [NSString stringWithUTF8String:(char *)sqlite3_column_text(stmt, rmqIDColumn)];
+ int64_t expirationTimestamp = sqlite3_column_int64(stmt, expirationTimestampColumn);
+ BOOL apnsReceived = sqlite3_column_int(stmt, apnsReceivedColumn);
+ BOOL mcsReceived = sqlite3_column_int(stmt, mcsReceivedColumn);
+
+ // create a new persistent message
+ persistentMessage =
+ [[FIRMessagingPersistentSyncMessage alloc] initWithRMQID:rmqID expirationTime:expirationTimestamp];
+ persistentMessage.apnsReceived = apnsReceived;
+ persistentMessage.mcsReceived = mcsReceived;
+
+ count++;
+ }
+ sqlite3_finalize(stmt);
+
+ _FIRMessagingDevAssert(count <= 1, @"Found multiple messages in %@ with same RMQ ID", kTableSyncMessages);
+ return persistentMessage;
+}
+
+- (BOOL)deleteSyncMessageWithRmqID:(NSString *)rmqID {
+ _FIRMessagingDevAssert([rmqID length], @"Invalid rmqID key %@ to delete in SYNC_RMQ", rmqID);
+ return [self deleteMessagesFromTable:kTableSyncMessages withRmqIds:@[rmqID]] > 0;
+}
+
+- (int)deleteExpiredOrFinishedSyncMessages:(NSError *__autoreleasing *)error {
+ int64_t now = FIRMessagingCurrentTimestampInSeconds();
+ NSString *deleteSQL = @"DELETE FROM %@ "
+ @"WHERE %@ < %lld OR " // expirationTime < now
+ @"(%@ = 1 AND %@ = 1)"; // apns_received = 1 AND mcs_received = 1
+ NSString *query = [NSString stringWithFormat:deleteSQL,
+ kTableSyncMessages,
+ kSyncMessageExpirationTimestampColumn,
+ now,
+ kSyncMessageAPNSReceivedColumn,
+ kSyncMessageMCSReceivedColumn];
+
+ NSString *errorReason = @"Failed to save delete expired sync messages from store.";
+
+ sqlite3_stmt *stmt;
+ if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &stmt, NULL) != SQLITE_OK) {
+ if (error) {
+ *error = [NSError fcm_errorWithCode:sqlite3_errcode(_database)
+ userInfo:@{ @"error" : errorReason }];
+ }
+ _FIRMessagingRmqLogAndExit(stmt, 0);
+ }
+
+ if (sqlite3_step(stmt) != SQLITE_DONE) {
+ if (error) {
+ *error = [NSError fcm_errorWithCode:sqlite3_errcode(_database)
+ userInfo:@{ @"error" : errorReason }];
+ }
+ _FIRMessagingRmqLogAndExit(stmt, 0);
+ }
+
+ sqlite3_finalize(stmt);
+ int deleteCount = sqlite3_changes(_database);
+ return deleteCount;
+}
+
+- (BOOL)saveSyncMessageWithRmqID:(NSString *)rmqID
+ expirationTime:(int64_t)expirationTime
+ apnsReceived:(BOOL)apnsReceived
+ mcsReceived:(BOOL)mcsReceived
+ error:(NSError **)error {
+ _FIRMessagingDevAssert([rmqID length], @"Invalid nil message to persist to SYNC_RMQ");
+
+ NSString *insertFormat = @"INSERT INTO %@ (%@, %@, %@, %@) VALUES (?, ?, ?, ?)";
+ NSString *insertSQL = [NSString stringWithFormat:insertFormat,
+ kTableSyncMessages, // Table name
+ kRmqIdColumn, // rmq_id
+ kSyncMessageExpirationTimestampColumn, // expiration_ts
+ kSyncMessageAPNSReceivedColumn, // apns_recv
+ kSyncMessageMCSReceivedColumn /* mcs_recv */];
+
+ sqlite3_stmt *stmt;
+
+ if (sqlite3_prepare_v2(_database, [insertSQL UTF8String], -1, &stmt, NULL) != SQLITE_OK) {
+ if (error) {
+ *error = [NSError fcm_errorWithCode:sqlite3_errcode(_database)
+ userInfo:@{ @"error" : @"Failed to save sync message to store." }];
+ }
+ _FIRMessagingRmqLogAndExit(stmt, NO);
+ }
+
+ if (sqlite3_bind_text(stmt, 1, [rmqID UTF8String], (int)[rmqID length], NULL) != SQLITE_OK) {
+ _FIRMessagingRmqLogAndExit(stmt, NO);
+ }
+
+ if (sqlite3_bind_int64(stmt, 2, expirationTime) != SQLITE_OK) {
+ _FIRMessagingRmqLogAndExit(stmt, NO);
+ }
+
+ if (sqlite3_bind_int(stmt, 3, apnsReceived ? 1 : 0) != SQLITE_OK) {
+ _FIRMessagingRmqLogAndExit(stmt, NO);
+ }
+
+ if (sqlite3_bind_int(stmt, 4, mcsReceived ? 1 : 0) != SQLITE_OK) {
+ _FIRMessagingRmqLogAndExit(stmt, NO);
+ }
+
+ if (sqlite3_step(stmt) != SQLITE_DONE) {
+ _FIRMessagingRmqLogAndExit(stmt, NO);
+ }
+
+ sqlite3_finalize(stmt);
+ return YES;
+}
+
+- (BOOL)updateSyncMessageViaAPNSWithRmqID:(NSString *)rmqID
+ error:(NSError **)error {
+ return [self updateSyncMessageWithRmqID:rmqID
+ column:kSyncMessageAPNSReceivedColumn
+ value:YES
+ error:error];
+}
+
+- (BOOL)updateSyncMessageViaMCSWithRmqID:(NSString *)rmqID
+ error:(NSError *__autoreleasing *)error {
+ return [self updateSyncMessageWithRmqID:rmqID
+ column:kSyncMessageMCSReceivedColumn
+ value:YES
+ error:error];
+}
+
+- (BOOL)updateSyncMessageWithRmqID:(NSString *)rmqID
+ column:(NSString *)column
+ value:(BOOL)value
+ error:(NSError **)error {
+ _FIRMessagingDevAssert([column isEqualToString:kSyncMessageAPNSReceivedColumn] ||
+ [column isEqualToString:kSyncMessageMCSReceivedColumn],
+ @"Invalid column name %@ for SYNC_RMQ", column);
+ NSString *queryFormat = @"UPDATE %@ " // Table name
+ @"SET %@ = %d " // column=value
+ @"WHERE %@ = ?"; // condition
+ NSString *query = [NSString stringWithFormat:queryFormat,
+ kTableSyncMessages,
+ column,
+ value ? 1 : 0,
+ kRmqIdColumn];
+ sqlite3_stmt *stmt;
+
+ if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &stmt, NULL) != SQLITE_OK) {
+ if (error) {
+ *error = [NSError fcm_errorWithCode:sqlite3_errcode(_database)
+ userInfo:@{ @"error" : @"Failed to update sync message"}];
+ }
+ _FIRMessagingRmqLogAndExit(stmt, NO);
+ }
+
+ if (sqlite3_bind_text(stmt, 1, [rmqID UTF8String], (int)[rmqID length], NULL) != SQLITE_OK) {
+ _FIRMessagingRmqLogAndExit(stmt, NO);
+ }
+
+ if (sqlite3_step(stmt) != SQLITE_DONE) {
+ _FIRMessagingRmqLogAndExit(stmt, NO);
+ }
+
+ sqlite3_finalize(stmt);
+ return YES;
+
+}
+
+#pragma mark - Private
+
+- (NSString *)lastErrorMessage {
+ return [NSString stringWithFormat:@"%s", sqlite3_errmsg(_database)];
+}
+
+- (int)lastErrorCode {
+ return sqlite3_errcode(_database);
+}
+
+- (void)logError {
+ FIRMessagingLoggerError(kFIRMessagingMessageCodeRmq2PersistentStore006,
+ @"%@ error: code (%d) message: %@", kFCMRmqStoreTag, [self lastErrorCode],
+ [self lastErrorMessage]);
+}
+
+- (void)logErrorAndFinalizeStatement:(sqlite3_stmt *)stmt {
+ [self logError];
+ sqlite3_finalize(stmt);
+}
+
+@end