Files
firebase-ios-sdk/FirebaseMLModelDownloader/Sources/ModelDownloader.swift
T

654 lines
27 KiB
Swift
Raw Normal View History

// Copyright 2021 Google LLC
+19
2020-11-17 11:38:31 -08:00
//
// 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 FirebaseInstallations
2023-11-21 12:12:05 -08:00
import Foundation
#if SWIFT_PACKAGE
@_implementationOnly import GoogleUtilities_UserDefaults
#else
@_implementationOnly import GoogleUtilities
#endif // SWIFT_PACKAGE
+19
2020-11-17 11:38:31 -08:00
/// Possible ways to get a custom model.
public enum ModelDownloadType {
2023-07-11 15:54:51 -07:00
/// Get local model stored on device if available. If no local model on device, this is the same
/// as `latestModel`.
+19
2020-11-17 11:38:31 -08:00
case localModel
2023-07-11 15:54:51 -07:00
/// 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`.
+19
2020-11-17 11:38:31 -08:00
case localModelUpdateInBackground
2023-07-11 15:54:51 -07:00
/// Get latest model from server. Does not make a network call for model file download if local
/// model matches the latest version on server.
+19
2020-11-17 11:38:31 -08:00
case latestModel
}
/// Downloader to manage custom model downloads.
public class ModelDownloader {
/// Name of the app associated with this instance of ModelDownloader.
private let appName: String
2021-02-10 09:47:07 -08:00
/// Current Firebase app options.
private let options: FirebaseOptions
2021-02-10 09:47:07 -08:00
/// Installations instance for current Firebase app.
private let installations: Installations
2021-02-10 09:47:07 -08:00
/// User defaults for model info.
private let userDefaults: GULUserDefaults
2021-02-10 09:47:07 -08:00
2021-01-14 15:29:04 -08:00
/// Telemetry logger tied to this instance of model downloader.
let telemetryLogger: TelemetryLogger?
2021-02-10 09:47:07 -08:00
/// Number of retries in case of model download URL expiry.
var numberOfRetries: Int = 1
2021-02-10 09:47:07 -08:00
/// Shared dictionary mapping app name to a specific instance of model downloader.
// TODO: Switch to using Firebase components.
private static var modelDownloaderDictionary: [String: ModelDownloader] = [:]
/// Download task associated with the model currently being downloaded.
private var currentDownloadTask: [String: ModelDownloadTask] = [:]
/// DispatchQueue to manage download task dictionary.
let taskSerialQueue = DispatchQueue(label: "downloadtask.serial.queue")
2021-02-11 19:42:11 -05:00
/// Re-dispatch a function on the main queue.
func asyncOnMainQueue(_ work: @autoclosure @escaping () -> Void) {
DispatchQueue.main.async {
2021-02-11 19:42:11 -05:00
work()
}
}
2021-02-10 09:47:07 -08:00
/// Private init for model downloader.
private init(app: FirebaseApp, defaults: GULUserDefaults = .firebaseMLDefaults) {
appName = app.name
options = app.options
installations = Installations.installations(app: app)
userDefaults = defaults
2021-02-10 09:47:07 -08:00
// Respect Firebase-wide data collection setting.
telemetryLogger = TelemetryLogger(app: app)
// Notification of app deletion.
let notificationName = "FIRAppDeleteNotification"
NotificationCenter.default.addObserver(
self,
selector: #selector(deleteModelDownloader),
name: Notification.Name(notificationName),
object: nil
)
}
/// Handles app deletion notification.
@objc private func deleteModelDownloader(notification: Notification) {
let userInfoKey = "FIRAppNameKey"
if let userInfo = notification.userInfo,
2022-01-21 13:03:15 -08:00
let appName = userInfo[userInfoKey] as? String {
ModelDownloader.modelDownloaderDictionary.removeValue(forKey: appName)
2021-02-10 09:47:07 -08:00
// TODO: Clean up user defaults.
// TODO: Clean up local instances of app.
2021-01-27 13:22:05 -08:00
DeviceLogger.logEvent(level: .debug,
message: ModelDownloader.DebugDescription.deleteModelDownloader,
messageCode: .downloaderInstanceDeleted)
}
}
/// Model downloader with default app.
public static func modelDownloader() -> ModelDownloader {
guard let defaultApp = FirebaseApp.app() else {
fatalError(ModelDownloader.ErrorDescription.defaultAppNotConfigured)
}
return modelDownloader(app: defaultApp)
}
/// Model Downloader with custom app.
public static func modelDownloader(app: FirebaseApp) -> ModelDownloader {
if let downloader = modelDownloaderDictionary[app.name] {
2021-01-27 13:22:05 -08:00
DeviceLogger.logEvent(level: .debug,
message: ModelDownloader.DebugDescription.retrieveModelDownloader,
messageCode: .downloaderInstanceRetrieved)
return downloader
} else {
let downloader = ModelDownloader(app: app)
modelDownloaderDictionary[app.name] = downloader
2021-01-27 13:22:05 -08:00
DeviceLogger.logEvent(level: .debug,
message: ModelDownloader.DebugDescription.createModelDownloader,
messageCode: .downloaderInstanceCreated)
return downloader
}
}
2023-07-11 15:54:51 -07:00
/// Downloads a custom model to device or gets a custom model already on device, with an optional
/// handler for progress.
2021-02-10 09:47:07 -08:00
/// - Parameters:
/// - modelName: The name of the model, matching Firebase console.
/// - downloadType: ModelDownloadType used to get the model.
/// - conditions: Conditions needed to perform a model download.
2023-07-11 15:54:51 -07:00
/// - progressHandler: Optional. Returns a float in [0.0, 1.0] that can be used to monitor model
/// download progress.
/// - completion: Returns either a `CustomModel` on success, or a `DownloadError` on failure, at
/// the end of a model download.
public func getModel(name modelName: String,
downloadType: ModelDownloadType,
+19
2020-11-17 11:38:31 -08:00
conditions: ModelDownloadConditions,
progressHandler: ((Float) -> Void)? = nil,
completion: @escaping (Result<CustomModel, DownloadError>) -> Void) {
2021-02-11 19:42:11 -05:00
guard !modelName.isEmpty else {
asyncOnMainQueue(completion(.failure(.emptyModelName)))
return
}
switch downloadType {
case .localModel:
if let localModel = getLocalModel(modelName: modelName) {
2021-01-27 13:22:05 -08:00
DeviceLogger.logEvent(level: .debug,
message: ModelDownloader.DebugDescription.localModelFound,
messageCode: .localModelFound)
2021-02-11 19:42:11 -05:00
asyncOnMainQueue(completion(.success(localModel)))
} else {
getRemoteModel(
modelName: modelName,
conditions: conditions,
progressHandler: progressHandler,
completion: completion
)
}
2021-02-10 09:47:07 -08:00
case .localModelUpdateInBackground:
if let localModel = getLocalModel(modelName: modelName) {
2021-01-27 13:22:05 -08:00
DeviceLogger.logEvent(level: .debug,
message: ModelDownloader.DebugDescription.localModelFound,
messageCode: .localModelFound)
2021-02-11 19:42:11 -05:00
asyncOnMainQueue(completion(.success(localModel)))
2021-01-29 12:57:38 -08:00
telemetryLogger?.logModelDownloadEvent(
eventName: .modelDownload,
status: .scheduled,
model: CustomModel(name: modelName, size: 0, path: "", hash: ""),
2021-01-29 12:57:38 -08:00
downloadErrorCode: .noError
)
2021-02-10 09:47:07 -08:00
// Update local model in the background.
DispatchQueue.global(qos: .utility).async { [weak self] in
self?.getRemoteModel(
modelName: modelName,
conditions: conditions,
progressHandler: nil,
completion: { result in
switch result {
2021-01-29 12:57:38 -08:00
case let .success(model):
2021-01-27 13:22:05 -08:00
DeviceLogger.logEvent(level: .debug,
message: ModelDownloader.DebugDescription
.backgroundModelDownloaded,
messageCode: .backgroundModelDownloaded)
2021-01-29 12:57:38 -08:00
self?.telemetryLogger?.logModelDownloadEvent(
eventName: .modelDownload,
status: .succeeded,
model: model,
downloadErrorCode: .noError
)
case .failure:
2021-01-25 17:15:15 -08:00
DeviceLogger.logEvent(level: .debug,
message: ModelDownloader.ErrorDescription
.backgroundModelDownload,
messageCode: .backgroundDownloadError)
2021-01-29 12:57:38 -08:00
self?.telemetryLogger?.logModelDownloadEvent(
eventName: .modelDownload,
status: .failed,
model: CustomModel(name: modelName, size: 0, path: "", hash: ""),
2021-01-29 12:57:38 -08:00
downloadErrorCode: .downloadFailed
)
}
}
)
}
} else {
getRemoteModel(
modelName: modelName,
conditions: conditions,
progressHandler: progressHandler,
completion: completion
)
}
+19
2020-11-17 11:38:31 -08:00
case .latestModel:
getRemoteModel(
modelName: modelName,
conditions: conditions,
progressHandler: progressHandler,
completion: completion
)
}
+19
2020-11-17 11:38:31 -08:00
}
2021-02-10 09:47:07 -08:00
/// Gets the set of all downloaded models saved on device.
2023-07-11 15:54:51 -07:00
/// - Parameter completion: Returns either a set of `CustomModel` models on success, or a
/// `DownloadedModelError` on failure.
+19
2020-11-17 11:38:31 -08:00
public func listDownloadedModels(completion: @escaping (Result<Set<CustomModel>,
DownloadedModelError>) -> Void) {
do {
2021-02-10 09:47:07 -08:00
let modelURLs = try ModelFileManager.contentsOfModelsDirectory()
var customModels = Set<CustomModel>()
2021-02-10 09:47:07 -08:00
// Retrieve model name from URL.
for url in modelURLs {
guard let modelName = ModelFileManager.getModelNameFromFilePath(url) else {
let description = ModelDownloader.ErrorDescription.parseModelName(url.path)
2021-01-27 13:22:05 -08:00
DeviceLogger.logEvent(level: .debug,
message: description,
messageCode: .modelNameParseError)
2021-02-11 19:42:11 -05:00
asyncOnMainQueue(completion(.failure(.internalError(description: description))))
return
}
2021-02-10 09:47:07 -08:00
// Check if model information corresponding to model is stored in UserDefaults.
guard let modelInfo = getLocalModelInfo(modelName: modelName) else {
2021-01-29 12:57:38 -08:00
let description = ModelDownloader.ErrorDescription.noLocalModelInfo(modelName)
2021-01-27 13:22:05 -08:00
DeviceLogger.logEvent(level: .debug,
message: description,
2021-01-29 12:57:38 -08:00
messageCode: .noLocalModelInfo)
2021-02-11 19:42:11 -05:00
asyncOnMainQueue(completion(.failure(.internalError(description: description))))
return
}
2021-02-10 09:47:07 -08:00
// Ensure that local model path is as expected, and reachable.
guard let modelURL = ModelFileManager.getDownloadedModelFileURL(
appName: appName,
modelName: modelName
),
2021-02-11 19:42:11 -05:00
ModelFileManager.isFileReachable(at: modelURL) else {
2021-01-27 13:22:05 -08:00
DeviceLogger.logEvent(level: .debug,
message: ModelDownloader.ErrorDescription.outdatedModelPath,
messageCode: .outdatedModelPathError)
2021-02-11 19:42:11 -05:00
asyncOnMainQueue(completion(.failure(.internalError(description: ModelDownloader
2021-01-27 13:22:05 -08:00
.ErrorDescription.outdatedModelPath))))
return
}
2021-02-10 09:47:07 -08:00
let model = CustomModel(localModelInfo: modelInfo, path: modelURL.path)
// Add model to result set.
customModels.insert(model)
}
2021-01-27 13:22:05 -08:00
DeviceLogger.logEvent(level: .debug,
message: ModelDownloader.DebugDescription.allLocalModelsFound,
messageCode: .allLocalModelsFound)
completion(.success(customModels))
} catch let error as DownloadedModelError {
2021-01-27 13:22:05 -08:00
DeviceLogger.logEvent(level: .debug,
2021-01-29 12:57:38 -08:00
message: ModelDownloader.ErrorDescription.listModelsFailed(error),
2021-01-27 13:22:05 -08:00
messageCode: .listModelsError)
2021-02-11 19:42:11 -05:00
asyncOnMainQueue(completion(.failure(error)))
} catch {
2021-01-27 13:22:05 -08:00
DeviceLogger.logEvent(level: .debug,
2021-01-29 12:57:38 -08:00
message: ModelDownloader.ErrorDescription.listModelsFailed(error),
2021-01-27 13:22:05 -08:00
messageCode: .listModelsError)
2021-02-11 19:42:11 -05:00
asyncOnMainQueue(completion(.failure(.internalError(description: error
2021-01-27 13:22:05 -08:00
.localizedDescription))))
}
+19
2020-11-17 11:38:31 -08:00
}
2023-07-11 15:54:51 -07:00
/// Deletes a custom model file from device as well as corresponding model information saved in
/// UserDefaults.
2021-02-10 09:47:07 -08:00
/// - Parameters:
2023-07-11 15:54:51 -07:00
/// - modelName: The name of the model, matching Firebase console and already downloaded to
/// device.
2021-02-10 09:47:07 -08:00
/// - completion: Returns a `DownloadedModelError` on failure.
+19
2020-11-17 11:38:31 -08:00
public func deleteDownloadedModel(name modelName: String,
completion: @escaping (Result<Void, DownloadedModelError>)
-> Void) {
2023-07-11 15:54:51 -07:00
// Ensure that there is a matching model file on device, with corresponding model information in
// UserDefaults.
2021-02-10 09:47:07 -08:00
guard let modelURL = ModelFileManager.getDownloadedModelFileURL(
appName: appName,
modelName: modelName
),
let localModelInfo = getLocalModelInfo(modelName: modelName),
ModelFileManager.isFileReachable(at: modelURL)
else {
2021-01-27 13:22:05 -08:00
DeviceLogger.logEvent(level: .debug,
message: ModelDownloader.ErrorDescription.modelNotFound(modelName),
messageCode: .modelNotFound)
2021-02-11 19:42:11 -05:00
asyncOnMainQueue(completion(.failure(.notFound)))
return
}
do {
2021-02-10 09:47:07 -08:00
// Remove model file from device.
try ModelFileManager.removeFile(at: modelURL)
// Clear out corresponding local model info.
localModelInfo.removeFromDefaults(userDefaults, appName: appName)
2021-01-27 13:22:05 -08:00
DeviceLogger.logEvent(level: .debug,
message: ModelDownloader.DebugDescription.modelDeleted,
messageCode: .modelDeleted)
2021-02-10 09:47:07 -08:00
telemetryLogger?.logModelDeletedEvent(
eventName: .remoteModelDeleteOnDevice,
isSuccessful: true
)
2021-02-11 19:42:11 -05:00
asyncOnMainQueue(completion(.success(())))
} catch let error as DownloadedModelError {
2021-02-10 09:47:07 -08:00
DeviceLogger.logEvent(level: .debug,
message: ModelDownloader.ErrorDescription.modelDeletionFailed(error),
messageCode: .modelDeletionFailed)
telemetryLogger?.logModelDeletedEvent(
eventName: .remoteModelDeleteOnDevice,
isSuccessful: false
)
2021-02-11 19:42:11 -05:00
asyncOnMainQueue(completion(.failure(error)))
} catch {
2021-02-10 09:47:07 -08:00
DeviceLogger.logEvent(level: .debug,
message: ModelDownloader.ErrorDescription.modelDeletionFailed(error),
messageCode: .modelDeletionFailed)
telemetryLogger?.logModelDeletedEvent(
eventName: .remoteModelDeleteOnDevice,
isSuccessful: false
)
2021-02-11 19:42:11 -05:00
asyncOnMainQueue(completion(.failure(.internalError(description: error
2021-01-27 13:22:05 -08:00
.localizedDescription))))
}
}
}
extension ModelDownloader {
2021-02-10 09:47:07 -08:00
/// Get model information for model saved on device, if available.
private func getLocalModelInfo(modelName: String) -> LocalModelInfo? {
guard let localModelInfo = LocalModelInfo(
fromDefaults: userDefaults,
name: modelName,
appName: appName
) else {
2021-01-29 12:57:38 -08:00
let description = ModelDownloader.DebugDescription.noLocalModelInfo(modelName)
DeviceLogger.logEvent(level: .debug,
message: description,
messageCode: .noLocalModelInfo)
return nil
}
2021-02-10 09:47:07 -08:00
/// Local model info is only considered valid if there is a corresponding model file on device.
guard let modelURL = ModelFileManager.getDownloadedModelFileURL(
appName: appName,
modelName: modelName
), ModelFileManager.isFileReachable(at: modelURL) else {
let description = ModelDownloader.DebugDescription.noLocalModelFile(modelName)
DeviceLogger.logEvent(level: .debug,
message: description,
messageCode: .noLocalModelFile)
return nil
}
return localModelInfo
}
2021-02-10 09:47:07 -08:00
/// Get model saved on device, if available.
private func getLocalModel(modelName: String) -> CustomModel? {
2021-02-10 09:47:07 -08:00
guard let modelURL = ModelFileManager.getDownloadedModelFileURL(
appName: appName,
modelName: modelName
), let localModelInfo = getLocalModelInfo(modelName: modelName) else { return nil }
let model = CustomModel(localModelInfo: localModelInfo, path: modelURL.path)
return model
}
/// Download and get model from server, unless the latest model is already available on device.
private func getRemoteModel(modelName: String,
conditions: ModelDownloadConditions,
progressHandler: ((Float) -> Void)? = nil,
completion: @escaping (Result<CustomModel, DownloadError>) -> Void) {
let localModelInfo = getLocalModelInfo(modelName: modelName)
2021-01-22 18:28:17 -08:00
guard let projectID = options.projectID, let apiKey = options.apiKey else {
2021-01-27 13:22:05 -08:00
DeviceLogger.logEvent(level: .debug,
message: ModelDownloader.ErrorDescription.invalidOptions,
messageCode: .invalidOptions)
2021-01-22 18:28:17 -08:00
completion(.failure(.internalError(description: ModelDownloader.ErrorDescription
.invalidOptions)))
return
}
let modelInfoRetriever = ModelInfoRetriever(
modelName: modelName,
2021-01-22 18:28:17 -08:00
projectID: projectID,
apiKey: apiKey,
2021-02-10 09:47:07 -08:00
appName: appName, installations: installations,
2021-01-29 12:57:38 -08:00
localModelInfo: localModelInfo,
telemetryLogger: telemetryLogger
)
let downloader = ModelFileDownloader(conditions: conditions)
downloadInfoAndModel(
modelName: modelName,
modelInfoRetriever: modelInfoRetriever,
downloader: downloader,
conditions: conditions,
progressHandler: progressHandler,
completion: completion
)
}
2021-02-10 09:47:07 -08:00
/// Get model info and model file from server.
func downloadInfoAndModel(modelName: String,
modelInfoRetriever: ModelInfoRetriever,
downloader: FileDownloader,
conditions: ModelDownloadConditions,
progressHandler: ((Float) -> Void)? = nil,
completion: @escaping (Result<CustomModel, DownloadError>)
-> Void) {
modelInfoRetriever.downloadModelInfo { result in
switch result {
case let .success(downloadModelInfoResult):
switch downloadModelInfoResult {
2021-02-10 09:47:07 -08:00
// New model info was downloaded from server.
case let .modelInfo(remoteModelInfo):
2021-02-10 09:47:07 -08:00
// Progress handler for model file download.
2021-02-03 15:44:40 -08:00
let taskProgressHandler: ModelDownloadTask.ProgressHandler = { progress in
if let progressHandler {
2021-02-11 19:42:11 -05:00
self.asyncOnMainQueue(progressHandler(progress))
}
2021-02-03 15:44:40 -08:00
}
2021-02-10 09:47:07 -08:00
// Completion handler for model file download.
2021-02-03 15:44:40 -08:00
let taskCompletion: ModelDownloadTask.Completion = { result in
switch result {
case let .success(model):
2021-02-11 19:42:11 -05:00
self.asyncOnMainQueue(completion(.success(model)))
case let .failure(error):
switch error {
case .notFound:
2021-02-11 19:42:11 -05:00
self.asyncOnMainQueue(completion(.failure(.notFound)))
case .invalidArgument:
2021-02-11 19:42:11 -05:00
self.asyncOnMainQueue(completion(.failure(.invalidArgument)))
case .permissionDenied:
2021-02-11 19:42:11 -05:00
self.asyncOnMainQueue(completion(.failure(.permissionDenied)))
2021-02-10 09:47:07 -08:00
// This is the error returned when model download URL has expired.
2021-01-27 13:22:05 -08:00
case .expiredDownloadURL:
2021-02-10 09:47:07 -08:00
// Retry model info and model file download, if allowed.
guard self.numberOfRetries > 0 else {
self
2021-02-11 19:42:11 -05:00
.asyncOnMainQueue(
completion(.failure(.internalError(description: ModelDownloader
.ErrorDescription
.expiredModelInfo)))
)
return
}
self.numberOfRetries -= 1
2021-01-27 13:22:05 -08:00
DeviceLogger.logEvent(level: .debug,
message: ModelDownloader.DebugDescription.retryDownload,
messageCode: .retryDownload)
self.downloadInfoAndModel(
modelName: modelName,
modelInfoRetriever: modelInfoRetriever,
downloader: downloader,
conditions: conditions,
progressHandler: progressHandler,
completion: completion
)
default:
2021-02-11 19:42:11 -05:00
self.asyncOnMainQueue(completion(.failure(error)))
}
}
2021-02-03 15:44:40 -08:00
self.taskSerialQueue.async {
2021-02-10 09:47:07 -08:00
// Stop keeping track of current download task.
2021-02-03 15:44:40 -08:00
self.currentDownloadTask.removeValue(forKey: modelName)
}
}
self.taskSerialQueue.sync {
2023-07-11 15:54:51 -07:00
// Merge duplicate requests if there is already a download in progress for the same
// model.
2021-02-03 15:44:40 -08:00
if let downloadTask = self.currentDownloadTask[modelName],
2022-01-21 13:03:15 -08:00
downloadTask.canMergeRequests() {
2021-02-03 15:44:40 -08:00
downloadTask.merge(
newProgressHandler: taskProgressHandler,
newCompletion: taskCompletion
)
DeviceLogger.logEvent(level: .debug,
message: ModelDownloader.DebugDescription.mergingRequests,
messageCode: .mergeRequests)
if downloadTask.canResume() {
downloadTask.resume()
}
2021-02-10 09:47:07 -08:00
// TODO: Handle else.
2021-02-03 15:44:40 -08:00
} else {
2021-02-10 09:47:07 -08:00
// Create download task for model file download.
2021-02-03 15:44:40 -08:00
let downloadTask = ModelDownloadTask(
remoteModelInfo: remoteModelInfo,
appName: self.appName,
defaults: self.userDefaults,
downloader: downloader,
progressHandler: taskProgressHandler,
completion: taskCompletion,
telemetryLogger: self.telemetryLogger
)
2021-02-10 09:47:07 -08:00
// Keep track of current download task to allow for merging duplicate requests.
2021-02-03 15:44:40 -08:00
self.currentDownloadTask[modelName] = downloadTask
downloadTask.resume()
}
}
/// Local model info is the latest model info.
case .notModified:
guard let localModel = self.getLocalModel(modelName: modelName) else {
2023-07-11 15:54:51 -07:00
// This can only happen if either local model info or the model file was wiped out after
// model info request but before server response.
self
2021-02-11 19:42:11 -05:00
.asyncOnMainQueue(completion(.failure(.internalError(description: ModelDownloader
2021-02-10 09:47:07 -08:00
.ErrorDescription.deletedLocalModelInfoOrFile))))
return
}
2021-02-11 19:42:11 -05:00
self.asyncOnMainQueue(completion(.success(localModel)))
}
2021-02-10 09:47:07 -08:00
// Error retrieving model info.
case let .failure(error):
2021-02-11 19:42:11 -05:00
self.asyncOnMainQueue(completion(.failure(error)))
}
}
}
}
2021-02-10 09:47:07 -08:00
/// Possible errors with model downloading.
public enum DownloadError: Error, Equatable {
/// No model with this name exists on server.
case notFound
/// Invalid, incomplete, or missing permissions for model download.
case permissionDenied
/// Conditions not met to perform download.
case failedPrecondition
/// Requests quota exhausted.
case resourceExhausted
/// Not enough space for model on device.
case notEnoughSpace
/// Malformed model name or Firebase app options.
case invalidArgument
2021-02-11 19:42:11 -05:00
/// Model name is empty.
case emptyModelName
2021-02-10 09:47:07 -08:00
/// Other errors with description.
case internalError(description: String)
+19
2020-11-17 11:38:31 -08:00
}
2021-02-10 09:47:07 -08:00
/// Possible errors with locating a model file on device.
public enum DownloadedModelError: Error {
/// No model with this name exists on device.
case notFound
/// File system error.
case fileIOError(description: String)
/// Other errors with description.
case internalError(description: String)
}
/// Extension to handle internally meaningful errors.
extension DownloadError {
/// Model download URL expired before model download.
// Model info retrieval and download is retried `numberOfRetries` times before failing.
2023-07-11 15:54:51 -07:00
static let expiredDownloadURL: DownloadError =
2022-01-21 13:03:15 -08:00
.internalError(description: "Expired model download URL.")
2021-02-10 09:47:07 -08:00
}
/// Possible debug and error messages while using model downloader.
extension ModelDownloader {
2021-01-27 13:22:05 -08:00
/// Debug descriptions.
private enum DebugDescription {
static let createModelDownloader =
"Initialized with new downloader instance associated with this app."
2021-02-10 09:47:07 -08:00
static let retrieveModelDownloader =
"Initialized with existing downloader instance associated with this app."
static let deleteModelDownloader = "Model downloader instance deleted due to app deletion."
2021-01-27 13:22:05 -08:00
static let localModelFound = "Found local model on device."
static let allLocalModelsFound = "Found and listed all local models."
2021-01-29 12:57:38 -08:00
static let noLocalModelInfo = { (name: String) in
2021-02-10 09:47:07 -08:00
"No local model info for model named: \(name)."
2021-01-29 12:57:38 -08:00
}
2021-02-03 15:44:40 -08:00
2021-02-10 09:47:07 -08:00
static let noLocalModelFile = { (name: String) in
"No local model file for model named: \(name)."
}
static let backgroundModelDownloaded = "Downloaded latest model in the background."
static let modelDeleted = "Model deleted successfully."
2021-02-03 15:44:40 -08:00
static let mergingRequests = "Merging duplicate download requests."
2021-02-10 09:47:07 -08:00
static let retryDownload = "Retrying download."
2021-01-27 13:22:05 -08:00
}
/// Error descriptions.
private enum ErrorDescription {
2021-02-10 09:47:07 -08:00
static let defaultAppNotConfigured = "Default Firebase app not configured."
2021-01-22 18:28:17 -08:00
static let invalidOptions = "Unable to retrieve project ID and/or API key for Firebase app."
2021-02-10 09:47:07 -08:00
static let modelDownloadFailed = { (error: Error) in
"Model download failed with error: \(error)"
2021-01-27 13:22:05 -08:00
}
static let modelNotFound = { (name: String) in
2021-01-29 12:57:38 -08:00
"Model deletion failed due to no model found with name: \(name)"
2021-01-27 13:22:05 -08:00
}
2021-01-29 12:57:38 -08:00
static let modelInfoRetrievalFailed = { (error: Error) in
"Model info retrieval failed with error: \(error)"
}
2021-02-10 09:47:07 -08:00
static let backgroundModelDownload = "Failed to update model in background."
static let expiredModelInfo = "Unable to update expired model info."
static let listModelsFailed = { (error: Error) in
"Unable to list models, failed with error: \(error)"
}
static let parseModelName = { (path: String) in
"List models failed due to unexpected model file name at \(path)."
}
static let noLocalModelInfo = { (name: String) in
"List models failed due to no local model info for model file named: \(name)."
}
static let deletedLocalModelInfoOrFile =
"Model unavailable due to deleted local model info or model file."
2021-01-29 12:57:38 -08:00
static let outdatedModelPath =
"List models failed due to outdated model paths in local storage."
2021-02-10 09:47:07 -08:00
static let modelDeletionFailed = { (error: Error) in
"Model deletion failed with error: \(error)"
}
}
}
/// Model downloader extension for testing.
extension ModelDownloader {
/// Model downloader instance for testing.
static func modelDownloaderWithDefaults(_ defaults: GULUserDefaults,
2021-02-10 09:47:07 -08:00
app: FirebaseApp) -> ModelDownloader {
let downloader = ModelDownloader(app: app, defaults: defaults)
return downloader
}
}