ModelDownloader.swift 27 KB

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