SEGDatabaseManager.m 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  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 application support sub-directory that the Segmentation database resides in.
  20. static NSString *const kApplicationSupportSubDirectory = @"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 SegmentationAddSkipBackupAttributeToItemAtPath(NSString *filePathString) {
  31. NSURL *URL = [NSURL fileURLWithPath:filePathString];
  32. assert([[NSFileManager defaultManager] fileExistsAtPath:[URL path]]);
  33. NSError *error = nil;
  34. BOOL success = [URL setResourceValue:[NSNumber numberWithBool:YES]
  35. forKey:NSURLIsExcludedFromBackupKey
  36. error:&error];
  37. if (!success) {
  38. // TODO(dmandar): log error.
  39. NSLog(@"Error excluding %@ from backup %@.", [URL lastPathComponent], error);
  40. }
  41. return success;
  42. }
  43. static BOOL SegmentationCreateFilePathIfNotExist(NSString *filePath) {
  44. if (!filePath || !filePath.length) {
  45. // TODO(dmandar) log error.
  46. NSLog(@"Failed to create subdirectory for an empty file path.");
  47. return NO;
  48. }
  49. NSFileManager *fileManager = [NSFileManager defaultManager];
  50. if (![fileManager fileExistsAtPath:filePath]) {
  51. NSError *error;
  52. [fileManager createDirectoryAtPath:[filePath stringByDeletingLastPathComponent]
  53. withIntermediateDirectories:YES
  54. attributes:nil
  55. error:&error];
  56. if (error) {
  57. // TODO(dmandar) log error.
  58. NSLog(@"Failed to create subdirectory for database file: %@.", error);
  59. return NO;
  60. }
  61. }
  62. return YES;
  63. }
  64. @interface SEGDatabaseManager () {
  65. /// Database storing all the config information.
  66. sqlite3 *_database;
  67. /// Serial queue for database read/write operations.
  68. dispatch_queue_t _databaseOperationQueue;
  69. }
  70. @end
  71. @implementation SEGDatabaseManager
  72. + (instancetype)sharedInstance {
  73. static dispatch_once_t onceToken;
  74. static SEGDatabaseManager *sharedInstance;
  75. dispatch_once(&onceToken, ^{
  76. sharedInstance = [[SEGDatabaseManager alloc] init];
  77. });
  78. return sharedInstance;
  79. }
  80. - (instancetype)init {
  81. self = [super init];
  82. if (self) {
  83. _databaseOperationQueue =
  84. dispatch_queue_create("com.google.firebasesegmentation.database", DISPATCH_QUEUE_SERIAL);
  85. }
  86. return self;
  87. }
  88. #pragma mark - Public Methods
  89. - (void)loadMainTableWithCompletion:(SEGRequestCompletion)completionHandler {
  90. __weak SEGDatabaseManager *weakSelf = self;
  91. dispatch_async(_databaseOperationQueue, ^{
  92. SEGDatabaseManager *strongSelf = weakSelf;
  93. if (!strongSelf) {
  94. completionHandler(NO, @{@"Database Error" : @"Internal database error"});
  95. }
  96. // Read the database into memory.
  97. NSDictionary<NSString *, NSDictionary<NSString *, NSString *> *> *associations =
  98. [self loadMainTable];
  99. completionHandler(YES, associations);
  100. });
  101. return;
  102. }
  103. - (void)createOrOpenDatabaseWithCompletion:(SEGRequestCompletion)completionHandler {
  104. __weak SEGDatabaseManager *weakSelf = self;
  105. dispatch_async(_databaseOperationQueue, ^{
  106. SEGDatabaseManager *strongSelf = weakSelf;
  107. if (!strongSelf) {
  108. completionHandler(NO, @{@"ErrorDescription" : @"Internal database error"});
  109. }
  110. NSString *dbPath = [SEGDatabaseManager pathForSegmentationDatabase];
  111. // TODO(dmandar) log.
  112. NSLog(@"Loading segmentation database at path %@", dbPath);
  113. const char *databasePath = dbPath.UTF8String;
  114. // Create or open database path.
  115. if (!SegmentationCreateFilePathIfNotExist(dbPath)) {
  116. completionHandler(NO, @{@"ErrorDescription" : @"Could not create database file at path"});
  117. }
  118. int flags = SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE | SQLITE_OPEN_FILEPROTECTION_COMPLETE |
  119. SQLITE_OPEN_FULLMUTEX;
  120. if (sqlite3_open_v2(databasePath, &strongSelf->_database, flags, NULL) == SQLITE_OK) {
  121. // Create table if does not exist already.
  122. if ([strongSelf createTableSchema]) {
  123. // DB file created or already exists.
  124. // Exclude the app data used from iCloud backup.
  125. SegmentationAddSkipBackupAttributeToItemAtPath(dbPath);
  126. // Read the database into memory.
  127. NSDictionary<NSString *, NSString *> *associations = [self loadMainTable];
  128. completionHandler(YES, associations);
  129. } else {
  130. // Remove database before fail.
  131. [strongSelf removeDatabase:dbPath];
  132. FIRLogError(kFIRLoggerSegmentation, @"I-SEG000010", @"Failed to create table.");
  133. // Create a new database if existing database file is corrupted.
  134. if (!SegmentationCreateFilePathIfNotExist(dbPath)) {
  135. completionHandler(NO,
  136. @{@"ErrorDescription" : @"Could not recreate database file at path"});
  137. }
  138. if (sqlite3_open_v2(databasePath, &strongSelf->_database, flags, NULL) == SQLITE_OK) {
  139. if (![strongSelf createTableSchema]) {
  140. // Remove database before fail.
  141. [strongSelf removeDatabase:dbPath];
  142. // If it failed again, there's nothing we can do here.
  143. FIRLogError(kFIRLoggerSegmentation, @"I-SEG000010", @"Failed to create table.");
  144. } else {
  145. // Exclude the app data used from iCloud backup.
  146. SegmentationAddSkipBackupAttributeToItemAtPath(dbPath);
  147. }
  148. } else {
  149. [strongSelf logDatabaseError];
  150. completionHandler(NO, @{@"ErrorDescription" : @"Could not create database."});
  151. }
  152. }
  153. } else {
  154. [strongSelf logDatabaseError];
  155. completionHandler(NO, @{@"ErrorDescription" : @"Error creating database."});
  156. }
  157. });
  158. }
  159. - (void)removeDatabase:(NSString *)path completion:(SEGRequestCompletion)completionHandler {
  160. __weak SEGDatabaseManager *weakSelf = self;
  161. dispatch_async(_databaseOperationQueue, ^{
  162. SEGDatabaseManager *strongSelf = weakSelf;
  163. if (!strongSelf) {
  164. return;
  165. }
  166. [strongSelf removeDatabase:path];
  167. });
  168. }
  169. #pragma mark - Private Methods
  170. - (NSDictionary *)loadMainTable {
  171. NSString *SQLQuery = [NSString
  172. stringWithFormat:@"SELECT %@, %@, %@, %@ FROM %@", kMainTableColumnApplicationIdentifier,
  173. kMainTableColumnCustomInstallationIdentifier,
  174. kMainTableColumnFirebaseInstallationIdentifier,
  175. kMainTableColumnAssociationStatus, kMainTableName];
  176. sqlite3_stmt *statement = [self prepareSQL:[SQLQuery cStringUsingEncoding:NSUTF8StringEncoding]];
  177. if (!statement) {
  178. return nil;
  179. }
  180. NSMutableDictionary<NSString *, NSDictionary<NSString *, NSString *> *> *associations =
  181. [[NSMutableDictionary alloc] init];
  182. while (sqlite3_step(statement) == SQLITE_ROW) {
  183. NSString *firebaseApplicationName =
  184. [[NSString alloc] initWithUTF8String:(char *)sqlite3_column_text(statement, 0)];
  185. NSString *customInstallationIdentifier =
  186. [[NSString alloc] initWithUTF8String:(char *)sqlite3_column_text(statement, 1)];
  187. NSString *firebaseInstallationIdentifier =
  188. [[NSString alloc] initWithUTF8String:(char *)sqlite3_column_text(statement, 2)];
  189. NSString *associationStatus =
  190. [[NSString alloc] initWithUTF8String:(char *)sqlite3_column_text(statement, 3)];
  191. NSDictionary<NSString *, NSString *> *associationData = @{
  192. kSEGCustomInstallationIdentifierKey : customInstallationIdentifier,
  193. kSEGFirebaseInstallationIdentifierKey : firebaseInstallationIdentifier,
  194. kSEGAssociationStatusKey : associationStatus
  195. };
  196. [associations setObject:associationData forKey:firebaseApplicationName];
  197. }
  198. sqlite3_finalize(statement);
  199. return associations;
  200. }
  201. /// Returns the current version of the Remote Config database.
  202. + (NSString *)pathForSegmentationDatabase {
  203. NSArray<NSString *> *dirPaths =
  204. NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES);
  205. NSString *appSupportPath = dirPaths.firstObject;
  206. NSArray<NSString *> *components =
  207. @[ appSupportPath, kApplicationSupportSubDirectory, 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. __weak SEGDatabaseManager *weakSelf = self;
  253. dispatch_async(_databaseOperationQueue, ^{
  254. NSArray<NSString *> *values =
  255. [[NSArray alloc] initWithObjects:firebaseApplication, customInstanceIdentifier,
  256. firebaseInstanceIdentifier, associationStatus, nil];
  257. BOOL success = [weakSelf insertMainTableWithValues:values];
  258. if (handler) {
  259. dispatch_async(dispatch_get_main_queue(), ^{
  260. handler(success, nil);
  261. });
  262. }
  263. });
  264. }
  265. - (BOOL)insertMainTableWithValues:(NSArray<NSString *> *)values {
  266. SEG_MUST_NOT_BE_MAIN_THREAD();
  267. if (values.count != 4) {
  268. FIRLogError(kFIRLoggerSegmentation, @"I-SEG000013",
  269. @"Failed to insert config record. Wrong number of give parameters, current "
  270. @"number is %ld, correct number is 4.",
  271. (long)values.count);
  272. return NO;
  273. }
  274. NSString *SQL = [NSString stringWithFormat:@"INSERT INTO %@ (%@, %@, %@, %@) values (?, ?, ?, ?)",
  275. kMainTableName, kMainTableColumnApplicationIdentifier,
  276. kMainTableColumnCustomInstallationIdentifier,
  277. kMainTableColumnFirebaseInstallationIdentifier,
  278. kMainTableColumnAssociationStatus];
  279. sqlite3_stmt *statement = [self prepareSQL:[SQL UTF8String]];
  280. if (!statement) {
  281. return NO;
  282. }
  283. NSString *aString = values[0];
  284. if (![self bindStringToStatement:statement index:1 string:aString]) {
  285. return [self logErrorWithSQL:[SQL UTF8String] finalizeStatement:statement returnValue:NO];
  286. }
  287. aString = values[1];
  288. if (![self bindStringToStatement:statement index:2 string:aString]) {
  289. return [self logErrorWithSQL:[SQL UTF8String] finalizeStatement:statement returnValue:NO];
  290. }
  291. aString = values[2];
  292. if (![self bindStringToStatement:statement index:3 string:aString]) {
  293. return [self logErrorWithSQL:[SQL UTF8String] finalizeStatement:statement returnValue:NO];
  294. }
  295. aString = values[3];
  296. if (![self bindStringToStatement:statement index:4 string:aString]) {
  297. return [self logErrorWithSQL:[SQL UTF8String] finalizeStatement:statement returnValue:NO];
  298. }
  299. if (sqlite3_step(statement) != SQLITE_DONE) {
  300. return [self logErrorWithSQL:[SQL UTF8String] finalizeStatement:statement returnValue:NO];
  301. }
  302. sqlite3_finalize(statement);
  303. return YES;
  304. }
  305. /// TODO: (Check if required). Clear the record of given namespace and package name
  306. /// before updating the table.
  307. - (void)deleteRecordFromMainTableWithCustomInstanceIdentifier:
  308. (nonnull NSString *)customInstanceIdentifier {
  309. }
  310. /// TODO: (Check if required). Remove all the records from a config content table.
  311. - (void)deleteAllRecordsFromTable {
  312. }
  313. #pragma mark - helper
  314. - (BOOL)executeQuery:(const char *)SQL withParams:(NSArray *)params {
  315. SEG_MUST_NOT_BE_MAIN_THREAD();
  316. sqlite3_stmt *statement = [self prepareSQL:SQL];
  317. if (!statement) {
  318. return NO;
  319. }
  320. [self bindStringsToStatement:statement stringArray:params];
  321. if (sqlite3_step(statement) != SQLITE_DONE) {
  322. return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];
  323. }
  324. sqlite3_finalize(statement);
  325. return YES;
  326. }
  327. /// Params only accept TEXT format string.
  328. - (BOOL)bindStringsToStatement:(sqlite3_stmt *)statement stringArray:(NSArray *)array {
  329. int index = 1;
  330. for (NSString *param in array) {
  331. if (![self bindStringToStatement:statement index:index string:param]) {
  332. return [self logErrorWithSQL:nil finalizeStatement:statement returnValue:NO];
  333. }
  334. index++;
  335. }
  336. return YES;
  337. }
  338. - (BOOL)bindStringToStatement:(sqlite3_stmt *)statement index:(int)index string:(NSString *)value {
  339. if (sqlite3_bind_text(statement, index, [value UTF8String], -1, SQLITE_TRANSIENT) != SQLITE_OK) {
  340. return [self logErrorWithSQL:nil finalizeStatement:statement returnValue:NO];
  341. }
  342. return YES;
  343. }
  344. - (sqlite3_stmt *)prepareSQL:(const char *)SQL {
  345. sqlite3_stmt *statement = nil;
  346. if (sqlite3_prepare_v2(_database, SQL, -1, &statement, NULL) != SQLITE_OK) {
  347. [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];
  348. return nil;
  349. }
  350. return statement;
  351. }
  352. - (NSString *)errorMessage {
  353. return [NSString stringWithFormat:@"%s", sqlite3_errmsg(_database)];
  354. }
  355. - (int)errorCode {
  356. return sqlite3_errcode(_database);
  357. }
  358. - (void)logDatabaseError {
  359. FIRLogError(kFIRLoggerSegmentation, @"I-SEG000015", @"Error message: %@. Error code: %d.",
  360. [self errorMessage], [self errorCode]);
  361. }
  362. - (BOOL)logErrorWithSQL:(const char *)SQL
  363. finalizeStatement:(sqlite3_stmt *)statement
  364. returnValue:(BOOL)returnValue {
  365. if (SQL) {
  366. FIRLogError(kFIRLoggerSegmentation, @"I-SEG000016", @"Failed with SQL: %s.", SQL);
  367. }
  368. [self logDatabaseError];
  369. if (statement) {
  370. sqlite3_finalize(statement);
  371. }
  372. return returnValue;
  373. }
  374. @end