AutorÃa | Ultima modificación | Ver Log |
/** 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 <sqlite3.h>#import "FirebaseRemoteConfig/Sources/RCNConfigDBManager.h"#import "FirebaseRemoteConfig/Sources/RCNConfigDefines.h"#import "FirebaseRemoteConfig/Sources/RCNConfigValue_Internal.h"#import "FirebaseCore/Sources/Private/FirebaseCoreInternal.h"/// Using macro for securely preprocessing string concatenation in query before runtime.#define RCNTableNameMain "main"#define RCNTableNameMainActive "main_active"#define RCNTableNameMainDefault "main_default"#define RCNTableNameMetadataDeprecated "fetch_metadata"#define RCNTableNameMetadata "fetch_metadata_v2"#define RCNTableNameInternalMetadata "internal_metadata"#define RCNTableNameExperiment "experiment"#define RCNTableNamePersonalization "personalization"static BOOL gIsNewDatabase;/// SQLite file name in versions 0, 1 and 2.static NSString *const RCNDatabaseName = @"RemoteConfig.sqlite3";/// The storage sub-directory that the Remote Config database resides in.static NSString *const RCNRemoteConfigStorageSubDirectory = @"Google/RemoteConfig";/// Remote Config database path for deprecated V0 version.static NSString *RemoteConfigPathForOldDatabaseV0() {NSArray *dirPaths =NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);NSString *docPath = dirPaths.firstObject;return [docPath stringByAppendingPathComponent:RCNDatabaseName];}/// Remote Config database path for current database.static NSString *RemoteConfigPathForDatabase(void) {#if TARGET_OS_TVNSArray *dirPaths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);#elseNSArray *dirPaths =NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES);#endifNSString *storageDirPath = dirPaths.firstObject;NSArray *components = @[ storageDirPath, RCNRemoteConfigStorageSubDirectory, RCNDatabaseName ];return [NSString pathWithComponents:components];}static BOOL RemoteConfigAddSkipBackupAttributeToItemAtPath(NSString *filePathString) {NSURL *URL = [NSURL fileURLWithPath:filePathString];assert([[NSFileManager defaultManager] fileExistsAtPath:[URL path]]);NSError *error = nil;BOOL success = [URL setResourceValue:[NSNumber numberWithBool:YES]forKey:NSURLIsExcludedFromBackupKeyerror:&error];if (!success) {FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000017", @"Error excluding %@ from backup %@.",[URL lastPathComponent], error);}return success;}static BOOL RemoteConfigCreateFilePathIfNotExist(NSString *filePath) {if (!filePath || !filePath.length) {FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000018",@"Failed to create subdirectory for an empty file path.");return NO;}NSFileManager *fileManager = [NSFileManager defaultManager];if (![fileManager fileExistsAtPath:filePath]) {gIsNewDatabase = YES;NSError *error;[fileManager createDirectoryAtPath:[filePath stringByDeletingLastPathComponent]withIntermediateDirectories:YESattributes:nilerror:&error];if (error) {FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000019",@"Failed to create subdirectory for database file: %@.", error);return NO;}}return YES;}static NSArray *RemoteConfigMetadataTableColumnsInOrder() {return @[RCNKeyBundleIdentifier, RCNKeyNamespace, RCNKeyFetchTime, RCNKeyDigestPerNamespace,RCNKeyDeviceContext, RCNKeyAppContext, RCNKeySuccessFetchTime, RCNKeyFailureFetchTime,RCNKeyLastFetchStatus, RCNKeyLastFetchError, RCNKeyLastApplyTime, RCNKeyLastSetDefaultsTime];}@interface RCNConfigDBManager () {/// Database storing all the config information.sqlite3 *_database;/// Serial queue for database read/write operations.dispatch_queue_t _databaseOperationQueue;}@end@implementation RCNConfigDBManager+ (instancetype)sharedInstance {static dispatch_once_t onceToken;static RCNConfigDBManager *sharedInstance;dispatch_once(&onceToken, ^{sharedInstance = [[RCNConfigDBManager alloc] init];});return sharedInstance;}/// Returns the current version of the Remote Config database.+ (NSString *)remoteConfigPathForDatabase {return RemoteConfigPathForDatabase();}- (instancetype)init {self = [super init];if (self) {_databaseOperationQueue =dispatch_queue_create("com.google.GoogleConfigService.database", DISPATCH_QUEUE_SERIAL);[self createOrOpenDatabase];}return self;}#pragma mark - database- (void)migrateV1NamespaceToV2Namespace {for (int table = 0; table < 3; table++) {NSString *tableName = @"" RCNTableNameMain;switch (table) {case 1:tableName = @"" RCNTableNameMainActive;break;case 2:tableName = @"" RCNTableNameMainDefault;break;default:break;}NSString *SQLString = [NSStringstringWithFormat:@"SELECT namespace FROM %@ WHERE namespace NOT LIKE '%%:%%'", tableName];const char *SQL = [SQLString UTF8String];sqlite3_stmt *statement = [self prepareSQL:SQL];if (!statement) {return;}NSMutableArray<NSString *> *namespaceArray = [[NSMutableArray alloc] init];while (sqlite3_step(statement) == SQLITE_ROW) {NSString *configNamespace =[[NSString alloc] initWithUTF8String:(char *)sqlite3_column_text(statement, 0)];[namespaceArray addObject:configNamespace];}sqlite3_finalize(statement);// Update.for (NSString *namespaceToUpdate in namespaceArray) {NSString *newNamespace =[NSString stringWithFormat:@"%@:%@", namespaceToUpdate, kFIRDefaultAppName];NSString *updateSQLString =[NSString stringWithFormat:@"UPDATE %@ SET namespace = ? WHERE namespace = ?", tableName];const char *updateSQL = [updateSQLString UTF8String];sqlite3_stmt *updateStatement = [self prepareSQL:updateSQL];if (!updateStatement) {return;}NSArray<NSString *> *updateParams = @[ newNamespace, namespaceToUpdate ];[self bindStringsToStatement:updateStatement stringArray:updateParams];int result = sqlite3_step(updateStatement);if (result != SQLITE_DONE) {[self logErrorWithSQL:SQL finalizeStatement:updateStatement returnValue:NO];return;}sqlite3_finalize(updateStatement);}}}- (void)createOrOpenDatabase {__weak RCNConfigDBManager *weakSelf = self;dispatch_async(_databaseOperationQueue, ^{RCNConfigDBManager *strongSelf = weakSelf;if (!strongSelf) {return;}NSString *oldV0DBPath = RemoteConfigPathForOldDatabaseV0();// Backward Compatibilityif ([[NSFileManager defaultManager] fileExistsAtPath:oldV0DBPath]) {FIRLogInfo(kFIRLoggerRemoteConfig, @"I-RCN000009",@"Old database V0 exists, removed it and replace with the new one.");[strongSelf removeDatabase:oldV0DBPath];}NSString *dbPath = [RCNConfigDBManager remoteConfigPathForDatabase];FIRLogInfo(kFIRLoggerRemoteConfig, @"I-RCN000062", @"Loading database at path %@", dbPath);const char *databasePath = dbPath.UTF8String;// Create or open database path.if (!RemoteConfigCreateFilePathIfNotExist(dbPath)) {return;}int flags = SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE |SQLITE_OPEN_FILEPROTECTION_COMPLETEUNTILFIRSTUSERAUTHENTICATION |SQLITE_OPEN_FULLMUTEX;if (sqlite3_open_v2(databasePath, &strongSelf->_database, flags, NULL) == SQLITE_OK) {// Always try to create table if not exists for backward compatibility.if (![strongSelf createTableSchema]) {// Remove database before fail.[strongSelf removeDatabase:dbPath];FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000010", @"Failed to create table.");// Create a new database if existing database file is corrupted.if (!RemoteConfigCreateFilePathIfNotExist(dbPath)) {return;}if (sqlite3_open_v2(databasePath, &strongSelf->_database, flags, NULL) == SQLITE_OK) {if (![strongSelf createTableSchema]) {// Remove database before fail.[strongSelf removeDatabase:dbPath];// If it failed again, there's nothing we can do here.FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000010", @"Failed to create table.");} else {// Exclude the app data used from iCloud backup.RemoteConfigAddSkipBackupAttributeToItemAtPath(dbPath);}} else {[strongSelf logDatabaseError];}} else {// DB file already exists. Migrate any V1 namespace column entries to V2 fully qualified// 'namespace:FIRApp' entries.[self migrateV1NamespaceToV2Namespace];// Exclude the app data used from iCloud backup.RemoteConfigAddSkipBackupAttributeToItemAtPath(dbPath);}} else {[strongSelf logDatabaseError];}});}- (BOOL)createTableSchema {RCN_MUST_NOT_BE_MAIN_THREAD();static const char *createTableMain ="create TABLE IF NOT EXISTS " RCNTableNameMain" (_id INTEGER PRIMARY KEY, bundle_identifier TEXT, namespace TEXT, key TEXT, value BLOB)";static const char *createTableMainActive ="create TABLE IF NOT EXISTS " RCNTableNameMainActive" (_id INTEGER PRIMARY KEY, bundle_identifier TEXT, namespace TEXT, key TEXT, value BLOB)";static const char *createTableMainDefault ="create TABLE IF NOT EXISTS " RCNTableNameMainDefault" (_id INTEGER PRIMARY KEY, bundle_identifier TEXT, namespace TEXT, key TEXT, value BLOB)";static const char *createTableMetadata ="create TABLE IF NOT EXISTS " RCNTableNameMetadata" (_id INTEGER PRIMARY KEY, bundle_identifier TEXT, namespace TEXT,"" fetch_time INTEGER, digest_per_ns BLOB, device_context BLOB, app_context BLOB, ""success_fetch_time BLOB, failure_fetch_time BLOB, last_fetch_status INTEGER, ""last_fetch_error INTEGER, last_apply_time INTEGER, last_set_defaults_time INTEGER)";static const char *createTableInternalMetadata ="create TABLE IF NOT EXISTS " RCNTableNameInternalMetadata" (_id INTEGER PRIMARY KEY, key TEXT, value BLOB)";static const char *createTableExperiment = "create TABLE IF NOT EXISTS " RCNTableNameExperiment" (_id INTEGER PRIMARY KEY, key TEXT, value BLOB)";static const char *createTablePersonalization ="create TABLE IF NOT EXISTS " RCNTableNamePersonalization" (_id INTEGER PRIMARY KEY, key INTEGER, value BLOB)";return [self executeQuery:createTableMain] && [self executeQuery:createTableMainActive] &&[self executeQuery:createTableMainDefault] && [self executeQuery:createTableMetadata] &&[self executeQuery:createTableInternalMetadata] &&[self executeQuery:createTableExperiment] &&[self executeQuery:createTablePersonalization];}- (void)removeDatabaseOnDatabaseQueueAtPath:(NSString *)path {__weak RCNConfigDBManager *weakSelf = self;dispatch_sync(_databaseOperationQueue, ^{RCNConfigDBManager *strongSelf = weakSelf;if (!strongSelf) {return;}if (sqlite3_close(strongSelf->_database) != SQLITE_OK) {[self logDatabaseError];}strongSelf->_database = nil;NSFileManager *fileManager = [NSFileManager defaultManager];NSError *error;if (![fileManager removeItemAtPath:path error:&error]) {FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000011",@"Failed to remove database at path %@ for error %@.", path, error);}});}- (void)removeDatabase:(NSString *)path {if (sqlite3_close(_database) != SQLITE_OK) {[self logDatabaseError];}_database = nil;NSFileManager *fileManager = [NSFileManager defaultManager];NSError *error;if (![fileManager removeItemAtPath:path error:&error]) {FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000011",@"Failed to remove database at path %@ for error %@.", path, error);}}#pragma mark - execute- (BOOL)executeQuery:(const char *)SQL {RCN_MUST_NOT_BE_MAIN_THREAD();char *error;if (sqlite3_exec(_database, SQL, nil, nil, &error) != SQLITE_OK) {FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000012", @"Failed to execute query with error %s.",error);return NO;}return YES;}#pragma mark - insert- (void)insertMetadataTableWithValues:(NSDictionary *)columnNameToValuecompletionHandler:(RCNDBCompletion)handler {__weak RCNConfigDBManager *weakSelf = self;dispatch_async(_databaseOperationQueue, ^{BOOL success = [weakSelf insertMetadataTableWithValues:columnNameToValue];if (handler) {dispatch_async(dispatch_get_main_queue(), ^{handler(success, nil);});}});}- (BOOL)insertMetadataTableWithValues:(NSDictionary *)columnNameToValue {RCN_MUST_NOT_BE_MAIN_THREAD();static const char *SQL ="INSERT INTO " RCNTableNameMetadata" (bundle_identifier, namespace, fetch_time, digest_per_ns, device_context, ""app_context, success_fetch_time, failure_fetch_time, last_fetch_status, ""last_fetch_error, last_apply_time, last_set_defaults_time) values (?, ?, ?, ?, ?, ?, ""?, ?, ?, ?, ?, ?)";sqlite3_stmt *statement = [self prepareSQL:SQL];if (!statement) {[self logErrorWithSQL:SQL finalizeStatement:nil returnValue:NO];return NO;}NSArray *columns = RemoteConfigMetadataTableColumnsInOrder();int index = 0;for (NSString *columnName in columns) {if ([columnName isEqualToString:RCNKeyBundleIdentifier] ||[columnName isEqualToString:RCNKeyNamespace]) {NSString *value = columnNameToValue[columnName];if (![self bindStringToStatement:statement index:++index string:value]) {return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];}} else if ([columnName isEqualToString:RCNKeyFetchTime] ||[columnName isEqualToString:RCNKeyLastApplyTime] ||[columnName isEqualToString:RCNKeyLastSetDefaultsTime]) {double value = [columnNameToValue[columnName] doubleValue];if (sqlite3_bind_double(statement, ++index, value) != SQLITE_OK) {return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];}} else if ([columnName isEqualToString:RCNKeyLastFetchStatus] ||[columnName isEqualToString:RCNKeyLastFetchError]) {int value = [columnNameToValue[columnName] intValue];if (sqlite3_bind_int(statement, ++index, value) != SQLITE_OK) {return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];}} else {NSData *data = columnNameToValue[columnName];if (sqlite3_bind_blob(statement, ++index, data.bytes, (int)data.length, NULL) != SQLITE_OK) {return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];}}}if (sqlite3_step(statement) != SQLITE_DONE) {return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];}sqlite3_finalize(statement);return YES;}- (void)insertMainTableWithValues:(NSArray *)valuesfromSource:(RCNDBSource)sourcecompletionHandler:(RCNDBCompletion)handler {__weak RCNConfigDBManager *weakSelf = self;dispatch_async(_databaseOperationQueue, ^{BOOL success = [weakSelf insertMainTableWithValues:values fromSource:source];if (handler) {dispatch_async(dispatch_get_main_queue(), ^{handler(success, nil);});}});}- (BOOL)insertMainTableWithValues:(NSArray *)values fromSource:(RCNDBSource)source {RCN_MUST_NOT_BE_MAIN_THREAD();if (values.count != 4) {FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000013",@"Failed to insert config record. Wrong number of give parameters, current "@"number is %ld, correct number is 4.",(long)values.count);return NO;}const char *SQL = "INSERT INTO " RCNTableNameMain" (bundle_identifier, namespace, key, value) values (?, ?, ?, ?)";if (source == RCNDBSourceDefault) {SQL = "INSERT INTO " RCNTableNameMainDefault" (bundle_identifier, namespace, key, value) values (?, ?, ?, ?)";} else if (source == RCNDBSourceActive) {SQL = "INSERT INTO " RCNTableNameMainActive" (bundle_identifier, namespace, key, value) values (?, ?, ?, ?)";}sqlite3_stmt *statement = [self prepareSQL:SQL];if (!statement) {return NO;}NSString *aString = values[0];if (![self bindStringToStatement:statement index:1 string:aString]) {return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];}aString = values[1];if (![self bindStringToStatement:statement index:2 string:aString]) {return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];}aString = values[2];if (![self bindStringToStatement:statement index:3 string:aString]) {return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];}NSData *blobData = values[3];if (sqlite3_bind_blob(statement, 4, blobData.bytes, (int)blobData.length, NULL) != SQLITE_OK) {return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];}if (sqlite3_step(statement) != SQLITE_DONE) {return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];}sqlite3_finalize(statement);return YES;}- (void)insertInternalMetadataTableWithValues:(NSArray *)valuescompletionHandler:(RCNDBCompletion)handler {__weak RCNConfigDBManager *weakSelf = self;dispatch_async(_databaseOperationQueue, ^{BOOL success = [weakSelf insertInternalMetadataWithValues:values];if (handler) {dispatch_async(dispatch_get_main_queue(), ^{handler(success, nil);});}});}- (BOOL)insertInternalMetadataWithValues:(NSArray *)values {RCN_MUST_NOT_BE_MAIN_THREAD();if (values.count != 2) {return NO;}const char *SQL ="INSERT OR REPLACE INTO " RCNTableNameInternalMetadata " (key, value) values (?, ?)";sqlite3_stmt *statement = [self prepareSQL:SQL];if (!statement) {return NO;}NSString *aString = values[0];if (![self bindStringToStatement:statement index:1 string:aString]) {[self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];return NO;}NSData *blobData = values[1];if (sqlite3_bind_blob(statement, 2, blobData.bytes, (int)blobData.length, NULL) != SQLITE_OK) {[self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];return NO;}if (sqlite3_step(statement) != SQLITE_DONE) {[self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];return NO;}sqlite3_finalize(statement);return YES;}- (void)insertExperimentTableWithKey:(NSString *)keyvalue:(NSData *)serializedValuecompletionHandler:(RCNDBCompletion)handler {dispatch_async(_databaseOperationQueue, ^{BOOL success = [self insertExperimentTableWithKey:key value:serializedValue];if (handler) {dispatch_async(dispatch_get_main_queue(), ^{handler(success, nil);});}});}- (BOOL)insertExperimentTableWithKey:(NSString *)key value:(NSData *)dataValue {if ([key isEqualToString:@RCNExperimentTableKeyMetadata]) {return [self updateExperimentMetadata:dataValue];}RCN_MUST_NOT_BE_MAIN_THREAD();const char *SQL = "INSERT INTO " RCNTableNameExperiment " (key, value) values (?, ?)";sqlite3_stmt *statement = [self prepareSQL:SQL];if (!statement) {return NO;}if (![self bindStringToStatement:statement index:1 string:key]) {return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];}if (sqlite3_bind_blob(statement, 2, dataValue.bytes, (int)dataValue.length, NULL) != SQLITE_OK) {return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];}if (sqlite3_step(statement) != SQLITE_DONE) {return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];}sqlite3_finalize(statement);return YES;}- (BOOL)updateExperimentMetadata:(NSData *)dataValue {RCN_MUST_NOT_BE_MAIN_THREAD();const char *SQL = "INSERT OR REPLACE INTO " RCNTableNameExperiment" (_id, key, value) values ((SELECT _id from " RCNTableNameExperiment" WHERE key = ?), ?, ?)";sqlite3_stmt *statement = [self prepareSQL:SQL];if (!statement) {return NO;}if (![self bindStringToStatement:statement index:1 string:@RCNExperimentTableKeyMetadata]) {return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];}if (![self bindStringToStatement:statement index:2 string:@RCNExperimentTableKeyMetadata]) {return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];}if (sqlite3_bind_blob(statement, 3, dataValue.bytes, (int)dataValue.length, NULL) != SQLITE_OK) {return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];}if (sqlite3_step(statement) != SQLITE_DONE) {return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];}sqlite3_finalize(statement);return YES;}- (BOOL)insertOrUpdatePersonalizationConfig:(NSDictionary *)dataValuefromSource:(RCNDBSource)source {RCN_MUST_NOT_BE_MAIN_THREAD();NSError *error;NSData *JSONPayload = [NSJSONSerialization dataWithJSONObject:dataValueoptions:NSJSONWritingPrettyPrintederror:&error];if (!JSONPayload || error) {FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000075",@"Invalid Personalization payload to be serialized.");}const char *SQL = "INSERT OR REPLACE INTO " RCNTableNamePersonalization" (_id, key, value) values ((SELECT _id from " RCNTableNamePersonalization" WHERE key = ?), ?, ?)";sqlite3_stmt *statement = [self prepareSQL:SQL];if (!statement) {return NO;}if (sqlite3_bind_int(statement, 1, (int)source) != SQLITE_OK) {return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];}if (sqlite3_bind_int(statement, 2, (int)source) != SQLITE_OK) {return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];}if (sqlite3_bind_blob(statement, 3, JSONPayload.bytes, (int)JSONPayload.length, NULL) !=SQLITE_OK) {return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];}if (sqlite3_step(statement) != SQLITE_DONE) {return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];}sqlite3_finalize(statement);return YES;}#pragma mark - update- (void)updateMetadataWithOption:(RCNUpdateOption)optionnamespace:(NSString *)namespacevalues:(NSArray *)valuescompletionHandler:(RCNDBCompletion)handler {dispatch_async(_databaseOperationQueue, ^{BOOL success = [self updateMetadataTableWithOption:option namespace:namespace andValues:values];if (handler) {dispatch_async(dispatch_get_main_queue(), ^{handler(success, nil);});}});}- (BOOL)updateMetadataTableWithOption:(RCNUpdateOption)optionnamespace:(NSString *)namespaceandValues:(NSArray *)values {RCN_MUST_NOT_BE_MAIN_THREAD();static const char *SQL ="UPDATE " RCNTableNameMetadata " (last_fetch_status, last_fetch_error, last_apply_time, ""last_set_defaults_time) values (?, ?, ?, ?) WHERE namespace = ?";if (option == RCNUpdateOptionFetchStatus) {SQL = "UPDATE " RCNTableNameMetadata" SET last_fetch_status = ?, last_fetch_error = ? WHERE namespace = ?";} else if (option == RCNUpdateOptionApplyTime) {SQL = "UPDATE " RCNTableNameMetadata " SET last_apply_time = ? WHERE namespace = ?";} else if (option == RCNUpdateOptionDefaultTime) {SQL = "UPDATE " RCNTableNameMetadata " SET last_set_defaults_time = ? WHERE namespace = ?";} else {return NO;}sqlite3_stmt *statement = [self prepareSQL:SQL];if (!statement) {return NO;}int index = 0;if ((option == RCNUpdateOptionApplyTime || option == RCNUpdateOptionDefaultTime) &&values.count == 1) {double value = [values[0] doubleValue];if (sqlite3_bind_double(statement, ++index, value) != SQLITE_OK) {return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];}} else if (option == RCNUpdateOptionFetchStatus && values.count == 2) {int value = [values[0] intValue];if (sqlite3_bind_int(statement, ++index, value) != SQLITE_OK) {return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];}value = [values[1] intValue];if (sqlite3_bind_int(statement, ++index, value) != SQLITE_OK) {return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];}}// bind namespace to queryif (sqlite3_bind_text(statement, ++index, [namespace UTF8String], -1, SQLITE_TRANSIENT) !=SQLITE_OK) {return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];}if (sqlite3_step(statement) != SQLITE_DONE) {return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];}sqlite3_finalize(statement);return YES;}#pragma mark - read from DB- (NSDictionary *)loadMetadataWithBundleIdentifier:(NSString *)bundleIdentifiernamespace:(NSString *)namespace {__block NSDictionary *metadataTableResult;__weak RCNConfigDBManager *weakSelf = self;dispatch_sync(_databaseOperationQueue, ^{metadataTableResult = [weakSelf loadMetadataTableWithBundleIdentifier:bundleIdentifiernamespace:namespace];});if (metadataTableResult) {return metadataTableResult;}return [[NSDictionary alloc] init];}- (NSMutableDictionary *)loadMetadataTableWithBundleIdentifier:(NSString *)bundleIdentifiernamespace:(NSString *)namespace {NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];const char *SQL ="SELECT bundle_identifier, fetch_time, digest_per_ns, device_context, app_context, ""success_fetch_time, failure_fetch_time , last_fetch_status, ""last_fetch_error, last_apply_time, last_set_defaults_time FROM " RCNTableNameMetadata" WHERE bundle_identifier = ? and namespace = ?";sqlite3_stmt *statement = [self prepareSQL:SQL];if (!statement) {return nil;}NSArray *params = @[ bundleIdentifier, namespace ];[self bindStringsToStatement:statement stringArray:params];while (sqlite3_step(statement) == SQLITE_ROW) {NSString *dbBundleIdentifier =[[NSString alloc] initWithUTF8String:(char *)sqlite3_column_text(statement, 0)];if (dbBundleIdentifier && ![dbBundleIdentifier isEqualToString:bundleIdentifier]) {FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000014",@"Load Metadata from table error: Wrong package name %@, should be %@.",dbBundleIdentifier, bundleIdentifier);return nil;}double fetchTime = sqlite3_column_double(statement, 1);NSData *digestPerNamespace = [NSData dataWithBytes:(char *)sqlite3_column_blob(statement, 2)length:sqlite3_column_bytes(statement, 2)];NSData *deviceContext = [NSData dataWithBytes:(char *)sqlite3_column_blob(statement, 3)length:sqlite3_column_bytes(statement, 3)];NSData *appContext = [NSData dataWithBytes:(char *)sqlite3_column_blob(statement, 4)length:sqlite3_column_bytes(statement, 4)];NSData *successTimeDigest = [NSData dataWithBytes:(char *)sqlite3_column_blob(statement, 5)length:sqlite3_column_bytes(statement, 5)];NSData *failureTimeDigest = [NSData dataWithBytes:(char *)sqlite3_column_blob(statement, 6)length:sqlite3_column_bytes(statement, 6)];int lastFetchStatus = sqlite3_column_int(statement, 7);int lastFetchFailReason = sqlite3_column_int(statement, 8);double lastApplyTimestamp = sqlite3_column_double(statement, 9);double lastSetDefaultsTimestamp = sqlite3_column_double(statement, 10);NSError *error;NSMutableDictionary *deviceContextDict = nil;if (deviceContext) {deviceContextDict = [NSJSONSerialization JSONObjectWithData:deviceContextoptions:NSJSONReadingMutableContainerserror:&error];}NSMutableDictionary *appContextDict = nil;if (appContext) {appContextDict = [NSJSONSerialization JSONObjectWithData:appContextoptions:NSJSONReadingMutableContainerserror:&error];}NSMutableDictionary<NSString *, id> *digestPerNamespaceDictionary = nil;if (digestPerNamespace) {digestPerNamespaceDictionary =[NSJSONSerialization JSONObjectWithData:digestPerNamespaceoptions:NSJSONReadingMutableContainerserror:&error];}NSMutableArray *successTimes = nil;if (successTimeDigest) {successTimes = [NSJSONSerialization JSONObjectWithData:successTimeDigestoptions:NSJSONReadingMutableContainerserror:&error];}NSMutableArray *failureTimes = nil;if (failureTimeDigest) {failureTimes = [NSJSONSerialization JSONObjectWithData:failureTimeDigestoptions:NSJSONReadingMutableContainerserror:&error];}dict[RCNKeyBundleIdentifier] = bundleIdentifier;dict[RCNKeyFetchTime] = @(fetchTime);dict[RCNKeyDigestPerNamespace] = digestPerNamespaceDictionary;dict[RCNKeyDeviceContext] = deviceContextDict;dict[RCNKeyAppContext] = appContextDict;dict[RCNKeySuccessFetchTime] = successTimes;dict[RCNKeyFailureFetchTime] = failureTimes;dict[RCNKeyLastFetchStatus] = @(lastFetchStatus);dict[RCNKeyLastFetchError] = @(lastFetchFailReason);dict[RCNKeyLastApplyTime] = @(lastApplyTimestamp);dict[RCNKeyLastSetDefaultsTime] = @(lastSetDefaultsTimestamp);break;}sqlite3_finalize(statement);return dict;}- (void)loadExperimentWithCompletionHandler:(RCNDBCompletion)handler {__weak RCNConfigDBManager *weakSelf = self;dispatch_async(_databaseOperationQueue, ^{RCNConfigDBManager *strongSelf = weakSelf;if (!strongSelf) {return;}NSMutableArray *experimentPayloads =[strongSelf loadExperimentTableFromKey:@RCNExperimentTableKeyPayload];if (!experimentPayloads) {experimentPayloads = [[NSMutableArray alloc] init];}NSMutableDictionary *experimentMetadata;NSMutableArray *experiments =[strongSelf loadExperimentTableFromKey:@RCNExperimentTableKeyMetadata];// There should be only one entry for experiment metadata.if (experiments.count > 0) {NSError *error;experimentMetadata = [NSJSONSerialization JSONObjectWithData:experiments[0]options:NSJSONReadingMutableContainerserror:&error];}if (!experimentMetadata) {experimentMetadata = [[NSMutableDictionary alloc] init];}if (handler) {dispatch_async(dispatch_get_main_queue(), ^{handler(YES, @{@RCNExperimentTableKeyPayload : [experimentPayloads copy],@RCNExperimentTableKeyMetadata : [experimentMetadata copy]});});}});}- (NSMutableArray<NSData *> *)loadExperimentTableFromKey:(NSString *)key {RCN_MUST_NOT_BE_MAIN_THREAD();NSMutableArray *results = [[NSMutableArray alloc] init];const char *SQL = "SELECT value FROM " RCNTableNameExperiment " WHERE key = ?";sqlite3_stmt *statement = [self prepareSQL:SQL];if (!statement) {return nil;}NSArray *params = @[ key ];[self bindStringsToStatement:statement stringArray:params];NSData *experimentData;while (sqlite3_step(statement) == SQLITE_ROW) {experimentData = [NSData dataWithBytes:(char *)sqlite3_column_blob(statement, 0)length:sqlite3_column_bytes(statement, 0)];if (experimentData) {[results addObject:experimentData];}}sqlite3_finalize(statement);return results;}- (void)loadPersonalizationWithCompletionHandler:(RCNDBLoadCompletion)handler {__weak RCNConfigDBManager *weakSelf = self;dispatch_async(_databaseOperationQueue, ^{RCNConfigDBManager *strongSelf = weakSelf;if (!strongSelf) {dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{handler(NO, [NSMutableDictionary new], [NSMutableDictionary new], nil);});return;}NSDictionary *activePersonalization;NSData *personalizationResult = [strongSelf loadPersonalizationTableFromKey:RCNDBSourceActive];// There should be only one entry for Personalization metadata.if (personalizationResult) {NSError *error;activePersonalization = [NSJSONSerialization JSONObjectWithData:personalizationResultoptions:0error:&error];}if (!activePersonalization) {activePersonalization = [[NSMutableDictionary alloc] init];}NSDictionary *fetchedPersonalization;personalizationResult = [strongSelf loadPersonalizationTableFromKey:RCNDBSourceFetched];// There should be only one entry for Personalization metadata.if (personalizationResult) {NSError *error;fetchedPersonalization = [NSJSONSerialization JSONObjectWithData:personalizationResultoptions:0error:&error];}if (!fetchedPersonalization) {fetchedPersonalization = [[NSMutableDictionary alloc] init];}if (handler) {dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{handler(YES, fetchedPersonalization, activePersonalization, nil);});}});}- (NSData *)loadPersonalizationTableFromKey:(int)key {RCN_MUST_NOT_BE_MAIN_THREAD();NSMutableArray *results = [[NSMutableArray alloc] init];const char *SQL = "SELECT value FROM " RCNTableNamePersonalization " WHERE key = ?";sqlite3_stmt *statement = [self prepareSQL:SQL];if (!statement) {return nil;}if (sqlite3_bind_int(statement, 1, key) != SQLITE_OK) {[self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO];return nil;}NSData *personalizationData;while (sqlite3_step(statement) == SQLITE_ROW) {personalizationData = [NSData dataWithBytes:(char *)sqlite3_column_blob(statement, 0)length:sqlite3_column_bytes(statement, 0)];if (personalizationData) {[results addObject:personalizationData];}}sqlite3_finalize(statement);// There should be only one entry in this table.if (results.count != 1) {return nil;}return results[0];}- (NSDictionary *)loadInternalMetadataTable {__block NSMutableDictionary *internalMetadataTableResult;__weak RCNConfigDBManager *weakSelf = self;dispatch_sync(_databaseOperationQueue, ^{internalMetadataTableResult = [weakSelf loadInternalMetadataTableInternal];});return internalMetadataTableResult;}- (NSMutableDictionary *)loadInternalMetadataTableInternal {NSMutableDictionary *internalMetadata = [[NSMutableDictionary alloc] init];const char *SQL = "SELECT key, value FROM " RCNTableNameInternalMetadata;sqlite3_stmt *statement = [self prepareSQL:SQL];if (!statement) {return nil;}while (sqlite3_step(statement) == SQLITE_ROW) {NSString *key = [[NSString alloc] initWithUTF8String:(char *)sqlite3_column_text(statement, 0)];NSData *dataValue = [NSData dataWithBytes:(char *)sqlite3_column_blob(statement, 1)length:sqlite3_column_bytes(statement, 1)];internalMetadata[key] = dataValue;}sqlite3_finalize(statement);return internalMetadata;}/// This method is only meant to be called at init time. The underlying logic will need to be/// revaluated if the assumption changes at a later time.- (void)loadMainWithBundleIdentifier:(NSString *)bundleIdentifiercompletionHandler:(RCNDBLoadCompletion)handler {__weak RCNConfigDBManager *weakSelf = self;dispatch_async(_databaseOperationQueue, ^{RCNConfigDBManager *strongSelf = weakSelf;if (!strongSelf) {dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{handler(NO, [NSDictionary new], [NSDictionary new], [NSDictionary new]);});return;}__block NSDictionary *fetchedConfig =[strongSelf loadMainTableWithBundleIdentifier:bundleIdentifierfromSource:RCNDBSourceFetched];__block NSDictionary *activeConfig =[strongSelf loadMainTableWithBundleIdentifier:bundleIdentifierfromSource:RCNDBSourceActive];__block NSDictionary *defaultConfig =[strongSelf loadMainTableWithBundleIdentifier:bundleIdentifierfromSource:RCNDBSourceDefault];if (handler) {dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{fetchedConfig = fetchedConfig ? fetchedConfig : [[NSDictionary alloc] init];activeConfig = activeConfig ? activeConfig : [[NSDictionary alloc] init];defaultConfig = defaultConfig ? defaultConfig : [[NSDictionary alloc] init];handler(YES, fetchedConfig, activeConfig, defaultConfig);});}});}- (NSMutableDictionary *)loadMainTableWithBundleIdentifier:(NSString *)bundleIdentifierfromSource:(RCNDBSource)source {NSMutableDictionary *namespaceToConfig = [[NSMutableDictionary alloc] init];const char *SQL = "SELECT bundle_identifier, namespace, key, value FROM " RCNTableNameMain" WHERE bundle_identifier = ?";if (source == RCNDBSourceDefault) {SQL = "SELECT bundle_identifier, namespace, key, value FROM " RCNTableNameMainDefault" WHERE bundle_identifier = ?";} else if (source == RCNDBSourceActive) {SQL = "SELECT bundle_identifier, namespace, key, value FROM " RCNTableNameMainActive" WHERE bundle_identifier = ?";}NSArray *params = @[ bundleIdentifier ];sqlite3_stmt *statement = [self prepareSQL:SQL];if (!statement) {return nil;}[self bindStringsToStatement:statement stringArray:params];while (sqlite3_step(statement) == SQLITE_ROW) {NSString *configNamespace =[[NSString alloc] initWithUTF8String:(char *)sqlite3_column_text(statement, 1)];NSString *key = [[NSString alloc] initWithUTF8String:(char *)sqlite3_column_text(statement, 2)];NSData *value = [NSData dataWithBytes:(char *)sqlite3_column_blob(statement, 3)length:sqlite3_column_bytes(statement, 3)];if (!namespaceToConfig[configNamespace]) {namespaceToConfig[configNamespace] = [[NSMutableDictionary alloc] init];}if (source == RCNDBSourceDefault) {namespaceToConfig[configNamespace][key] =[[FIRRemoteConfigValue alloc] initWithData:value source:FIRRemoteConfigSourceDefault];} else {namespaceToConfig[configNamespace][key] =[[FIRRemoteConfigValue alloc] initWithData:value source:FIRRemoteConfigSourceRemote];}}sqlite3_finalize(statement);return namespaceToConfig;}#pragma mark - delete- (void)deleteRecordFromMainTableWithNamespace:(NSString *)namespace_pbundleIdentifier:(NSString *)bundleIdentifierfromSource:(RCNDBSource)source {__weak RCNConfigDBManager *weakSelf = self;dispatch_async(_databaseOperationQueue, ^{RCNConfigDBManager *strongSelf = weakSelf;if (!strongSelf) {return;}NSArray *params = @[ bundleIdentifier, namespace_p ];const char *SQL ="DELETE FROM " RCNTableNameMain " WHERE bundle_identifier = ? and namespace = ?";if (source == RCNDBSourceDefault) {SQL = "DELETE FROM " RCNTableNameMainDefault " WHERE bundle_identifier = ? and namespace = ?";} else if (source == RCNDBSourceActive) {SQL = "DELETE FROM " RCNTableNameMainActive " WHERE bundle_identifier = ? and namespace = ?";}[strongSelf executeQuery:SQL withParams:params];});}- (void)deleteRecordWithBundleIdentifier:(NSString *)bundleIdentifiernamespace:(NSString *)namespaceisInternalDB:(BOOL)isInternalDB {__weak RCNConfigDBManager *weakSelf = self;dispatch_async(_databaseOperationQueue, ^{RCNConfigDBManager *strongSelf = weakSelf;if (!strongSelf) {return;}const char *SQL = "DELETE FROM " RCNTableNameInternalMetadata " WHERE key LIKE ?";NSArray *params = @[ bundleIdentifier ];if (!isInternalDB) {SQL = "DELETE FROM " RCNTableNameMetadata " WHERE bundle_identifier = ? and namespace = ?";params = @[ bundleIdentifier, namespace ];}[strongSelf executeQuery:SQL withParams:params];});}- (void)deleteAllRecordsFromTableWithSource:(RCNDBSource)source {__weak RCNConfigDBManager *weakSelf = self;dispatch_async(_databaseOperationQueue, ^{RCNConfigDBManager *strongSelf = weakSelf;if (!strongSelf) {return;}const char *SQL = "DELETE FROM " RCNTableNameMain;if (source == RCNDBSourceDefault) {SQL = "DELETE FROM " RCNTableNameMainDefault;} else if (source == RCNDBSourceActive) {SQL = "DELETE FROM " RCNTableNameMainActive;}[strongSelf executeQuery:SQL];});}- (void)deleteExperimentTableForKey:(NSString *)key {__weak RCNConfigDBManager *weakSelf = self;dispatch_async(_databaseOperationQueue, ^{RCNConfigDBManager *strongSelf = weakSelf;if (!strongSelf) {return;}NSArray *params = @[ key ];const char *SQL = "DELETE FROM " RCNTableNameExperiment " WHERE key = ?";[strongSelf executeQuery:SQL withParams:params];});}#pragma mark - helper- (BOOL)executeQuery:(const char *)SQL withParams:(NSArray *)params {RCN_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:nil 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(kFIRLoggerRemoteConfig, @"I-RCN000015", @"Error message: %@. Error code: %d.",[self errorMessage], [self errorCode]);}- (BOOL)logErrorWithSQL:(const char *)SQLfinalizeStatement:(sqlite3_stmt *)statementreturnValue:(BOOL)returnValue {if (SQL) {FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000016", @"Failed with SQL: %s.", SQL);}[self logDatabaseError];if (statement) {sqlite3_finalize(statement);}return returnValue;}- (BOOL)isNewDatabase {return gIsNewDatabase;}@end