SEGDatabaseManager.m 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  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 *, NSDictionary<NSString *, NSString *> *> *associations =
  119. [self loadMainTable];
  120. completionHandler(YES, associations);
  121. } else {
  122. // Remove database before fail.
  123. [self removeDatabase:dbPath];
  124. FIRLogError(kFIRLoggerSegmentation, @"I-SEG00006", @"Failed to create table.");
  125. // Create a new database if existing database file is corrupted.
  126. if (!SEGCreateFilePathIfNotExist(dbPath)) {
  127. NSString *description =
  128. [NSString stringWithFormat:@"Could not recreate database file at path: %@", dbPath];
  129. completionHandler(NO, @{kSEGErrorDescription : description});
  130. return;
  131. }
  132. // Try to open the database with the new file.
  133. if (sqlite3_open_v2(databasePath, &self->_database, flags, NULL) == SQLITE_OK) {
  134. if (![self createTableSchema]) {
  135. // Remove database before fail.
  136. [self removeDatabase:dbPath];
  137. // If it failed again, there's nothing we can do here.
  138. FIRLogError(kFIRLoggerSegmentation, @"I-SEG00007", @"Failed to create table.");
  139. completionHandler(NO, @{kSEGErrorDescription : @"Failed to re-open new database file"});
  140. } else {
  141. // Exclude the app data used from iCloud backup.
  142. SEGAddSkipBackupAttributeToItemAtPath(dbPath);
  143. // Skip reading the db into memory, since it's empty.
  144. completionHandler(YES, @{});
  145. }
  146. } else {
  147. [self logDatabaseError];
  148. completionHandler(NO, @{kSEGErrorDescription : @"Could not create database."});
  149. }
  150. }
  151. } else {
  152. [self logDatabaseError];
  153. completionHandler(NO, @{kSEGErrorDescription : @"Error creating database."});
  154. }
  155. });
  156. }
  157. - (void)removeDatabase:(NSString *)path completion:(SEGRequestCompletion)completionHandler {
  158. dispatch_async(_databaseOperationQueue, ^{
  159. [self removeDatabase:path];
  160. completionHandler(YES, nil);
  161. });
  162. }
  163. #pragma mark - Private Methods
  164. - (NSDictionary<NSString *, NSDictionary<NSString *, NSString *> *> *)loadMainTable {
  165. NSString *SQLQuery = [NSString
  166. stringWithFormat:@"SELECT %@, %@, %@, %@ FROM %@", kMainTableColumnApplicationIdentifier,
  167. kMainTableColumnCustomInstallationIdentifier,
  168. kMainTableColumnFirebaseInstallationIdentifier,
  169. kMainTableColumnAssociationStatus, kMainTableName];
  170. sqlite3_stmt *statement = [self prepareSQL:[SQLQuery cStringUsingEncoding:NSUTF8StringEncoding]];
  171. if (!statement) {
  172. FIRLogError(kFIRLoggerSegmentation, @"I-SEG00008",
  173. @"Failed to create sqlite statement with query: %@.", SQLQuery);
  174. return nil;
  175. }
  176. NSMutableDictionary<NSString *, NSDictionary<NSString *, NSString *> *> *associations =
  177. [[NSMutableDictionary alloc] init];
  178. while (sqlite3_step(statement) == SQLITE_ROW) {
  179. NSString *firebaseApplicationName =
  180. [[NSString alloc] initWithUTF8String:(char *)sqlite3_column_text(statement, 0)];
  181. NSString *customInstallationIdentifier =
  182. [[NSString alloc] initWithUTF8String:(char *)sqlite3_column_text(statement, 1)];
  183. NSString *firebaseInstallationIdentifier =
  184. [[NSString alloc] initWithUTF8String:(char *)sqlite3_column_text(statement, 2)];
  185. NSString *associationStatus =
  186. [[NSString alloc] initWithUTF8String:(char *)sqlite3_column_text(statement, 3)];
  187. NSDictionary<NSString *, NSString *> *associationData = @{
  188. kSEGCustomInstallationIdentifierKey : customInstallationIdentifier,
  189. kSEGFirebaseInstallationIdentifierKey : firebaseInstallationIdentifier,
  190. kSEGAssociationStatusKey : associationStatus
  191. };
  192. [associations setObject:associationData forKey:firebaseApplicationName];
  193. }
  194. sqlite3_finalize(statement);
  195. return associations;
  196. }
  197. /// Returns the current version of the Remote Config database.
  198. + (NSString *)pathForSegmentationDatabase {
  199. #if TARGET_OS_TV
  200. NSArray *dirPaths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
  201. #else
  202. NSArray *dirPaths =
  203. NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES);
  204. #endif
  205. NSString *storageDir = dirPaths.firstObject;
  206. NSArray<NSString *> *components =
  207. @[ storageDir, kSegmentationStorageSubDirectory, kDatabaseName ];
  208. return [NSString pathWithComponents:components];
  209. }
  210. - (BOOL)createTableSchema {
  211. SEG_MUST_NOT_BE_MAIN_THREAD();
  212. NSString *mainTableSchema =
  213. [NSString stringWithFormat:@"create TABLE IF NOT EXISTS %@ (_id INTEGER PRIMARY KEY, %@ "
  214. @"TEXT, %@ TEXT, %@ TEXT, %@ TEXT)",
  215. kMainTableName, kMainTableColumnApplicationIdentifier,
  216. kMainTableColumnCustomInstallationIdentifier,
  217. kMainTableColumnFirebaseInstallationIdentifier,
  218. kMainTableColumnAssociationStatus];
  219. return [self executeQuery:[mainTableSchema cStringUsingEncoding:NSUTF8StringEncoding]];
  220. }
  221. - (void)removeDatabase:(NSString *)path {
  222. SEG_MUST_NOT_BE_MAIN_THREAD();
  223. if (sqlite3_close(self->_database) != SQLITE_OK) {
  224. [self logDatabaseError];
  225. }
  226. self->_database = nil;
  227. NSFileManager *fileManager = [NSFileManager defaultManager];
  228. NSError *error;
  229. if (![fileManager removeItemAtPath:path error:&error]) {
  230. FIRLogError(kFIRLoggerSegmentation, @"I-SEG000011",
  231. @"Failed to remove database at path %@ for error %@.", path, error);
  232. }
  233. }
  234. #pragma mark - execute
  235. - (BOOL)executeQuery:(const char *)SQL {
  236. SEG_MUST_NOT_BE_MAIN_THREAD();
  237. char *error;
  238. if (sqlite3_exec(_database, SQL, nil, nil, &error) != SQLITE_OK) {
  239. FIRLogError(kFIRLoggerSegmentation, @"I-SEG000012", @"Failed to execute query with error %s.",
  240. error);
  241. return NO;
  242. }
  243. return YES;
  244. }
  245. #pragma mark - insert
  246. - (void)insertMainTableApplicationNamed:(NSString *)firebaseApplication
  247. customInstanceIdentifier:(NSString *)customInstanceIdentifier
  248. firebaseInstanceIdentifier:(NSString *)firebaseInstanceIdentifier
  249. associationStatus:(NSString *)associationStatus
  250. completionHandler:(SEGRequestCompletion)handler {
  251. // TODO: delete the row first.
  252. dispatch_async(_databaseOperationQueue, ^{
  253. NSArray<NSString *> *values = @[
  254. firebaseApplication, customInstanceIdentifier, firebaseInstanceIdentifier, associationStatus
  255. ];
  256. BOOL success = [self insertMainTableWithValues:values];
  257. if (handler) {
  258. dispatch_async(dispatch_get_main_queue(), ^{
  259. handler(success, nil);
  260. });
  261. }
  262. });
  263. }
  264. - (BOOL)insertMainTableWithValues:(NSArray<NSString *> *)values {
  265. SEG_MUST_NOT_BE_MAIN_THREAD();
  266. if (values.count != 4) {
  267. FIRLogError(kFIRLoggerSegmentation, @"I-SEG000013",
  268. @"Failed to insert config record. Wrong number of give parameters, current "
  269. @"number is %ld, correct number is 4.",
  270. (long)values.count);
  271. return NO;
  272. }
  273. NSString *SQL = [NSString stringWithFormat:@"INSERT INTO %@ (%@, %@, %@, %@) values (?, ?, ?, ?)",
  274. kMainTableName, kMainTableColumnApplicationIdentifier,
  275. kMainTableColumnCustomInstallationIdentifier,
  276. kMainTableColumnFirebaseInstallationIdentifier,
  277. kMainTableColumnAssociationStatus];
  278. sqlite3_stmt *statement = [self prepareSQL:[SQL UTF8String]];
  279. if (!statement) {
  280. return NO;
  281. }
  282. NSString *aString = values[0];
  283. if (![self bindStringToStatement:statement index:1 string:aString]) {
  284. return [self logErrorWithSQL:[SQL UTF8String] finalizeStatement:statement returnValue:NO];
  285. }
  286. aString = values[1];
  287. if (![self bindStringToStatement:statement index:2 string:aString]) {
  288. return [self logErrorWithSQL:[SQL UTF8String] finalizeStatement:statement returnValue:NO];
  289. }
  290. aString = values[2];
  291. if (![self bindStringToStatement:statement index:3 string:aString]) {
  292. return [self logErrorWithSQL:[SQL UTF8String] finalizeStatement:statement returnValue:NO];
  293. }
  294. aString = values[3];
  295. if (![self bindStringToStatement:statement index:4 string:aString]) {
  296. return [self logErrorWithSQL:[SQL UTF8String] finalizeStatement:statement returnValue:NO];
  297. }
  298. if (sqlite3_step(statement) != SQLITE_DONE) {
  299. return [self logErrorWithSQL:[SQL UTF8String] finalizeStatement:statement returnValue:NO];
  300. }
  301. sqlite3_finalize(statement);
  302. return YES;
  303. }
  304. /// TODO: (Check if required). Clear the record of given namespace and package name
  305. /// before updating the table.
  306. - (void)deleteRecordFromMainTableWithCustomInstanceIdentifier:
  307. (nonnull NSString *)customInstanceIdentifier {
  308. }
  309. /// TODO: (Check if required). Remove all the records from a config content table.
  310. - (void)deleteAllRecordsFromTable {
  311. }
  312. #pragma mark - helper
  313. - (BOOL)executeQuery:(const char *)SQL withParams:(NSArray *)params {
  314. SEG_MUST_NOT_BE_MAIN_THREAD();
  315. sqlite3_stmt *statement = [self prepareSQL:SQL];
  316. if (!statement) {
  317. return NO;
  318. }
  319. [self bindStringsToStatement:statement stringArray:params];
  320. if (sqlite3_step(statement) != SQLITE_DONE) {
  321. return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];
  322. }
  323. sqlite3_finalize(statement);
  324. return YES;
  325. }
  326. /// Params only accept TEXT format string.
  327. - (BOOL)bindStringsToStatement:(sqlite3_stmt *)statement stringArray:(NSArray *)array {
  328. int index = 1;
  329. for (NSString *param in array) {
  330. if (![self bindStringToStatement:statement index:index string:param]) {
  331. return [self logErrorWithSQL:nil finalizeStatement:statement returnValue:NO];
  332. }
  333. index++;
  334. }
  335. return YES;
  336. }
  337. - (BOOL)bindStringToStatement:(sqlite3_stmt *)statement index:(int)index string:(NSString *)value {
  338. if (sqlite3_bind_text(statement, index, [value UTF8String], -1, SQLITE_TRANSIENT) != SQLITE_OK) {
  339. return [self logErrorWithSQL:nil finalizeStatement:statement returnValue:NO];
  340. }
  341. return YES;
  342. }
  343. - (sqlite3_stmt *)prepareSQL:(const char *)SQL {
  344. sqlite3_stmt *statement = nil;
  345. if (sqlite3_prepare_v2(_database, SQL, -1, &statement, NULL) != SQLITE_OK) {
  346. [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];
  347. return nil;
  348. }
  349. return statement;
  350. }
  351. - (NSString *)errorMessage {
  352. return [NSString stringWithFormat:@"%s", sqlite3_errmsg(_database)];
  353. }
  354. - (int)errorCode {
  355. return sqlite3_errcode(_database);
  356. }
  357. - (void)logDatabaseError {
  358. FIRLogError(kFIRLoggerSegmentation, @"I-SEG000015", @"Error message: %@. Error code: %d.",
  359. [self errorMessage], [self errorCode]);
  360. }
  361. - (BOOL)logErrorWithSQL:(const char *)SQL
  362. finalizeStatement:(sqlite3_stmt *)statement
  363. returnValue:(BOOL)returnValue {
  364. if (SQL) {
  365. FIRLogError(kFIRLoggerSegmentation, @"I-SEG000016", @"Failed with SQL: %s.", SQL);
  366. } else {
  367. const char *sqlString = sqlite3_sql(statement);
  368. NSString *sql;
  369. if (sqlString != NULL) {
  370. sql = [NSString stringWithCString:sqlString encoding:NSUTF8StringEncoding];
  371. }
  372. if (sql) {
  373. FIRLogError(kFIRLoggerSegmentation, @"I-SEG000016", @"Failed with SQL: %s.", SQL);
  374. }
  375. }
  376. [self logDatabaseError];
  377. if (statement) {
  378. sqlite3_finalize(statement);
  379. }
  380. return returnValue;
  381. }
  382. @end