| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424 |
- // Copyright 2019 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 "FirebaseSegmentation/Sources/SEGDatabaseManager.h"
- #import <sqlite3.h>
- #import "FirebaseCore/Sources/Private/FIRLogger.h"
- /// SQLite file name.
- static NSString *const kDatabaseName = @"FirebaseSegmentation.sqlite3";
- /// The storage sub-directory that the Segmentation database resides in.
- static NSString *const kSegmentationStorageSubDirectory = @"Google/FirebaseSegmentation";
- /// Column names
- static NSString *const kMainTableName = @"main";
- static NSString *const kMainTableColumnApplicationIdentifier = @"firebase_app_identifier";
- static NSString *const kMainTableColumnCustomInstallationIdentifier =
- @"custom_installation_identifier";
- static NSString *const kMainTableColumnFirebaseInstallationIdentifier =
- @"firebase_installation_identifier";
- static NSString *const kMainTableColumnAssociationStatus = @"association_status";
- // Exclude the database from iCloud backup.
- static BOOL SEGAddSkipBackupAttributeToItemAtPath(NSString *filePathString) {
- NSURL *URL = [NSURL fileURLWithPath:filePathString];
- assert([[NSFileManager defaultManager] fileExistsAtPath:URL.path]);
- NSError *error = nil;
- BOOL success = [URL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:&error];
- if (!success) {
- FIRLogError(kFIRLoggerSegmentation, @"I-SEG000001", @"Error excluding %@ from backup %@.",
- [URL lastPathComponent], error);
- }
- return success;
- }
- static BOOL SEGCreateFilePathIfNotExist(NSString *filePath) {
- if (!filePath.length) {
- FIRLogError(kFIRLoggerSegmentation, @"I-SEG000002",
- @"Failed to create subdirectory for an empty file path.");
- return NO;
- }
- NSFileManager *fileManager = [NSFileManager defaultManager];
- if (![fileManager fileExistsAtPath:filePath]) {
- NSError *error;
- [fileManager createDirectoryAtPath:[filePath stringByDeletingLastPathComponent]
- withIntermediateDirectories:YES
- attributes:nil
- error:&error];
- if (error) {
- FIRLogError(kFIRLoggerSegmentation, @"I-SEG000003",
- @"Failed to create subdirectory for database file: %@.", error);
- return NO;
- }
- }
- return YES;
- }
- @implementation SEGDatabaseManager {
- /// Database storing all the config information.
- sqlite3 *_database;
- /// Serial queue for database read/write operations.
- dispatch_queue_t _databaseOperationQueue;
- }
- + (instancetype)sharedInstance {
- static dispatch_once_t onceToken;
- static SEGDatabaseManager *sharedInstance;
- dispatch_once(&onceToken, ^{
- sharedInstance = [[SEGDatabaseManager alloc] init];
- });
- return sharedInstance;
- }
- - (instancetype)init {
- self = [super init];
- if (self) {
- _databaseOperationQueue =
- dispatch_queue_create("com.google.firebasesegmentation.database", DISPATCH_QUEUE_SERIAL);
- }
- return self;
- }
- #pragma mark - Public Methods
- - (void)loadMainTableWithCompletion:(SEGRequestCompletion)completionHandler {
- dispatch_async(_databaseOperationQueue, ^{
- // Read the database into memory.
- NSDictionary<NSString *, NSDictionary<NSString *, NSString *> *> *associations =
- [self loadMainTable];
- if (associations != nil) {
- completionHandler(YES, associations);
- } else {
- FIRLogError(kFIRLoggerSegmentation, @"I-SEG000004", @"Failed to load main table.");
- completionHandler(NO, @{});
- }
- });
- return;
- }
- - (void)createOrOpenDatabaseWithCompletion:(SEGRequestCompletion)completionHandler {
- dispatch_async(_databaseOperationQueue, ^{
- NSString *dbPath = [SEGDatabaseManager pathForSegmentationDatabase];
- FIRLogDebug(kFIRLoggerSegmentation, @"I-SEG000005", @"Loading segmentation database at path %@",
- dbPath);
- const char *databasePath = dbPath.UTF8String;
- // Create or open database path.
- if (!SEGCreateFilePathIfNotExist(dbPath)) {
- completionHandler(NO, @{kSEGErrorDescription : @"Could not create database file at path"});
- }
- int flags = SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE | SQLITE_OPEN_FILEPROTECTION_COMPLETE |
- SQLITE_OPEN_FULLMUTEX;
- if (sqlite3_open_v2(databasePath, &self->_database, flags, NULL) == SQLITE_OK) {
- // Create table if does not exist already.
- if ([self createTableSchema]) {
- // DB file created or already exists.
- // Exclude the app data used from iCloud backup.
- SEGAddSkipBackupAttributeToItemAtPath(dbPath);
- // Read the database into memory.
- NSDictionary<NSString *, NSString *> *associations = [self loadMainTable];
- completionHandler(YES, associations);
- } else {
- // Remove database before fail.
- [self removeDatabase:dbPath];
- FIRLogError(kFIRLoggerSegmentation, @"I-SEG00006", @"Failed to create table.");
- // Create a new database if existing database file is corrupted.
- if (!SEGCreateFilePathIfNotExist(dbPath)) {
- completionHandler(
- NO,
- @{kSEGErrorDescription : @"Could not recreate database file at path: %@", dbpath});
- return;
- }
- // Try to open the database with the new file.
- if (sqlite3_open_v2(databasePath, &self->_database, flags, NULL) == SQLITE_OK) {
- if (![self createTableSchema]) {
- // Remove database before fail.
- [self removeDatabase:dbPath];
- // If it failed again, there's nothing we can do here.
- FIRLogError(kFIRLoggerSegmentation, @"I-SEG00007", @"Failed to create table.");
- completionHandler(NO, @{kSEGErrorDescription : @"Failed to re-open new database file"});
- } else {
- // Exclude the app data used from iCloud backup.
- SEGAddSkipBackupAttributeToItemAtPath(dbPath);
- // Skip reading the db into memory, since it's empty.
- completionHandler(YES, @{});
- }
- } else {
- [self logDatabaseError];
- completionHandler(NO, @{kSEGErrorDescription : @"Could not create database."});
- }
- }
- } else {
- [self logDatabaseError];
- completionHandler(NO, @{kSEGErrorDescription : @"Error creating database."});
- }
- });
- }
- - (void)removeDatabase:(NSString *)path completion:(SEGRequestCompletion)completionHandler {
- dispatch_async(_databaseOperationQueue, ^{
- [self removeDatabase:path];
- completionHandler(YES, nil);
- });
- }
- #pragma mark - Private Methods
- - (NSDictionary *)loadMainTable {
- NSString *SQLQuery = [NSString
- stringWithFormat:@"SELECT %@, %@, %@, %@ FROM %@", kMainTableColumnApplicationIdentifier,
- kMainTableColumnCustomInstallationIdentifier,
- kMainTableColumnFirebaseInstallationIdentifier,
- kMainTableColumnAssociationStatus, kMainTableName];
- sqlite3_stmt *statement = [self prepareSQL:[SQLQuery cStringUsingEncoding:NSUTF8StringEncoding]];
- if (!statement) {
- FIRLogError(kFIRLoggerSegmentation, @"I-SEG00008",
- @"Failed to create sqlite statement with query: %@.", SQLQuery);
- return nil;
- }
- NSMutableDictionary<NSString *, NSDictionary<NSString *, NSString *> *> *associations =
- [[NSMutableDictionary alloc] init];
- while (sqlite3_step(statement) == SQLITE_ROW) {
- NSString *firebaseApplicationName =
- [[NSString alloc] initWithUTF8String:(char *)sqlite3_column_text(statement, 0)];
- NSString *customInstallationIdentifier =
- [[NSString alloc] initWithUTF8String:(char *)sqlite3_column_text(statement, 1)];
- NSString *firebaseInstallationIdentifier =
- [[NSString alloc] initWithUTF8String:(char *)sqlite3_column_text(statement, 2)];
- NSString *associationStatus =
- [[NSString alloc] initWithUTF8String:(char *)sqlite3_column_text(statement, 3)];
- NSDictionary<NSString *, NSString *> *associationData = @{
- kSEGCustomInstallationIdentifierKey : customInstallationIdentifier,
- kSEGFirebaseInstallationIdentifierKey : firebaseInstallationIdentifier,
- kSEGAssociationStatusKey : associationStatus
- };
- [associations setObject:associationData forKey:firebaseApplicationName];
- }
- sqlite3_finalize(statement);
- return associations;
- }
- /// Returns the current version of the Remote Config database.
- + (NSString *)pathForSegmentationDatabase {
- #if TARGET_OS_TV
- NSArray *dirPaths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
- #else
- NSArray *dirPaths =
- NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES);
- #endif
- NSString *storageDir = dirPaths.firstObject;
- NSArray<NSString *> *components =
- @[ storageDir, kSegmentationStorageSubDirectory, kDatabaseName ];
- return [NSString pathWithComponents:components];
- }
- - (BOOL)createTableSchema {
- SEG_MUST_NOT_BE_MAIN_THREAD();
- NSString *mainTableSchema =
- [NSString stringWithFormat:@"create TABLE IF NOT EXISTS %@ (_id INTEGER PRIMARY KEY, %@ "
- @"TEXT, %@ TEXT, %@ TEXT, %@ TEXT)",
- kMainTableName, kMainTableColumnApplicationIdentifier,
- kMainTableColumnCustomInstallationIdentifier,
- kMainTableColumnFirebaseInstallationIdentifier,
- kMainTableColumnAssociationStatus];
- return [self executeQuery:[mainTableSchema cStringUsingEncoding:NSUTF8StringEncoding]];
- }
- - (void)removeDatabase:(NSString *)path {
- SEG_MUST_NOT_BE_MAIN_THREAD();
- if (sqlite3_close(self->_database) != SQLITE_OK) {
- [self logDatabaseError];
- }
- self->_database = nil;
- NSFileManager *fileManager = [NSFileManager defaultManager];
- NSError *error;
- if (![fileManager removeItemAtPath:path error:&error]) {
- FIRLogError(kFIRLoggerSegmentation, @"I-SEG000011",
- @"Failed to remove database at path %@ for error %@.", path, error);
- }
- }
- #pragma mark - execute
- - (BOOL)executeQuery:(const char *)SQL {
- SEG_MUST_NOT_BE_MAIN_THREAD();
- char *error;
- if (sqlite3_exec(_database, SQL, nil, nil, &error) != SQLITE_OK) {
- FIRLogError(kFIRLoggerSegmentation, @"I-SEG000012", @"Failed to execute query with error %s.",
- error);
- return NO;
- }
- return YES;
- }
- #pragma mark - insert
- - (void)insertMainTableApplicationNamed:(NSString *)firebaseApplication
- customInstanceIdentifier:(NSString *)customInstanceIdentifier
- firebaseInstanceIdentifier:(NSString *)firebaseInstanceIdentifier
- associationStatus:(NSString *)associationStatus
- completionHandler:(SEGRequestCompletion)handler {
- // TODO: delete the row first.
- dispatch_async(_databaseOperationQueue, ^{
- NSArray<NSString *> *values = @[
- firebaseApplication, customInstanceIdentifier, firebaseInstanceIdentifier, associationStatus
- ];
- BOOL success = [self insertMainTableWithValues:values];
- if (handler) {
- dispatch_async(dispatch_get_main_queue(), ^{
- handler(success, nil);
- });
- }
- });
- }
- - (BOOL)insertMainTableWithValues:(NSArray<NSString *> *)values {
- SEG_MUST_NOT_BE_MAIN_THREAD();
- if (values.count != 4) {
- FIRLogError(kFIRLoggerSegmentation, @"I-SEG000013",
- @"Failed to insert config record. Wrong number of give parameters, current "
- @"number is %ld, correct number is 4.",
- (long)values.count);
- return NO;
- }
- NSString *SQL = [NSString stringWithFormat:@"INSERT INTO %@ (%@, %@, %@, %@) values (?, ?, ?, ?)",
- kMainTableName, kMainTableColumnApplicationIdentifier,
- kMainTableColumnCustomInstallationIdentifier,
- kMainTableColumnFirebaseInstallationIdentifier,
- kMainTableColumnAssociationStatus];
- sqlite3_stmt *statement = [self prepareSQL:[SQL UTF8String]];
- if (!statement) {
- return NO;
- }
- NSString *aString = values[0];
- if (![self bindStringToStatement:statement index:1 string:aString]) {
- return [self logErrorWithSQL:[SQL UTF8String] finalizeStatement:statement returnValue:NO];
- }
- aString = values[1];
- if (![self bindStringToStatement:statement index:2 string:aString]) {
- return [self logErrorWithSQL:[SQL UTF8String] finalizeStatement:statement returnValue:NO];
- }
- aString = values[2];
- if (![self bindStringToStatement:statement index:3 string:aString]) {
- return [self logErrorWithSQL:[SQL UTF8String] finalizeStatement:statement returnValue:NO];
- }
- aString = values[3];
- if (![self bindStringToStatement:statement index:4 string:aString]) {
- return [self logErrorWithSQL:[SQL UTF8String] finalizeStatement:statement returnValue:NO];
- }
- if (sqlite3_step(statement) != SQLITE_DONE) {
- return [self logErrorWithSQL:[SQL UTF8String] finalizeStatement:statement returnValue:NO];
- }
- sqlite3_finalize(statement);
- return YES;
- }
- /// TODO: (Check if required). Clear the record of given namespace and package name
- /// before updating the table.
- - (void)deleteRecordFromMainTableWithCustomInstanceIdentifier:
- (nonnull NSString *)customInstanceIdentifier {
- }
- /// TODO: (Check if required). Remove all the records from a config content table.
- - (void)deleteAllRecordsFromTable {
- }
- #pragma mark - helper
- - (BOOL)executeQuery:(const char *)SQL withParams:(NSArray *)params {
- SEG_MUST_NOT_BE_MAIN_THREAD();
- sqlite3_stmt *statement = [self prepareSQL:SQL];
- if (!statement) {
- return NO;
- }
- [self bindStringsToStatement:statement stringArray:params];
- if (sqlite3_step(statement) != SQLITE_DONE) {
- return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];
- }
- sqlite3_finalize(statement);
- return YES;
- }
- /// Params only accept TEXT format string.
- - (BOOL)bindStringsToStatement:(sqlite3_stmt *)statement stringArray:(NSArray *)array {
- int index = 1;
- for (NSString *param in array) {
- if (![self bindStringToStatement:statement index:index string:param]) {
- return [self logErrorWithSQL:sql finalizeStatement:statement returnValue:NO];
- }
- index++;
- }
- return YES;
- }
- - (BOOL)bindStringToStatement:(sqlite3_stmt *)statement index:(int)index string:(NSString *)value {
- if (sqlite3_bind_text(statement, index, [value UTF8String], -1, SQLITE_TRANSIENT) != SQLITE_OK) {
- return [self logErrorWithSQL:nil finalizeStatement:statement returnValue:NO];
- }
- return YES;
- }
- - (sqlite3_stmt *)prepareSQL:(const char *)SQL {
- sqlite3_stmt *statement = nil;
- if (sqlite3_prepare_v2(_database, SQL, -1, &statement, NULL) != SQLITE_OK) {
- [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];
- return nil;
- }
- return statement;
- }
- - (NSString *)errorMessage {
- return [NSString stringWithFormat:@"%s", sqlite3_errmsg(_database)];
- }
- - (int)errorCode {
- return sqlite3_errcode(_database);
- }
- - (void)logDatabaseError {
- FIRLogError(kFIRLoggerSegmentation, @"I-SEG000015", @"Error message: %@. Error code: %d.",
- [self errorMessage], [self errorCode]);
- }
- - (BOOL)logErrorWithSQL:(const char *)SQL
- finalizeStatement:(sqlite3_stmt *)statement
- returnValue:(BOOL)returnValue {
- if (SQL) {
- FIRLogError(kFIRLoggerSegmentation, @"I-SEG000016", @"Failed with SQL: %s.", SQL);
- } else {
- const char *sqlString = sqlite3_sql(statement);
- NSString *sql;
- if (sqlString != NULL) {
- sql = [NSString stringWithCString:sqlString encoding:NSUTF8StringEncoding];
- }
- if (sql) {
- FIRLogError(kFIRLoggerSegmentation, @"I-SEG000016", @"Failed with SQL: %s.", SQL);
- }
- }
- [self logDatabaseError];
- if (statement) {
- sqlite3_finalize(statement);
- }
- return returnValue;
- }
- @end
|