ModelDownloader.swift 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654
  1. // Copyright 2021 Google LLC
  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 FirebaseCore
  15. import FirebaseInstallations
  16. import Foundation
  17. #if SWIFT_PACKAGE
  18. @_implementationOnly import GoogleUtilities_UserDefaults
  19. #else
  20. @_implementationOnly import GoogleUtilities
  21. #endif // SWIFT_PACKAGE
  22. /// Possible ways to get a custom model.
  23. public enum ModelDownloadType {
  24. /// Get local model stored on device if available. If no local model on device, this is the same
  25. /// as `latestModel`.
  26. case localModel
  27. /// Get local model on device if available and update to latest model from server in the
  28. /// background. If no local model on device, this is the same as `latestModel`.
  29. case localModelUpdateInBackground
  30. /// Get latest model from server. Does not make a network call for model file download if local
  31. /// model matches the latest version on server.
  32. case latestModel
  33. }
  34. /// Downloader to manage custom model downloads.
  35. public class ModelDownloader {
  36. /// Name of the app associated with this instance of ModelDownloader.
  37. private let appName: String
  38. /// Current Firebase app options.
  39. private let options: FirebaseOptions
  40. /// Installations instance for current Firebase app.
  41. private let installations: Installations
  42. /// User defaults for model info.
  43. private let userDefaults: GULUserDefaults
  44. /// Telemetry logger tied to this instance of model downloader.
  45. let telemetryLogger: TelemetryLogger?
  46. /// Number of retries in case of model download URL expiry.
  47. var numberOfRetries: Int = 1
  48. /// Shared dictionary mapping app name to a specific instance of model downloader.
  49. // TODO: Switch to using Firebase components.
  50. private static var modelDownloaderDictionary: [String: ModelDownloader] = [:]
  51. /// Download task associated with the model currently being downloaded.
  52. private var currentDownloadTask: [String: ModelDownloadTask] = [:]
  53. /// DispatchQueue to manage download task dictionary.
  54. let taskSerialQueue = DispatchQueue(label: "downloadtask.serial.queue")
  55. /// Re-dispatch a function on the main queue.
  56. func asyncOnMainQueue(_ work: @autoclosure @escaping () -> Void) {
  57. DispatchQueue.main.async {
  58. work()
  59. }
  60. }
  61. /// Private init for model downloader.
  62. private init(app: FirebaseApp, defaults: GULUserDefaults = .firebaseMLDefaults) {
  63. appName = app.name
  64. options = app.options
  65. installations = Installations.installations(app: app)
  66. userDefaults = defaults
  67. // Respect Firebase-wide data collection setting.
  68. telemetryLogger = TelemetryLogger(app: app)
  69. // Notification of app deletion.
  70. let notificationName = "FIRAppDeleteNotification"
  71. NotificationCenter.default.addObserver(
  72. self,
  73. selector: #selector(deleteModelDownloader),
  74. name: Notification.Name(notificationName),
  75. object: nil
  76. )
  77. }
  78. /// Handles app deletion notification.
  79. @objc private func deleteModelDownloader(notification: Notification) {
  80. let userInfoKey = "FIRAppNameKey"
  81. if let userInfo = notification.userInfo,
  82. let appName = userInfo[userInfoKey] as? String {
  83. ModelDownloader.modelDownloaderDictionary.removeValue(forKey: appName)
  84. // TODO: Clean up user defaults.
  85. // TODO: Clean up local instances of app.
  86. DeviceLogger.logEvent(level: .debug,
  87. message: ModelDownloader.DebugDescription.deleteModelDownloader,
  88. messageCode: .downloaderInstanceDeleted)
  89. }
  90. }
  91. /// Model downloader with default app.
  92. public static func modelDownloader() -> ModelDownloader {
  93. guard let defaultApp = FirebaseApp.app() else {
  94. fatalError(ModelDownloader.ErrorDescription.defaultAppNotConfigured)
  95. }
  96. return modelDownloader(app: defaultApp)
  97. }
  98. /// Model Downloader with custom app.
  99. public static func modelDownloader(app: FirebaseApp) -> ModelDownloader {
  100. if let downloader = modelDownloaderDictionary[app.name] {
  101. DeviceLogger.logEvent(level: .debug,
  102. message: ModelDownloader.DebugDescription.retrieveModelDownloader,
  103. messageCode: .downloaderInstanceRetrieved)
  104. return downloader
  105. } else {
  106. let downloader = ModelDownloader(app: app)
  107. modelDownloaderDictionary[app.name] = downloader
  108. DeviceLogger.logEvent(level: .debug,
  109. message: ModelDownloader.DebugDescription.createModelDownloader,
  110. messageCode: .downloaderInstanceCreated)
  111. return downloader
  112. }
  113. }
  114. /// Downloads a custom model to device or gets a custom model already on device, with an optional
  115. /// handler for progress.
  116. /// - Parameters:
  117. /// - modelName: The name of the model, matching Firebase console.
  118. /// - downloadType: ModelDownloadType used to get the model.
  119. /// - conditions: Conditions needed to perform a model download.
  120. /// - progressHandler: Optional. Returns a float in [0.0, 1.0] that can be used to monitor model
  121. /// download progress.
  122. /// - completion: Returns either a `CustomModel` on success, or a `DownloadError` on failure, at
  123. /// the end of a model download.
  124. public func getModel(name modelName: String,
  125. downloadType: ModelDownloadType,
  126. conditions: ModelDownloadConditions,
  127. progressHandler: ((Float) -> Void)? = nil,
  128. completion: @escaping (Result<CustomModel, DownloadError>) -> Void) {
  129. guard !modelName.isEmpty else {
  130. asyncOnMainQueue(completion(.failure(.emptyModelName)))
  131. return
  132. }
  133. switch downloadType {
  134. case .localModel:
  135. if let localModel = getLocalModel(modelName: modelName) {
  136. DeviceLogger.logEvent(level: .debug,
  137. message: ModelDownloader.DebugDescription.localModelFound,
  138. messageCode: .localModelFound)
  139. asyncOnMainQueue(completion(.success(localModel)))
  140. } else {
  141. getRemoteModel(
  142. modelName: modelName,
  143. conditions: conditions,
  144. progressHandler: progressHandler,
  145. completion: completion
  146. )
  147. }
  148. case .localModelUpdateInBackground:
  149. if let localModel = getLocalModel(modelName: modelName) {
  150. DeviceLogger.logEvent(level: .debug,
  151. message: ModelDownloader.DebugDescription.localModelFound,
  152. messageCode: .localModelFound)
  153. asyncOnMainQueue(completion(.success(localModel)))
  154. telemetryLogger?.logModelDownloadEvent(
  155. eventName: .modelDownload,
  156. status: .scheduled,
  157. model: CustomModel(name: modelName, size: 0, path: "", hash: ""),
  158. downloadErrorCode: .noError
  159. )
  160. // Update local model in the background.
  161. DispatchQueue.global(qos: .utility).async { [weak self] in
  162. self?.getRemoteModel(
  163. modelName: modelName,
  164. conditions: conditions,
  165. progressHandler: nil,
  166. completion: { result in
  167. switch result {
  168. case let .success(model):
  169. DeviceLogger.logEvent(level: .debug,
  170. message: ModelDownloader.DebugDescription
  171. .backgroundModelDownloaded,
  172. messageCode: .backgroundModelDownloaded)
  173. self?.telemetryLogger?.logModelDownloadEvent(
  174. eventName: .modelDownload,
  175. status: .succeeded,
  176. model: model,
  177. downloadErrorCode: .noError
  178. )
  179. case .failure:
  180. DeviceLogger.logEvent(level: .debug,
  181. message: ModelDownloader.ErrorDescription
  182. .backgroundModelDownload,
  183. messageCode: .backgroundDownloadError)
  184. self?.telemetryLogger?.logModelDownloadEvent(
  185. eventName: .modelDownload,
  186. status: .failed,
  187. model: CustomModel(name: modelName, size: 0, path: "", hash: ""),
  188. downloadErrorCode: .downloadFailed
  189. )
  190. }
  191. }
  192. )
  193. }
  194. } else {
  195. getRemoteModel(
  196. modelName: modelName,
  197. conditions: conditions,
  198. progressHandler: progressHandler,
  199. completion: completion
  200. )
  201. }
  202. case .latestModel:
  203. getRemoteModel(
  204. modelName: modelName,
  205. conditions: conditions,
  206. progressHandler: progressHandler,
  207. completion: completion
  208. )
  209. }
  210. }
  211. /// Gets the set of all downloaded models saved on device.
  212. /// - Parameter completion: Returns either a set of `CustomModel` models on success, or a
  213. /// `DownloadedModelError` on failure.
  214. public func listDownloadedModels(completion: @escaping (Result<Set<CustomModel>,
  215. DownloadedModelError>) -> Void) {
  216. do {
  217. let modelURLs = try ModelFileManager.contentsOfModelsDirectory()
  218. var customModels = Set<CustomModel>()
  219. // Retrieve model name from URL.
  220. for url in modelURLs {
  221. guard let modelName = ModelFileManager.getModelNameFromFilePath(url) else {
  222. let description = ModelDownloader.ErrorDescription.parseModelName(url.path)
  223. DeviceLogger.logEvent(level: .debug,
  224. message: description,
  225. messageCode: .modelNameParseError)
  226. asyncOnMainQueue(completion(.failure(.internalError(description: description))))
  227. return
  228. }
  229. // Check if model information corresponding to model is stored in UserDefaults.
  230. guard let modelInfo = getLocalModelInfo(modelName: modelName) else {
  231. let description = ModelDownloader.ErrorDescription.noLocalModelInfo(modelName)
  232. DeviceLogger.logEvent(level: .debug,
  233. message: description,
  234. messageCode: .noLocalModelInfo)
  235. asyncOnMainQueue(completion(.failure(.internalError(description: description))))
  236. return
  237. }
  238. // Ensure that local model path is as expected, and reachable.
  239. guard let modelURL = ModelFileManager.getDownloadedModelFileURL(
  240. appName: appName,
  241. modelName: modelName
  242. ),
  243. ModelFileManager.isFileReachable(at: modelURL) else {
  244. DeviceLogger.logEvent(level: .debug,
  245. message: ModelDownloader.ErrorDescription.outdatedModelPath,
  246. messageCode: .outdatedModelPathError)
  247. asyncOnMainQueue(completion(.failure(.internalError(description: ModelDownloader
  248. .ErrorDescription.outdatedModelPath))))
  249. return
  250. }
  251. let model = CustomModel(localModelInfo: modelInfo, path: modelURL.path)
  252. // Add model to result set.
  253. customModels.insert(model)
  254. }
  255. DeviceLogger.logEvent(level: .debug,
  256. message: ModelDownloader.DebugDescription.allLocalModelsFound,
  257. messageCode: .allLocalModelsFound)
  258. completion(.success(customModels))
  259. } catch let error as DownloadedModelError {
  260. DeviceLogger.logEvent(level: .debug,
  261. message: ModelDownloader.ErrorDescription.listModelsFailed(error),
  262. messageCode: .listModelsError)
  263. asyncOnMainQueue(completion(.failure(error)))
  264. } catch {
  265. DeviceLogger.logEvent(level: .debug,
  266. message: ModelDownloader.ErrorDescription.listModelsFailed(error),
  267. messageCode: .listModelsError)
  268. asyncOnMainQueue(completion(.failure(.internalError(description: error
  269. .localizedDescription))))
  270. }
  271. }
  272. /// Deletes a custom model file from device as well as corresponding model information saved in
  273. /// UserDefaults.
  274. /// - Parameters:
  275. /// - modelName: The name of the model, matching Firebase console and already downloaded to
  276. /// device.
  277. /// - completion: Returns a `DownloadedModelError` on failure.
  278. public func deleteDownloadedModel(name modelName: String,
  279. completion: @escaping (Result<Void, DownloadedModelError>)
  280. -> Void) {
  281. // Ensure that there is a matching model file on device, with corresponding model information in
  282. // UserDefaults.
  283. guard let modelURL = ModelFileManager.getDownloadedModelFileURL(
  284. appName: appName,
  285. modelName: modelName
  286. ),
  287. let localModelInfo = getLocalModelInfo(modelName: modelName),
  288. ModelFileManager.isFileReachable(at: modelURL)
  289. else {
  290. DeviceLogger.logEvent(level: .debug,
  291. message: ModelDownloader.ErrorDescription.modelNotFound(modelName),
  292. messageCode: .modelNotFound)
  293. asyncOnMainQueue(completion(.failure(.notFound)))
  294. return
  295. }
  296. do {
  297. // Remove model file from device.
  298. try ModelFileManager.removeFile(at: modelURL)
  299. // Clear out corresponding local model info.
  300. localModelInfo.removeFromDefaults(userDefaults, appName: appName)
  301. DeviceLogger.logEvent(level: .debug,
  302. message: ModelDownloader.DebugDescription.modelDeleted,
  303. messageCode: .modelDeleted)
  304. telemetryLogger?.logModelDeletedEvent(
  305. eventName: .remoteModelDeleteOnDevice,
  306. isSuccessful: true
  307. )
  308. asyncOnMainQueue(completion(.success(())))
  309. } catch let error as DownloadedModelError {
  310. DeviceLogger.logEvent(level: .debug,
  311. message: ModelDownloader.ErrorDescription.modelDeletionFailed(error),
  312. messageCode: .modelDeletionFailed)
  313. telemetryLogger?.logModelDeletedEvent(
  314. eventName: .remoteModelDeleteOnDevice,
  315. isSuccessful: false
  316. )
  317. asyncOnMainQueue(completion(.failure(error)))
  318. } catch {
  319. DeviceLogger.logEvent(level: .debug,
  320. message: ModelDownloader.ErrorDescription.modelDeletionFailed(error),
  321. messageCode: .modelDeletionFailed)
  322. telemetryLogger?.logModelDeletedEvent(
  323. eventName: .remoteModelDeleteOnDevice,
  324. isSuccessful: false
  325. )
  326. asyncOnMainQueue(completion(.failure(.internalError(description: error
  327. .localizedDescription))))
  328. }
  329. }
  330. }
  331. extension ModelDownloader {
  332. /// Get model information for model saved on device, if available.
  333. private func getLocalModelInfo(modelName: String) -> LocalModelInfo? {
  334. guard let localModelInfo = LocalModelInfo(
  335. fromDefaults: userDefaults,
  336. name: modelName,
  337. appName: appName
  338. ) else {
  339. let description = ModelDownloader.DebugDescription.noLocalModelInfo(modelName)
  340. DeviceLogger.logEvent(level: .debug,
  341. message: description,
  342. messageCode: .noLocalModelInfo)
  343. return nil
  344. }
  345. /// Local model info is only considered valid if there is a corresponding model file on device.
  346. guard let modelURL = ModelFileManager.getDownloadedModelFileURL(
  347. appName: appName,
  348. modelName: modelName
  349. ), ModelFileManager.isFileReachable(at: modelURL) else {
  350. let description = ModelDownloader.DebugDescription.noLocalModelFile(modelName)
  351. DeviceLogger.logEvent(level: .debug,
  352. message: description,
  353. messageCode: .noLocalModelFile)
  354. return nil
  355. }
  356. return localModelInfo
  357. }
  358. /// Get model saved on device, if available.
  359. private func getLocalModel(modelName: String) -> CustomModel? {
  360. guard let modelURL = ModelFileManager.getDownloadedModelFileURL(
  361. appName: appName,
  362. modelName: modelName
  363. ), let localModelInfo = getLocalModelInfo(modelName: modelName) else { return nil }
  364. let model = CustomModel(localModelInfo: localModelInfo, path: modelURL.path)
  365. return model
  366. }
  367. /// Download and get model from server, unless the latest model is already available on device.
  368. private func getRemoteModel(modelName: String,
  369. conditions: ModelDownloadConditions,
  370. progressHandler: ((Float) -> Void)? = nil,
  371. completion: @escaping (Result<CustomModel, DownloadError>) -> Void) {
  372. let localModelInfo = getLocalModelInfo(modelName: modelName)
  373. guard let projectID = options.projectID, let apiKey = options.apiKey else {
  374. DeviceLogger.logEvent(level: .debug,
  375. message: ModelDownloader.ErrorDescription.invalidOptions,
  376. messageCode: .invalidOptions)
  377. completion(.failure(.internalError(description: ModelDownloader.ErrorDescription
  378. .invalidOptions)))
  379. return
  380. }
  381. let modelInfoRetriever = ModelInfoRetriever(
  382. modelName: modelName,
  383. projectID: projectID,
  384. apiKey: apiKey,
  385. appName: appName, installations: installations,
  386. localModelInfo: localModelInfo,
  387. telemetryLogger: telemetryLogger
  388. )
  389. let downloader = ModelFileDownloader(conditions: conditions)
  390. downloadInfoAndModel(
  391. modelName: modelName,
  392. modelInfoRetriever: modelInfoRetriever,
  393. downloader: downloader,
  394. conditions: conditions,
  395. progressHandler: progressHandler,
  396. completion: completion
  397. )
  398. }
  399. /// Get model info and model file from server.
  400. func downloadInfoAndModel(modelName: String,
  401. modelInfoRetriever: ModelInfoRetriever,
  402. downloader: FileDownloader,
  403. conditions: ModelDownloadConditions,
  404. progressHandler: ((Float) -> Void)? = nil,
  405. completion: @escaping (Result<CustomModel, DownloadError>)
  406. -> Void) {
  407. modelInfoRetriever.downloadModelInfo { result in
  408. switch result {
  409. case let .success(downloadModelInfoResult):
  410. switch downloadModelInfoResult {
  411. // New model info was downloaded from server.
  412. case let .modelInfo(remoteModelInfo):
  413. // Progress handler for model file download.
  414. let taskProgressHandler: ModelDownloadTask.ProgressHandler = { progress in
  415. if let progressHandler {
  416. self.asyncOnMainQueue(progressHandler(progress))
  417. }
  418. }
  419. // Completion handler for model file download.
  420. let taskCompletion: ModelDownloadTask.Completion = { result in
  421. switch result {
  422. case let .success(model):
  423. self.asyncOnMainQueue(completion(.success(model)))
  424. case let .failure(error):
  425. switch error {
  426. case .notFound:
  427. self.asyncOnMainQueue(completion(.failure(.notFound)))
  428. case .invalidArgument:
  429. self.asyncOnMainQueue(completion(.failure(.invalidArgument)))
  430. case .permissionDenied:
  431. self.asyncOnMainQueue(completion(.failure(.permissionDenied)))
  432. // This is the error returned when model download URL has expired.
  433. case .expiredDownloadURL:
  434. // Retry model info and model file download, if allowed.
  435. guard self.numberOfRetries > 0 else {
  436. self
  437. .asyncOnMainQueue(
  438. completion(.failure(.internalError(description: ModelDownloader
  439. .ErrorDescription
  440. .expiredModelInfo)))
  441. )
  442. return
  443. }
  444. self.numberOfRetries -= 1
  445. DeviceLogger.logEvent(level: .debug,
  446. message: ModelDownloader.DebugDescription.retryDownload,
  447. messageCode: .retryDownload)
  448. self.downloadInfoAndModel(
  449. modelName: modelName,
  450. modelInfoRetriever: modelInfoRetriever,
  451. downloader: downloader,
  452. conditions: conditions,
  453. progressHandler: progressHandler,
  454. completion: completion
  455. )
  456. default:
  457. self.asyncOnMainQueue(completion(.failure(error)))
  458. }
  459. }
  460. self.taskSerialQueue.async {
  461. // Stop keeping track of current download task.
  462. self.currentDownloadTask.removeValue(forKey: modelName)
  463. }
  464. }
  465. self.taskSerialQueue.sync {
  466. // Merge duplicate requests if there is already a download in progress for the same
  467. // model.
  468. if let downloadTask = self.currentDownloadTask[modelName],
  469. downloadTask.canMergeRequests() {
  470. downloadTask.merge(
  471. newProgressHandler: taskProgressHandler,
  472. newCompletion: taskCompletion
  473. )
  474. DeviceLogger.logEvent(level: .debug,
  475. message: ModelDownloader.DebugDescription.mergingRequests,
  476. messageCode: .mergeRequests)
  477. if downloadTask.canResume() {
  478. downloadTask.resume()
  479. }
  480. // TODO: Handle else.
  481. } else {
  482. // Create download task for model file download.
  483. let downloadTask = ModelDownloadTask(
  484. remoteModelInfo: remoteModelInfo,
  485. appName: self.appName,
  486. defaults: self.userDefaults,
  487. downloader: downloader,
  488. progressHandler: taskProgressHandler,
  489. completion: taskCompletion,
  490. telemetryLogger: self.telemetryLogger
  491. )
  492. // Keep track of current download task to allow for merging duplicate requests.
  493. self.currentDownloadTask[modelName] = downloadTask
  494. downloadTask.resume()
  495. }
  496. }
  497. /// Local model info is the latest model info.
  498. case .notModified:
  499. guard let localModel = self.getLocalModel(modelName: modelName) else {
  500. // This can only happen if either local model info or the model file was wiped out after
  501. // model info request but before server response.
  502. self
  503. .asyncOnMainQueue(completion(.failure(.internalError(description: ModelDownloader
  504. .ErrorDescription.deletedLocalModelInfoOrFile))))
  505. return
  506. }
  507. self.asyncOnMainQueue(completion(.success(localModel)))
  508. }
  509. // Error retrieving model info.
  510. case let .failure(error):
  511. self.asyncOnMainQueue(completion(.failure(error)))
  512. }
  513. }
  514. }
  515. }
  516. /// Possible errors with model downloading.
  517. public enum DownloadError: Error, Equatable {
  518. /// No model with this name exists on server.
  519. case notFound
  520. /// Invalid, incomplete, or missing permissions for model download.
  521. case permissionDenied
  522. /// Conditions not met to perform download.
  523. case failedPrecondition
  524. /// Requests quota exhausted.
  525. case resourceExhausted
  526. /// Not enough space for model on device.
  527. case notEnoughSpace
  528. /// Malformed model name or Firebase app options.
  529. case invalidArgument
  530. /// Model name is empty.
  531. case emptyModelName
  532. /// Other errors with description.
  533. case internalError(description: String)
  534. }
  535. /// Possible errors with locating a model file on device.
  536. public enum DownloadedModelError: Error {
  537. /// No model with this name exists on device.
  538. case notFound
  539. /// File system error.
  540. case fileIOError(description: String)
  541. /// Other errors with description.
  542. case internalError(description: String)
  543. }
  544. /// Extension to handle internally meaningful errors.
  545. extension DownloadError {
  546. /// Model download URL expired before model download.
  547. // Model info retrieval and download is retried `numberOfRetries` times before failing.
  548. static let expiredDownloadURL: DownloadError =
  549. .internalError(description: "Expired model download URL.")
  550. }
  551. /// Possible debug and error messages while using model downloader.
  552. extension ModelDownloader {
  553. /// Debug descriptions.
  554. private enum DebugDescription {
  555. static let createModelDownloader =
  556. "Initialized with new downloader instance associated with this app."
  557. static let retrieveModelDownloader =
  558. "Initialized with existing downloader instance associated with this app."
  559. static let deleteModelDownloader = "Model downloader instance deleted due to app deletion."
  560. static let localModelFound = "Found local model on device."
  561. static let allLocalModelsFound = "Found and listed all local models."
  562. static let noLocalModelInfo = { (name: String) in
  563. "No local model info for model named: \(name)."
  564. }
  565. static let noLocalModelFile = { (name: String) in
  566. "No local model file for model named: \(name)."
  567. }
  568. static let backgroundModelDownloaded = "Downloaded latest model in the background."
  569. static let modelDeleted = "Model deleted successfully."
  570. static let mergingRequests = "Merging duplicate download requests."
  571. static let retryDownload = "Retrying download."
  572. }
  573. /// Error descriptions.
  574. private enum ErrorDescription {
  575. static let defaultAppNotConfigured = "Default Firebase app not configured."
  576. static let invalidOptions = "Unable to retrieve project ID and/or API key for Firebase app."
  577. static let modelDownloadFailed = { (error: Error) in
  578. "Model download failed with error: \(error)"
  579. }
  580. static let modelNotFound = { (name: String) in
  581. "Model deletion failed due to no model found with name: \(name)"
  582. }
  583. static let modelInfoRetrievalFailed = { (error: Error) in
  584. "Model info retrieval failed with error: \(error)"
  585. }
  586. static let backgroundModelDownload = "Failed to update model in background."
  587. static let expiredModelInfo = "Unable to update expired model info."
  588. static let listModelsFailed = { (error: Error) in
  589. "Unable to list models, failed with error: \(error)"
  590. }
  591. static let parseModelName = { (path: String) in
  592. "List models failed due to unexpected model file name at \(path)."
  593. }
  594. static let noLocalModelInfo = { (name: String) in
  595. "List models failed due to no local model info for model file named: \(name)."
  596. }
  597. static let deletedLocalModelInfoOrFile =
  598. "Model unavailable due to deleted local model info or model file."
  599. static let outdatedModelPath =
  600. "List models failed due to outdated model paths in local storage."
  601. static let modelDeletionFailed = { (error: Error) in
  602. "Model deletion failed with error: \(error)"
  603. }
  604. }
  605. }
  606. /// Model downloader extension for testing.
  607. extension ModelDownloader {
  608. /// Model downloader instance for testing.
  609. static func modelDownloaderWithDefaults(_ defaults: GULUserDefaults,
  610. app: FirebaseApp) -> ModelDownloader {
  611. let downloader = ModelDownloader(app: app, defaults: defaults)
  612. return downloader
  613. }
  614. }