ModelDownloader.swift 27 KB

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