SEGDatabaseManager.m 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  1. // Copyright 2019 Google
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. #import "FirebaseSegmentation/Sources/SEGDatabaseManager.h"
  15. #import <sqlite3.h>
  16. #import "FirebaseCore/Sources/Private/FIRLogger.h"
  17. /// SQLite file name.
  18. static NSString *const kDatabaseName = @"FirebaseSegmentation.sqlite3";
  19. /// The storage sub-directory that the Segmentation database resides in.
  20. static NSString *const kSegmentationStorageSubDirectory = @"Google/FirebaseSegmentation";
  21. /// Column names
  22. static NSString *const kMainTableName = @"main";
  23. static NSString *const kMainTableColumnApplicationIdentifier = @"firebase_app_identifier";
  24. static NSString *const kMainTableColumnCustomInstallationIdentifier =
  25. @"custom_installation_identifier";
  26. static NSString *const kMainTableColumnFirebaseInstallationIdentifier =
  27. @"firebase_installation_identifier";
  28. static NSString *const kMainTableColumnAssociationStatus = @"association_status";
  29. // Exclude the database from iCloud backup.
  30. static BOOL SEGAddSkipBackupAttributeToItemAtPath(NSString *filePathString) {
  31. NSURL *URL = [NSURL fileURLWithPath:filePathString];
  32. assert([[NSFileManager defaultManager] fileExistsAtPath:URL.path]);
  33. NSError *error = nil;
  34. BOOL success = [URL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:&error];
  35. if (!success) {
  36. FIRLogError(kFIRLoggerSegmentation, @"I-SEG000001", @"Error excluding %@ from backup %@.",
  37. [URL lastPathComponent], error);
  38. }
  39. return success;
  40. }
  41. static BOOL SEGCreateFilePathIfNotExist(NSString *filePath) {
  42. if (!filePath.length) {
  43. FIRLogError(kFIRLoggerSegmentation, @"I-SEG000002",
  44. @"Failed to create subdirectory for an empty file path.");
  45. return NO;
  46. }
  47. NSFileManager *fileManager = [NSFileManager defaultManager];
  48. if (![fileManager fileExistsAtPath:filePath]) {
  49. NSError *error;
  50. [fileManager createDirectoryAtPath:[filePath stringByDeletingLastPathComponent]
  51. withIntermediateDirectories:YES
  52. attributes:nil
  53. error:&error];
  54. if (error) {
  55. FIRLogError(kFIRLoggerSegmentation, @"I-SEG000003",
  56. @"Failed to create subdirectory for database file: %@.", error);
  57. return NO;
  58. }
  59. }
  60. return YES;
  61. }
  62. @implementation SEGDatabaseManager {
  63. /// Database storing all the config information.
  64. sqlite3 *_database;
  65. /// Serial queue for database read/write operations.
  66. dispatch_queue_t _databaseOperationQueue;
  67. }
  68. + (instancetype)sharedInstance {
  69. static dispatch_once_t onceToken;
  70. static SEGDatabaseManager *sharedInstance;
  71. dispatch_once(&onceToken, ^{
  72. sharedInstance = [[SEGDatabaseManager alloc] init];
  73. });
  74. return sharedInstance;
  75. }
  76. - (instancetype)init {
  77. self = [super init];
  78. if (self) {
  79. _databaseOperationQueue =
  80. dispatch_queue_create("com.google.firebasesegmentation.database", DISPATCH_QUEUE_SERIAL);
  81. }
  82. return self;
  83. }
  84. #pragma mark - Public Methods
  85. - (void)loadMainTableWithCompletion:(SEGRequestCompletion)completionHandler {
  86. dispatch_async(_databaseOperationQueue, ^{
  87. // Read the database into memory.
  88. NSDictionary<NSString *, NSDictionary<NSString *, NSString *> *> *associations =
  89. [self loadMainTable];
  90. if (associations != nil) {
  91. completionHandler(YES, associations);
  92. } else {
  93. FIRLogError(kFIRLoggerSegmentation, @"I-SEG000004", @"Failed to load main table.");
  94. completionHandler(NO, @{});
  95. }
  96. });
  97. return;
  98. }
  99. - (void)createOrOpenDatabaseWithCompletion:(SEGRequestCompletion)completionHandler {
  100. dispatch_async(_databaseOperationQueue, ^{
  101. NSString *dbPath = [SEGDatabaseManager pathForSegmentationDatabase];
  102. FIRLogDebug(kFIRLoggerSegmentation, @"I-SEG000005", @"Loading segmentation database at path %@",
  103. dbPath);
  104. const char *databasePath = dbPath.UTF8String;
  105. // Create or open database path.
  106. if (!SEGCreateFilePathIfNotExist(dbPath)) {
  107. completionHandler(NO, @{kSEGErrorDescription : @"Could not create database file at path"});
  108. }
  109. int flags = SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE | SQLITE_OPEN_FILEPROTECTION_COMPLETE |
  110. SQLITE_OPEN_FULLMUTEX;
  111. if (sqlite3_open_v2(databasePath, &self->_database, flags, NULL) == SQLITE_OK) {
  112. // Create table if does not exist already.
  113. if ([self createTableSchema]) {
  114. // DB file created or already exists.
  115. // Exclude the app data used from iCloud backup.
  116. SEGAddSkipBackupAttributeToItemAtPath(dbPath);
  117. // Read the database into memory.
  118. NSDictionary<NSString *, NSString *> *associations = [self loadMainTable];
  119. completionHandler(YES, associations);
  120. } else {
  121. // Remove database before fail.
  122. [self removeDatabase:dbPath];
  123. FIRLogError(kFIRLoggerSegmentation, @"I-SEG00006", @"Failed to create table.");
  124. // Create a new database if existing database file is corrupted.
  125. if (!SEGCreateFilePathIfNotExist(dbPath)) {
  126. NSString *description =
  127. [NSString stringWithFormat:@"Could not recreate database file at path: %@", dbPath];
  128. completionHandler(NO, @{kSEGErrorDescription : description});
  129. return;
  130. }
  131. // Try to open the database with the new file.
  132. if (sqlite3_open_v2(databasePath, &self->_database, flags, NULL) == SQLITE_OK) {
  133. if (![self createTableSchema]) {
  134. // Remove database before fail.
  135. [self removeDatabase:dbPath];
  136. // If it failed again, there's nothing we can do here.
  137. FIRLogError(kFIRLoggerSegmentation, @"I-SEG00007", @"Failed to create table.");
  138. completionHandler(NO, @{kSEGErrorDescription : @"Failed to re-open new database file"});
  139. } else {
  140. // Exclude the app data used from iCloud backup.
  141. SEGAddSkipBackupAttributeToItemAtPath(dbPath);
  142. // Skip reading the db into memory, since it's empty.
  143. completionHandler(YES, @{});
  144. }
  145. } else {
  146. [self logDatabaseError];
  147. completionHandler(NO, @{kSEGErrorDescription : @"Could not create database."});
  148. }
  149. }
  150. } else {
  151. [self logDatabaseError];
  152. completionHandler(NO, @{kSEGErrorDescription : @"Error creating database."});
  153. }
  154. });
  155. }
  156. - (void)removeDatabase:(NSString *)path completion:(SEGRequestCompletion)completionHandler {
  157. dispatch_async(_databaseOperationQueue, ^{
  158. [self removeDatabase:path];
  159. completionHandler(YES, nil);
  160. });
  161. }
  162. #pragma mark - Private Methods
  163. - (NSDictionary *)loadMainTable {
  164. NSString *SQLQuery = [NSString
  165. stringWithFormat:@"SELECT %@, %@, %@, %@ FROM %@", kMainTableColumnApplicationIdentifier,
  166. kMainTableColumnCustomInstallationIdentifier,
  167. kMainTableColumnFirebaseInstallationIdentifier,
  168. kMainTableColumnAssociationStatus, kMainTableName];
  169. sqlite3_stmt *statement = [self prepareSQL:[SQLQuery cStringUsingEncoding:NSUTF8StringEncoding]];
  170. if (!statement) {
  171. FIRLogError(kFIRLoggerSegmentation, @"I-SEG00008",
  172. @"Failed to create sqlite statement with query: %@.", SQLQuery);
  173. return nil;
  174. }
  175. NSMutableDictionary<NSString *, NSDictionary<NSString *, NSString *> *> *associations =
  176. [[NSMutableDictionary alloc] init];
  177. while (sqlite3_step(statement) == SQLITE_ROW) {
  178. NSString *firebaseApplicationName =
  179. [[NSString alloc] initWithUTF8String:(char *)sqlite3_column_text(statement, 0)];
  180. NSString *customInstallationIdentifier =
  181. [[NSString alloc] initWithUTF8String:(char *)sqlite3_column_text(statement, 1)];
  182. NSString *firebaseInstallationIdentifier =
  183. [[NSString alloc] initWithUTF8String:(char *)sqlite3_column_text(statement, 2)];
  184. NSString *associationStatus =
  185. [[NSString alloc] initWithUTF8String:(char *)sqlite3_column_text(statement, 3)];
  186. NSDictionary<NSString *, NSString *> *associationData = @{
  187. kSEGCustomInstallationIdentifierKey : customInstallationIdentifier,
  188. kSEGFirebaseInstallationIdentifierKey : firebaseInstallationIdentifier,
  189. kSEGAssociationStatusKey : associationStatus
  190. };
  191. [associations setObject:associationData forKey:firebaseApplicationName];
  192. }
  193. sqlite3_finalize(statement);
  194. return associations;
  195. }
  196. /// Returns the current version of the Remote Config database.
  197. + (NSString *)pathForSegmentationDatabase {
  198. #if TARGET_OS_TV
  199. NSArray *dirPaths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
  200. #else
  201. NSArray *dirPaths =
  202. NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES);
  203. #endif
  204. NSString *storageDir = dirPaths.firstObject;
  205. NSArray<NSString *> *components =
  206. @[ storageDir, kSegmentationStorageSubDirectory, kDatabaseName ];
  207. return [NSString pathWithComponents:components];
  208. }
  209. - (BOOL)createTableSchema {
  210. SEG_MUST_NOT_BE_MAIN_THREAD();
  211. NSString *mainTableSchema =
  212. [NSString stringWithFormat:@"create TABLE IF NOT EXISTS %@ (_id INTEGER PRIMARY KEY, %@ "
  213. @"TEXT, %@ TEXT, %@ TEXT, %@ TEXT)",
  214. kMainTableName, kMainTableColumnApplicationIdentifier,
  215. kMainTableColumnCustomInstallationIdentifier,
  216. kMainTableColumnFirebaseInstallationIdentifier,
  217. kMainTableColumnAssociationStatus];
  218. return [self executeQuery:[mainTableSchema cStringUsingEncoding:NSUTF8StringEncoding]];
  219. }
  220. - (void)removeDatabase:(NSString *)path {
  221. SEG_MUST_NOT_BE_MAIN_THREAD();
  222. if (sqlite3_close(self->_database) != SQLITE_OK) {
  223. [self logDatabaseError];
  224. }
  225. self->_database = nil;
  226. NSFileManager *fileManager = [NSFileManager defaultManager];
  227. NSError *error;
  228. if (![fileManager removeItemAtPath:path error:&error]) {
  229. FIRLogError(kFIRLoggerSegmentation, @"I-SEG000011",
  230. @"Failed to remove database at path %@ for error %@.", path, error);
  231. }
  232. }
  233. #pragma mark - execute
  234. - (BOOL)executeQuery:(const char *)SQL {
  235. SEG_MUST_NOT_BE_MAIN_THREAD();
  236. char *error;
  237. if (sqlite3_exec(_database, SQL, nil, nil, &error) != SQLITE_OK) {
  238. FIRLogError(kFIRLoggerSegmentation, @"I-SEG000012", @"Failed to execute query with error %s.",
  239. error);
  240. return NO;
  241. }
  242. return YES;
  243. }
  244. #pragma mark - insert
  245. - (void)insertMainTableApplicationNamed:(NSString *)firebaseApplication
  246. customInstanceIdentifier:(NSString *)customInstanceIdentifier
  247. firebaseInstanceIdentifier:(NSString *)firebaseInstanceIdentifier
  248. associationStatus:(NSString *)associationStatus
  249. completionHandler:(SEGRequestCompletion)handler {
  250. // TODO: delete the row first.
  251. dispatch_async(_databaseOperationQueue, ^{
  252. NSArray<NSString *> *values = @[
  253. firebaseApplication, customInstanceIdentifier, firebaseInstanceIdentifier, associationStatus
  254. ];
  255. BOOL success = [self insertMainTableWithValues:values];
  256. if (handler) {
  257. dispatch_async(dispatch_get_main_queue(), ^{
  258. handler(success, nil);
  259. });
  260. }
  261. });
  262. }
  263. - (BOOL)insertMainTableWithValues:(NSArray<NSString *> *)values {
  264. SEG_MUST_NOT_BE_MAIN_THREAD();
  265. if (values.count != 4) {
  266. FIRLogError(kFIRLoggerSegmentation, @"I-SEG000013",
  267. @"Failed to insert config record. Wrong number of give parameters, current "
  268. @"number is %ld, correct number is 4.",
  269. (long)values.count);
  270. return NO;
  271. }
  272. NSString *SQL = [NSString stringWithFormat:@"INSERT INTO %@ (%@, %@, %@, %@) values (?, ?, ?, ?)",
  273. kMainTableName, kMainTableColumnApplicationIdentifier,
  274. kMainTableColumnCustomInstallationIdentifier,
  275. kMainTableColumnFirebaseInstallationIdentifier,
  276. kMainTableColumnAssociationStatus];
  277. sqlite3_stmt *statement = [self prepareSQL:[SQL UTF8String]];
  278. if (!statement) {
  279. return NO;
  280. }
  281. NSString *aString = values[0];
  282. if (![self bindStringToStatement:statement index:1 string:aString]) {
  283. return [self logErrorWithSQL:[SQL UTF8String] finalizeStatement:statement returnValue:NO];
  284. }
  285. aString = values[1];
  286. if (![self bindStringToStatement:statement index:2 string:aString]) {
  287. return [self logErrorWithSQL:[SQL UTF8String] finalizeStatement:statement returnValue:NO];
  288. }
  289. aString = values[2];
  290. if (![self bindStringToStatement:statement index:3 string:aString]) {
  291. return [self logErrorWithSQL:[SQL UTF8String] finalizeStatement:statement returnValue:NO];
  292. }
  293. aString = values[3];
  294. if (![self bindStringToStatement:statement index:4 string:aString]) {
  295. return [self logErrorWithSQL:[SQL UTF8String] finalizeStatement:statement returnValue:NO];
  296. }
  297. if (sqlite3_step(statement) != SQLITE_DONE) {
  298. return [self logErrorWithSQL:[SQL UTF8String] finalizeStatement:statement returnValue:NO];
  299. }
  300. sqlite3_finalize(statement);
  301. return YES;
  302. }
  303. /// TODO: (Check if required). Clear the record of given namespace and package name
  304. /// before updating the table.
  305. - (void)deleteRecordFromMainTableWithCustomInstanceIdentifier:
  306. (nonnull NSString *)customInstanceIdentifier {
  307. }
  308. /// TODO: (Check if required). Remove all the records from a config content table.
  309. - (void)deleteAllRecordsFromTable {
  310. }
  311. #pragma mark - helper
  312. - (BOOL)executeQuery:(const char *)SQL withParams:(NSArray *)params {
  313. SEG_MUST_NOT_BE_MAIN_THREAD();
  314. sqlite3_stmt *statement = [self prepareSQL:SQL];
  315. if (!statement) {
  316. return NO;
  317. }
  318. [self bindStringsToStatement:statement stringArray:params];
  319. if (sqlite3_step(statement) != SQLITE_DONE) {
  320. return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];
  321. }
  322. sqlite3_finalize(statement);
  323. return YES;
  324. }
  325. /// Params only accept TEXT format string.
  326. - (BOOL)bindStringsToStatement:(sqlite3_stmt *)statement stringArray:(NSArray *)array {
  327. int index = 1;
  328. for (NSString *param in array) {
  329. if (![self bindStringToStatement:statement index:index string:param]) {
  330. return [self logErrorWithSQL:nil finalizeStatement:statement returnValue:NO];
  331. }
  332. index++;
  333. }
  334. return YES;
  335. }
  336. - (BOOL)bindStringToStatement:(sqlite3_stmt *)statement index:(int)index string:(NSString *)value {
  337. if (sqlite3_bind_text(statement, index, [value UTF8String], -1, SQLITE_TRANSIENT) != SQLITE_OK) {
  338. return [self logErrorWithSQL:nil finalizeStatement:statement returnValue:NO];
  339. }
  340. return YES;
  341. }
  342. - (sqlite3_stmt *)prepareSQL:(const char *)SQL {
  343. sqlite3_stmt *statement = nil;
  344. if (sqlite3_prepare_v2(_database, SQL, -1, &statement, NULL) != SQLITE_OK) {
  345. [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];
  346. return nil;
  347. }
  348. return statement;
  349. }
  350. - (NSString *)errorMessage {
  351. return [NSString stringWithFormat:@"%s", sqlite3_errmsg(_database)];
  352. }
  353. - (int)errorCode {
  354. return sqlite3_errcode(_database);
  355. }
  356. - (void)logDatabaseError {
  357. FIRLogError(kFIRLoggerSegmentation, @"I-SEG000015", @"Error message: %@. Error code: %d.",
  358. [self errorMessage], [self errorCode]);
  359. }
  360. - (BOOL)logErrorWithSQL:(const char *)SQL
  361. finalizeStatement:(sqlite3_stmt *)statement
  362. returnValue:(BOOL)returnValue {
  363. if (SQL) {
  364. FIRLogError(kFIRLoggerSegmentation, @"I-SEG000016", @"Failed with SQL: %s.", SQL);
  365. } else {
  366. const char *sqlString = sqlite3_sql(statement);
  367. NSString *sql;
  368. if (sqlString != NULL) {
  369. sql = [NSString stringWithCString:sqlString encoding:NSUTF8StringEncoding];
  370. }
  371. if (sql) {
  372. FIRLogError(kFIRLoggerSegmentation, @"I-SEG000016", @"Failed with SQL: %s.", SQL);
  373. }
  374. }
  375. [self logDatabaseError];
  376. if (statement) {
  377. sqlite3_finalize(statement);
  378. }
  379. return returnValue;
  380. }
  381. @end