added iOS source code
[wl-app.git] / iOS / Pods / FirebaseMessaging / Firebase / Messaging / FIRMessagingRmq2PersistentStore.m
1 /*
2  * Copyright 2017 Google
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 #import "FIRMessagingRmq2PersistentStore.h"
18
19 #import <sqlite3.h>
20
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"
28
29 #ifndef _FIRMessagingRmqLogAndExit
30 #define _FIRMessagingRmqLogAndExit(stmt, return_value)   \
31 do {                              \
32 [self logErrorAndFinalizeStatement:stmt];  \
33 return return_value; \
34 } while(0)
35 #endif
36
37 typedef enum : NSUInteger {
38   FIRMessagingRmqDirectoryUnknown,
39   FIRMessagingRmqDirectoryDocuments,
40   FIRMessagingRmqDirectoryApplicationSupport,
41 } FIRMessagingRmqDirectory;
42
43 static NSString *const kFCMRmqStoreTag = @"FIRMessagingRmqStore:";
44
45 // table names
46 NSString *const kTableOutgoingRmqMessages = @"outgoingRmqMessages";
47 NSString *const kTableLastRmqId = @"lastrmqid";
48 NSString *const kOldTableS2DRmqIds = @"s2dRmqIds";
49 NSString *const kTableS2DRmqIds = @"s2dRmqIds_1";
50
51 // Used to prevent de-duping of sync messages received both via APNS and MCS.
52 NSString *const kTableSyncMessages = @"incomingSyncMessages";
53
54 static NSString *const kTablePrefix = @"";
55
56 // create tables
57 static NSString *const kCreateTableOutgoingRmqMessages =
58     @"create TABLE IF NOT EXISTS %@%@ "
59     @"(_id INTEGER PRIMARY KEY, "
60     @"rmq_id INTEGER, "
61     @"type INTEGER, "
62     @"ts INTEGER, "
63     @"data BLOB)";
64
65 static NSString *const kCreateTableLastRmqId =
66     @"create TABLE IF NOT EXISTS %@%@ "
67     @"(_id INTEGER PRIMARY KEY, "
68     @"rmq_id INTEGER)";
69
70 static NSString *const kCreateTableS2DRmqIds =
71     @"create TABLE IF NOT EXISTS %@%@ "
72     @"(_id INTEGER PRIMARY KEY, "
73     @"rmq_id TEXT)";
74
75 static NSString *const kCreateTableSyncMessages =
76     @"create TABLE IF NOT EXISTS %@%@ "
77     @"(_id INTEGER PRIMARY KEY, "
78     @"rmq_id TEXT, "
79     @"expiration_ts INTEGER, "
80     @"apns_recv INTEGER, "
81     @"mcs_recv INTEGER)";
82
83 static NSString *const kDropTableCommand =
84     @"drop TABLE if exists %@%@";
85
86 // table infos
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";
91
92 static NSString *const kOutgoingRmqMessagesColumns = @"rmq_id, type, data";
93
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";
100
101 // table data handlers
102 typedef void(^FCMOutgoingRmqMessagesTableHandler)(int64_t rmqId, int8_t tag, NSData *data);
103
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);
109 #pragma pop
110   NSString *errorString = [NSString stringWithFormat:@"%d - %s", result, errorStr];
111   return errorString;
112 }
113
114 @interface FIRMessagingRmq2PersistentStore () {
115   sqlite3 *_database;
116 }
117
118 @property(nonatomic, readwrite, strong) NSString *databaseName;
119 @property(nonatomic, readwrite, assign) FIRMessagingRmqDirectory currentDirectory;
120
121 @end
122
123 @implementation FIRMessagingRmq2PersistentStore
124
125 - (instancetype)initWithDatabaseName:(NSString *)databaseName {
126   self = [super init];
127   if (self) {
128     _databaseName = [databaseName copy];
129     BOOL didMoveToApplicationSupport =
130         [self moveToApplicationSupportSubDirectory:kFIRMessagingApplicationSupportSubDirectory];
131
132     _currentDirectory = didMoveToApplicationSupport
133                             ? FIRMessagingRmqDirectoryApplicationSupport
134                             : FIRMessagingRmqDirectoryDocuments;
135
136     [self openDatabase:_databaseName];
137   }
138   return self;
139 }
140
141 - (void)dealloc {
142   sqlite3_close(_database);
143 }
144
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;
152
153   if (![[NSFileManager defaultManager] fileExistsAtPath:subDirectoryPath
154                                             isDirectory:&hasSubDirectory]) {
155     // Cannot move to non-existent directory
156     return NO;
157   }
158
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];
165
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);
173       }
174       return NO;
175     }
176     NSError *moveError;
177     if (![[NSFileManager defaultManager] moveItemAtPath:oldPlistPath
178                                                  toPath:newPlistPath
179                                                   error:&moveError]) {
180       FIRMessagingLoggerError(kFIRMessagingMessageCodeRmq2PersistentStore001,
181                               @"Failed to move file %@ from %@ to %@. Error: %@", self.databaseName,
182                               oldPlistPath, newPlistPath, moveError);
183       return NO;
184     }
185   }
186   // We moved the file if it existed, otherwise we didn't need to do anything
187   return YES;
188 }
189
190 - (BOOL)doesFileExistInDirectory:(FIRMessagingRmqDirectory)directory {
191   NSString *path = [[self class] pathForDatabase:self.databaseName inDirectory:directory];
192   return [[NSFileManager defaultManager] fileExistsAtPath:path];
193 }
194
195 + (NSString *)pathForDatabase:(NSString *)dbName inDirectory:(FIRMessagingRmqDirectory)directory {
196   NSArray *paths;
197   NSArray *components;
198   NSString *dbNameWithExtension = [NSString stringWithFormat:@"%@.sqlite", dbName];
199   NSString *errorMessage;
200
201   switch (directory) {
202     case FIRMessagingRmqDirectoryDocuments:
203       paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
204       components = @[paths.lastObject, dbNameWithExtension];
205       break;
206
207     case FIRMessagingRmqDirectoryApplicationSupport:
208       paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory,
209                                                   NSUserDomainMask,
210                                                   YES);
211       components = @[
212                      paths.lastObject,
213                      kFIRMessagingApplicationSupportSubDirectory,
214                      dbNameWithExtension
215                      ];
216       break;
217
218     default:
219       errorMessage = [NSString stringWithFormat:@"Invalid directory type %lu",
220                       (unsigned long)directory];
221       FIRMessagingLoggerError(kFIRMessagingMessageCodeRmq2PersistentStoreInvalidRmqDirectory,
222                               @"%@",
223                               errorMessage);
224       NSAssert(NO, errorMessage);
225       break;
226   }
227
228   return [NSString pathWithComponents:components];
229 }
230
231 - (void)createTableWithName:(NSString *)tableName command:(NSString *)command {
232   char *error;
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,
241                             @"%@",
242                             errorMessage);
243     NSAssert(NO, errorMessage);
244   }
245 }
246
247 - (void)dropTableWithName:(NSString *)tableName {
248   char *error;
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);
253   }
254 }
255
256 - (void)removeDatabase {
257   NSString *path = [[self class] pathForDatabase:self.databaseName
258                                      inDirectory:self.currentDirectory];
259   [[NSFileManager defaultManager] removeItemAtPath:path error:nil];
260 }
261
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];
269 }
270
271 - (void)openDatabase:(NSString *)dbName {
272   NSFileManager *fileManager = [NSFileManager defaultManager];
273   NSString *path = [[self class] pathForDatabase:dbName inDirectory:self.currentDirectory];
274
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: %@",
283                                      path,
284                                      errorString];
285       FIRMessagingLoggerError(kFIRMessagingMessageCodeRmq2PersistentStoreErrorOpeningDatabase,
286                               @"%@",
287                               errorMessage);
288       NSAssert(NO, errorMessage);
289       return;
290     }
291     [self createTableWithName:kTableOutgoingRmqMessages
292                       command:kCreateTableOutgoingRmqMessages];
293
294     [self createTableWithName:kTableLastRmqId command:kCreateTableLastRmqId];
295     [self createTableWithName:kTableS2DRmqIds command:kCreateTableS2DRmqIds];
296   } else {
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: %@",
303                                      path,
304                                      errorString];
305       FIRMessagingLoggerError(kFIRMessagingMessageCodeRmq2PersistentStoreErrorCreatingDatabase,
306                               @"%@",
307                               errorMessage);
308       NSAssert(NO, errorMessage);
309       didOpenDatabase = NO;
310     } else {
311       [self updateDbWithStringRmqID];
312     }
313   }
314
315   if (didOpenDatabase) {
316     [self createTableWithName:kTableSyncMessages command:kCreateTableSyncMessages];
317   }
318 }
319
320 - (void)updateDbWithStringRmqID {
321   [self createTableWithName:kTableS2DRmqIds command:kCreateTableS2DRmqIds];
322   [self dropTableWithName:kOldTableS2DRmqIds];
323 }
324
325 #pragma mark - Insert
326
327 - (BOOL)saveUnackedS2dMessageWithRmqId:(NSString *)rmqId {
328   NSString *insertFormat = @"INSERT INTO %@ (%@) VALUES (?)";
329   NSString *insertSQL = [NSString stringWithFormat:insertFormat,
330                          kTableS2DRmqIds,
331                          kRmqIdColumn];
332   sqlite3_stmt *insert_statement;
333   if (sqlite3_prepare_v2(_database, [insertSQL UTF8String], -1, &insert_statement, NULL)
334       != SQLITE_OK) {
335     _FIRMessagingRmqLogAndExit(insert_statement, NO);
336   }
337   if (sqlite3_bind_text(insert_statement,
338                         1,
339                         [rmqId UTF8String],
340                         (int)[rmqId length],
341                         SQLITE_STATIC) != SQLITE_OK) {
342     _FIRMessagingRmqLogAndExit(insert_statement, NO);
343   }
344   if (sqlite3_step(insert_statement) != SQLITE_DONE) {
345     _FIRMessagingRmqLogAndExit(insert_statement, NO);
346   }
347   sqlite3_finalize(insert_statement);
348   return YES;
349 }
350
351 - (BOOL)saveMessageWithRmqId:(int64_t)rmqId
352                          tag:(int8_t)tag
353                         data:(NSData *)data
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)
361       != SQLITE_OK) {
362     if (error) {
363       *error = [NSError errorWithDomain:[NSString stringWithFormat:@"%s", sqlite3_errmsg(_database)]
364                                    code:sqlite3_errcode(_database)
365                                userInfo:nil];
366     }
367     _FIRMessagingRmqLogAndExit(insert_statement, NO);
368   }
369   if (sqlite3_bind_int64(insert_statement, 1, rmqId) != SQLITE_OK) {
370     _FIRMessagingRmqLogAndExit(insert_statement, NO);
371   }
372   if (sqlite3_bind_int(insert_statement, 2, tag) != SQLITE_OK) {
373     _FIRMessagingRmqLogAndExit(insert_statement, NO);
374   }
375   if (sqlite3_bind_blob(insert_statement, 3, [data bytes], (int)[data length], NULL) != SQLITE_OK) {
376     _FIRMessagingRmqLogAndExit(insert_statement, NO);
377   }
378   if (sqlite3_step(insert_statement) != SQLITE_DONE) {
379     _FIRMessagingRmqLogAndExit(insert_statement, NO);
380   }
381
382   sqlite3_finalize(insert_statement);
383   return YES;
384 }
385
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);
393
394   BOOL isRmqIDString = NO;
395   // RmqID is a string only for outgoing messages
396   if ([tableName isEqualToString:kTableS2DRmqIds] ||
397       [tableName isEqualToString:kTableSyncMessages]) {
398     isRmqIDString = YES;
399   }
400
401   NSMutableString *delete = [NSMutableString stringWithFormat:@"DELETE FROM %@ WHERE ", tableName];
402
403   NSString *toDeleteArgument = [NSString stringWithFormat:@"%@ = ? OR ", kRmqIdColumn];
404
405   int toDelete = (int)[rmqIds count];
406   if (toDelete == 0) {
407     return 0;
408   }
409   int maxBatchSize = 100;
410   int start = 0;
411   int deleteCount = 0;
412   while (start < toDelete) {
413
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];
419     }
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];
424
425
426     // sqlite update
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);
431     }
432
433     // bind values
434     int rmqIndex = 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) {
438         rmqIndex++;
439         continue;
440       } else if (rmqIndex >= end) {
441         break;
442       } else {
443         if (isRmqIDString) {
444           if (sqlite3_bind_text(delete_statement,
445                                 placeholderIndex,
446                                 [rmqId UTF8String],
447                                 (int)[rmqId length],
448                                 SQLITE_STATIC) != SQLITE_OK) {
449             FIRMessagingLoggerDebug(kFIRMessagingMessageCodeRmq2PersistentStore003,
450                                     @"Failed to bind rmqID %@", rmqId);
451             continue;
452           }
453         } else {
454           int64_t rmqIdValue = [rmqId longLongValue];
455           sqlite3_bind_int64(delete_statement, placeholderIndex, rmqIdValue);
456         }
457         placeholderIndex++;
458       }
459       rmqIndex++;
460     }
461     if (sqlite3_step(delete_statement) != SQLITE_DONE) {
462       _FIRMessagingRmqLogAndExit(delete_statement, deleteCount);
463     }
464     sqlite3_finalize(delete_statement);
465     deleteCount += sqlite3_changes(_database);
466     start = end;
467   }
468
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);
473   return deleteCount;
474 }
475
476 #pragma mark - Query
477
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
484                      1]; // limit
485
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);
490   }
491   if (sqlite3_step(statement) == SQLITE_ROW) {
492     highestRmqId = sqlite3_column_int64(statement, 0);
493   }
494   sqlite3_finalize(statement);
495   return highestRmqId;
496 }
497
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
504                      1]; // limit
505
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);
510   }
511   if (sqlite3_step(statement) == SQLITE_ROW) {
512     lastRmqId = sqlite3_column_int64(statement, 0);
513   }
514   sqlite3_finalize(statement);
515   return lastRmqId;
516 }
517
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);
526   }
527   if (sqlite3_bind_int(statement, 1, 1) != SQLITE_OK) {
528     _FIRMessagingRmqLogAndExit(statement, NO);
529   }
530   if (sqlite3_bind_int64(statement, 2, rmqID) != SQLITE_OK) {
531     _FIRMessagingRmqLogAndExit(statement, NO);
532   }
533   if (sqlite3_step(statement) != SQLITE_DONE) {
534     _FIRMessagingRmqLogAndExit(statement, NO);
535   }
536   sqlite3_finalize(statement);
537   return YES;
538 }
539
540 - (NSArray *)unackedS2dRmqIds {
541   NSString *queryFormat = @"SELECT %@ FROM %@ ORDER BY %@ ASC";
542   NSString *query = [NSString stringWithFormat:queryFormat,
543                      kRmqIdColumn,
544                      kTableS2DRmqIds,
545                      kRmqIdColumn];
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, @[]);
551   }
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]];
556   }
557   sqlite3_finalize(statement);
558   return rmqIDArray;
559 }
560
561 #pragma mark - Scan
562
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) {
572     [self logError];
573     sqlite3_finalize(statement);
574     return;
575   }
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",
587                            kFCMRmqStoreTag);
588     NSData *data = [NSData dataWithBytes:bytes length:length];
589     handler(rmqId, type, data);
590   }
591   sqlite3_finalize(statement);
592 }
593
594 #pragma mark - Sync Messages
595
596 - (FIRMessagingPersistentSyncMessage *)querySyncMessageWithRmqID:(NSString *)rmqID {
597   _FIRMessagingDevAssert([rmqID length], @"Invalid rmqID key %@ to search in SYNC_RMQ", rmqID);
598
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
604                      rmqID];
605
606   sqlite3_stmt *stmt;
607   if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &stmt, NULL) != SQLITE_OK) {
608     [self logError];
609     sqlite3_finalize(stmt);
610     return nil;
611   }
612
613   const int rmqIDColumn = 0;
614   const int expirationTimestampColumn = 1;
615   const int apnsReceivedColumn = 2;
616   const int mcsReceivedColumn = 3;
617
618   int count = 0;
619   FIRMessagingPersistentSyncMessage *persistentMessage;
620
621   while (sqlite3_step(stmt) == SQLITE_ROW) {
622     NSString *rmqID =
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);
627
628     // create a new persistent message
629     persistentMessage =
630         [[FIRMessagingPersistentSyncMessage alloc] initWithRMQID:rmqID expirationTime:expirationTimestamp];
631     persistentMessage.apnsReceived = apnsReceived;
632     persistentMessage.mcsReceived = mcsReceived;
633
634     count++;
635   }
636   sqlite3_finalize(stmt);
637
638   _FIRMessagingDevAssert(count <= 1, @"Found multiple messages in %@ with same RMQ ID", kTableSyncMessages);
639   return persistentMessage;
640 }
641
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;
645 }
646
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,
653                      kTableSyncMessages,
654                      kSyncMessageExpirationTimestampColumn,
655                      now,
656                      kSyncMessageAPNSReceivedColumn,
657                      kSyncMessageMCSReceivedColumn];
658
659   NSString *errorReason = @"Failed to save delete expired sync messages from store.";
660
661   sqlite3_stmt *stmt;
662   if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &stmt, NULL) != SQLITE_OK) {
663     if (error) {
664       *error = [NSError fcm_errorWithCode:sqlite3_errcode(_database)
665                                  userInfo:@{ @"error" : errorReason }];
666     }
667     _FIRMessagingRmqLogAndExit(stmt, 0);
668   }
669
670   if (sqlite3_step(stmt) != SQLITE_DONE) {
671     if (error) {
672       *error = [NSError fcm_errorWithCode:sqlite3_errcode(_database)
673                                  userInfo:@{ @"error" : errorReason }];
674     }
675     _FIRMessagingRmqLogAndExit(stmt, 0);
676   }
677
678   sqlite3_finalize(stmt);
679   int deleteCount = sqlite3_changes(_database);
680   return deleteCount;
681 }
682
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");
689
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 */];
697
698   sqlite3_stmt *stmt;
699
700   if (sqlite3_prepare_v2(_database, [insertSQL UTF8String], -1, &stmt, NULL) != SQLITE_OK) {
701     if (error) {
702       *error = [NSError fcm_errorWithCode:sqlite3_errcode(_database)
703                                  userInfo:@{ @"error" : @"Failed to save sync message to store." }];
704     }
705     _FIRMessagingRmqLogAndExit(stmt, NO);
706   }
707
708   if (sqlite3_bind_text(stmt, 1, [rmqID UTF8String], (int)[rmqID length], NULL) != SQLITE_OK) {
709     _FIRMessagingRmqLogAndExit(stmt, NO);
710   }
711
712   if (sqlite3_bind_int64(stmt, 2, expirationTime) != SQLITE_OK) {
713     _FIRMessagingRmqLogAndExit(stmt, NO);
714   }
715
716   if (sqlite3_bind_int(stmt, 3, apnsReceived ? 1 : 0) != SQLITE_OK) {
717     _FIRMessagingRmqLogAndExit(stmt, NO);
718   }
719
720   if (sqlite3_bind_int(stmt, 4, mcsReceived ? 1 : 0) != SQLITE_OK) {
721     _FIRMessagingRmqLogAndExit(stmt, NO);
722   }
723
724   if (sqlite3_step(stmt) != SQLITE_DONE) {
725     _FIRMessagingRmqLogAndExit(stmt, NO);
726   }
727
728   sqlite3_finalize(stmt);
729   return YES;
730 }
731
732 - (BOOL)updateSyncMessageViaAPNSWithRmqID:(NSString *)rmqID
733                                     error:(NSError **)error {
734   return [self updateSyncMessageWithRmqID:rmqID
735                                    column:kSyncMessageAPNSReceivedColumn
736                                     value:YES
737                                     error:error];
738 }
739
740 - (BOOL)updateSyncMessageViaMCSWithRmqID:(NSString *)rmqID
741                                    error:(NSError *__autoreleasing *)error {
742   return [self updateSyncMessageWithRmqID:rmqID
743                                    column:kSyncMessageMCSReceivedColumn
744                                     value:YES
745                                     error:error];
746 }
747
748 - (BOOL)updateSyncMessageWithRmqID:(NSString *)rmqID
749                             column:(NSString *)column
750                              value:(BOOL)value
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,
759                      kTableSyncMessages,
760                      column,
761                      value ? 1 : 0,
762                      kRmqIdColumn];
763   sqlite3_stmt *stmt;
764
765   if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &stmt, NULL) != SQLITE_OK) {
766     if (error) {
767       *error = [NSError fcm_errorWithCode:sqlite3_errcode(_database)
768                                  userInfo:@{ @"error" : @"Failed to update sync message"}];
769     }
770     _FIRMessagingRmqLogAndExit(stmt, NO);
771   }
772
773   if (sqlite3_bind_text(stmt, 1, [rmqID UTF8String], (int)[rmqID length], NULL) != SQLITE_OK) {
774     _FIRMessagingRmqLogAndExit(stmt, NO);
775   }
776
777   if (sqlite3_step(stmt) != SQLITE_DONE) {
778     _FIRMessagingRmqLogAndExit(stmt, NO);
779   }
780
781   sqlite3_finalize(stmt);
782   return YES;
783
784 }
785
786 #pragma mark - Private
787
788 - (NSString *)lastErrorMessage {
789   return [NSString stringWithFormat:@"%s", sqlite3_errmsg(_database)];
790 }
791
792 - (int)lastErrorCode {
793   return sqlite3_errcode(_database);
794 }
795
796 - (void)logError {
797   FIRMessagingLoggerError(kFIRMessagingMessageCodeRmq2PersistentStore006,
798                           @"%@ error: code (%d) message: %@", kFCMRmqStoreTag, [self lastErrorCode],
799                           [self lastErrorMessage]);
800 }
801
802 - (void)logErrorAndFinalizeStatement:(sqlite3_stmt *)stmt {
803   [self logError];
804   sqlite3_finalize(stmt);
805 }
806
807 @end