| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350 |
- // Copyright 2024 Google LLC
- //
- // 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 FirebaseCore
- import FirebaseCoreExtension
- import Foundation
- import SQLite3
- @objc public enum UpdateOption: Int {
- case applyTime
- case defaultTime
- case fetchStatus
- }
- /// Column names in metadata table
- let RCNKeyBundleIdentifier = "bundle_identifier"
- let RCNKeyNamespace = "namespace"
- let RCNKeyFetchTime = "fetch_time"
- let RCNKeyDigestPerNamespace = "digest_per_ns"
- let RCNKeyDeviceContext = "device_context"
- let RCNKeyAppContext = "app_context"
- let RCNKeySuccessFetchTime = "success_fetch_time"
- let RCNKeyFailureFetchTime = "failure_fetch_time"
- let RCNKeyLastFetchStatus = "last_fetch_status"
- let RCNKeyLastFetchError = "last_fetch_error"
- let RCNKeyLastApplyTime = "last_apply_time"
- let RCNKeyLastSetDefaultsTime = "last_set_defaults_time"
- /// SQLite file name in versions 0, 1 and 2.
- private let RCNDatabaseName = "RemoteConfig.sqlite3"
- /// The storage sub-directory that the Remote Config database resides in.
- private let RCNRemoteConfigStorageSubDirectory = "Google/RemoteConfig"
- // TODO: Delete all publics, opens, and objc's
- // TODO: Convert all callback functions to `async` functions
- // TODO: Consider deleting this file completely in favor of direct async calls to DatabaseActor.
- /// Persist config data in sqlite database on device. Managing data read/write from/to database.
- @objc(RCNConfigDBManager)
- open class ConfigDBManager: NSObject {
- /// Shared Singleton Instance
- @objc public static let sharedInstance = ConfigDBManager()
- let databaseActor: DatabaseActor
- @objc public var isNewDatabase: Bool
- @objc public init(dbPath: String = remoteConfigPathForDatabase()) {
- databaseActor = DatabaseActor(dbPath: dbPath)
- isNewDatabase = !FileManager.default.fileExists(atPath: dbPath)
- super.init()
- }
- /// Returns the current version of the Remote Config database.
- public static func remoteConfigPathForDatabase() -> String {
- #if os(tvOS)
- let dirPaths = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true)
- #else
- let dirPaths = NSSearchPathForDirectoriesInDomains(.applicationSupportDirectory,
- .userDomainMask, true)
- #endif
- let storageDirPath = dirPaths[0]
- let dbPath = URL(fileURLWithPath: storageDirPath)
- .appendingPathComponent(RCNRemoteConfigStorageSubDirectory)
- .appendingPathComponent(RCNDatabaseName).path
- return dbPath
- }
- // MARK: - Insert
- @objc public
- func insertMetadataTable(withValues columnNameToValue: [String: Any],
- completionHandler handler: ((Bool, [String: AnyHashable]?) -> Void)? =
- nil) {
- Task { // Use Task to call the actor method asynchronously
- let success = await self.databaseActor.insertMetadataTable(withValues: columnNameToValue)
- if let handler {
- handler(success, nil) // Call the completion handler
- }
- }
- }
- @objc public
- func insertMainTable(withValues values: [Any],
- fromSource source: DBSource,
- completionHandler handler: ((Bool, [String: AnyHashable]?) -> Void)? = nil) {
- Task { // Use Task to call the actor method asynchronously
- let success = await self.databaseActor.insertMainTable(withValues: values, fromSource: source)
- if let handler {
- handler(success, nil) // Call the completion handler
- }
- }
- }
- @objc public
- func insertInternalMetadataTable(withValues values: [Any],
- completionHandler handler: ((Bool, [String: AnyHashable]?)
- -> Void)? = nil) {
- Task { // Use Task to call the actor method asynchronously
- let success = await self.databaseActor.insertInternalMetadataTable(withValues: values)
- if let handler {
- handler(success, nil) // Call the completion handler
- }
- }
- }
- @objc public
- func insertExperimentTable(withKey key: String,
- value serializedValue: Data,
- completionHandler handler: ((Bool, [String: AnyHashable]?) -> Void)? =
- nil) {
- Task { // Use Task to call the actor method asynchronously
- let success = key == ConfigConstants.experimentTableKeyMetadata ?
- await self.databaseActor.update(experimentMetadata: serializedValue) :
- await self.databaseActor.insertExperimentTable(withKey: key, value: serializedValue)
- if let handler {
- handler(success, nil) // Call the completion handler
- }
- }
- }
- @objc public
- func insertOrUpdatePersonalizationConfig(_ dataValue: [String: Any],
- fromSource source: DBSource) {
- do {
- let payload = try JSONSerialization.data(withJSONObject: dataValue,
- options: .prettyPrinted)
- Task {
- await databaseActor.insertOrUpdatePersonalizationConfig(payload, fromSource: source)
- }
- } catch {
- RCLog.error("I-RCN000075",
- "Invalid Personalization payload to be serialized.")
- }
- }
- @objc public
- func insertOrUpdateRolloutTable(withKey key: String,
- value metadataList: [[String: Any]],
- completionHandler handler: ((Bool, [String: AnyHashable]?)
- -> Void)? = nil) {
- Task { // Use Task to call the actor method asynchronously
- let success = await self.databaseActor.insertOrUpdateRolloutTable(
- withKey: key,
- value: metadataList
- )
- if let handler {
- handler(success, nil) // Call the completion handler
- }
- }
- }
- // MARK: - Update
- @objc public
- func updateMetadata(withOption option: UpdateOption,
- namespace: String,
- values: [Any],
- completionHandler handler: ((Bool, [String: AnyHashable]?) -> Void)? = nil) {
- Task { // Use Task to call the actor method asynchronously
- let success = await self.databaseActor.updateMetadataTable(withOption: option,
- namespace: namespace,
- values: values)
- if let handler {
- handler(success, nil) // Call the completion handler
- }
- }
- }
- // MARK: - Load from DB
- @objc public
- func loadMetadata(withBundleIdentifier bundleIdentifier: String,
- namespace: String,
- completionHandler handler: @escaping (([String: Sendable]) -> Void)) {
- Task { // Use Task to call the actor method asynchronously
- let table = await self.databaseActor.loadMetadataTable(withBundleIdentifier: bundleIdentifier,
- namespace: namespace)
- handler(table) // Call the completion handler
- }
- }
- // MARK: - Load
- @objc public
- func loadMain(withBundleIdentifier bundleIdentifier: String,
- completionHandler handler: ((Bool, [String: [String: RemoteConfigValue]],
- [String: [String: RemoteConfigValue]],
- [String: [String: RemoteConfigValue]],
- [String: [[String: Sendable]]]) -> Void)? = nil) {
- Task {
- let fetchedConfig = await self.databaseActor.loadMainTable(
- withBundleIdentifier: bundleIdentifier,
- fromSource: .fetched
- )
- let activeConfig = await self.databaseActor.loadMainTable(
- withBundleIdentifier: bundleIdentifier,
- fromSource: .active
- )
- let defaultConfig = await self.databaseActor.loadMainTable(
- withBundleIdentifier: bundleIdentifier,
- fromSource: .default
- )
- let fetchedRolloutMetadata = await self.databaseActor.loadRolloutTable(
- fromKey: ConfigConstants.rolloutTableKeyFetchedMetadata
- )
- let activeRolloutMetadata = await self.databaseActor.loadRolloutTable(
- fromKey: ConfigConstants.rolloutTableKeyActiveMetadata
- )
- if let handler {
- handler(true, fetchedConfig, activeConfig, defaultConfig, [
- ConfigConstants.rolloutTableKeyFetchedMetadata: fetchedRolloutMetadata,
- ConfigConstants.rolloutTableKeyActiveMetadata: activeRolloutMetadata,
- ])
- }
- }
- }
- @objc public
- func loadExperiment(completionHandler handler: ((Bool, [String: Sendable]?) -> Void)? = nil) {
- Task {
- let experimentPayloads = await self.databaseActor.loadExperimentTable(
- fromKey: ConfigConstants.experimentTableKeyPayload
- ) ?? []
- let metadata = await self.databaseActor.loadExperimentTable(
- fromKey: ConfigConstants.experimentTableKeyMetadata
- ) ?? []
- let experimentMetadata =
- if let experiments = metadata.first,
- // There should be only one entry for experiment metadata.
- let object =
- try? JSONSerialization.jsonObject(with: experiments,
- options: .mutableContainers) as? [String: Sendable] {
- object
- } else {
- [String: String]()
- }
- let activeExperimentPayloads = (await self.databaseActor.loadExperimentTable(
- fromKey: ConfigConstants.experimentTableKeyActivePayload
- ) ?? [])
- if let handler {
- handler(true, [
- ConfigConstants.experimentTableKeyPayload: experimentPayloads,
- ConfigConstants.experimentTableKeyMetadata: experimentMetadata,
- ConfigConstants.experimentTableKeyActivePayload: activeExperimentPayloads,
- ])
- }
- }
- }
- @objc public
- func loadPersonalization(completionHandler handler: ((Bool, [String: AnyHashable],
- [String: AnyHashable]) -> Void)? = nil) {
- Task {
- let activePersonalizationData =
- await self.databaseActor.loadPersonalizationTable(fromKey: DBSource.active.rawValue)
- let activePersonalization =
- if let activePersonalizationData,
- let object =
- try? JSONSerialization
- .jsonObject(with: activePersonalizationData, options: []) as? [String: String] {
- object
- } else {
- [String: String]()
- }
- let fetchedPersonalizationData =
- await self.databaseActor.loadPersonalizationTable(fromKey: DBSource.fetched.rawValue)
- let fetchedPersonalization =
- if let fetchedPersonalizationData,
- let object =
- try? JSONSerialization
- .jsonObject(with: fetchedPersonalizationData, options: []) as? [String: String] {
- object
- } else {
- [String: String]()
- }
- if let handler {
- handler(true, fetchedPersonalization, activePersonalization)
- }
- }
- }
- @objc public
- func loadInternalMetadataTable(completionHandler handler: @escaping (([String: Data]) -> Void)) {
- Task {
- let metadata = await self.databaseActor.loadInternalMetadataTableInternal()
- handler(metadata)
- }
- }
- // MARK: - Delete
- @objc public
- func deleteRecord(fromMainTableWithNamespace namespace: String,
- bundleIdentifier: String,
- fromSource source: DBSource) {
- Task {
- await self.databaseActor
- .deleteRecord(
- fromMainTableWithNamespace: namespace,
- bundleIdentifier: bundleIdentifier,
- fromSource: source
- )
- }
- }
- @objc public
- func deleteRecord(withBundleIdentifier bundleIdentifier: String,
- namespace: String) {
- Task {
- await self.databaseActor.deleteRecord(
- withBundleIdentifier: bundleIdentifier,
- namespace: namespace
- )
- }
- }
- @objc public
- func deleteExperimentTable(forKey key: String) {
- Task {
- await self.databaseActor.deleteExperimentTable(forKey: key)
- }
- }
- // MARK: - for unit tests
- @objc public func removeDatabase(path: String) {
- Task {
- await databaseActor.removeDatabase(atPath: path)
- }
- }
- @objc func createOrOpenDatabase() {
- Task {
- await databaseActor.createOrOpenDatabase()
- }
- }
- }
|