spiegel-keyman/ios/engine/KMEI/KeymanEngine/Classes/KeyboardRepository/ResourceDownloadManager.swift
2019-08-16 15:58:39 +07:00

838 lines
33 KiB
Swift

//
// ResourceDownloadManager.swift
// KeymanEngine
//
// Created by Joshua Horton on 8/15/19.
// Copyright © 2019 SIL International. All rights reserved.
//
import Foundation
import Reachability
private class DownloadTask {
public enum Resource {
case keyboard, lexicalModel, other
}
public final var type: Resource
public final var resources: [LanguageResource]?
public final var request: HTTPDownloadRequest
public init(do request: HTTPDownloadRequest, for resources: [LanguageResource]?, type: DownloadTask.Resource) {
self.request = request
self.type = type
self.resources = resources
}
}
/**
* Represents one overall resource-related command for requests against the Keyman Cloud API.
*/
private class DownloadBatch {
public enum Activity {
case download, update
}
/*
* Three main cases so far:
* - [.download, .keyboard]: download new Keyboard (possibly with an associated lexical model)
* - [.download, .lexicalModel]: download new LexicalModel
* - [.update, -don't-care-]: batch update of all resources.
*/
public final var activity: Activity
public final var type: DownloadTask.Resource
public final var tasks: [DownloadTask]
//public final var promise: Int
public init(do tasks: [DownloadTask], as activity: Activity, ofType type: DownloadTask.Resource) {
self.activity = activity
self.type = type
self.tasks = tasks
}
}
public class ResourceDownloadManager: HTTPDownloadDelegate {
private var currentBatch: DownloadBatch?
private var downloadQueue: HTTPDownloader? = nil
var currentRequest: HTTPDownloadRequest?
private var reachability: Reachability!
private let keymanHostName = "api.keyman.com"
private var updateKbdQueue: [InstallableKeyboard]? = nil
private var updateLexQueue: [InstallableLexicalModel]? = nil
private var _isDoneButtonEnabled = false
private var isDidUpdateCheck = false
// private var keyboardDownloadStartedObserver: NotificationObserver?
// private var keyboardDownloadCompletedObserver: NotificationObserver?
// private var keyboardDownloadFailedObserver: NotificationObserver?
// private var lexicalModelDownloadStartedObserver: NotificationObserver?
// private var lexicalModelDownloadCompletedObserver: NotificationObserver?
// private var lexicalModelDownloadFailedObserver: NotificationObserver?
public static let shared = ResourceDownloadManager()
private init() {
//downloadQueue = HTTPDownloader(self) // TODO: Consider using a persistent one.
reachability = Reachability(hostname: keymanHostName)
// keyboardDownloadStartedObserver = NotificationCenter.default.addObserver(
// forName: Notifications.keyboardDownloadStarted,
// observer: self,
// function: ResourceDownloadManager.keyboardDownloadStarted)
// keyboardDownloadCompletedObserver = NotificationCenter.default.addObserver(
// forName: Notifications.keyboardDownloadCompleted,
// observer: self,
// function: ResourceDownloadManager.keyboardDownloadCompleted)
// keyboardDownloadFailedObserver = NotificationCenter.default.addObserver(
// forName: Notifications.keyboardDownloadFailed,
// observer: self,
// function: ResourceDownloadManager.keyboardDownloadFailed)
//
// lexicalModelDownloadStartedObserver = NotificationCenter.default.addObserver(
// forName: Notifications.lexicalModelDownloadStarted,
// observer: self,
// function: ResourceDownloadManager.lexicalModelDownloadStarted)
// lexicalModelDownloadCompletedObserver = NotificationCenter.default.addObserver(
// forName: Notifications.lexicalModelDownloadCompleted,
// observer: self,
// function: ResourceDownloadManager.lexicalModelDownloadCompleted)
// lexicalModelDownloadFailedObserver = NotificationCenter.default.addObserver(
// forName: Notifications.lexicalModelDownloadFailed,
// observer: self,
// function: ResourceDownloadManager.lexicalModelDownloadFailed)
}
deinit {
// Just to be safe, we'll invalidate any pending download requests when this class is deinitialized.
if let currentRequest = currentRequest {
currentRequest.userInfo["completionBlock"] = nil
}
}
public func updatesAvailable() -> Bool {
if Manager.shared.apiKeyboardRepository.languages == nil && Manager.shared.apiLexicalModelRepository.languages == nil {
return false
}
isDidUpdateCheck = true
let userKeyboards = Storage.active.userDefaults.userKeyboards
let hasKbdUpdate = userKeyboards?.contains { keyboard in
let kbID = keyboard.id
return stateForKeyboard(withID: kbID) == .needsUpdate
} ?? false
let userLexicalModels = Storage.active.userDefaults.userLexicalModels
let hasLexUpdate = userLexicalModels?.contains { lexicalModel in
let lmID = lexicalModel.id
return stateForLexicalModel(withID: lmID) == .needsUpdate
} ?? false
// FIXME: Testing only! Forces 'update'.
return true
//return hasKbdUpdate || hasLexUpdate
}
// TODO: Not yet ready.
public func performUpdates() {
// The plan is to create new notifications to handle batch updates here, rather than
// require a UI to manage the update queue.
updateKeyboards()
updateLexicalModels()
}
private func updateKeyboards() {
updateKbdQueue = []
var kbIDs = Set<String>()
// Build the keyboard update queue
Storage.active.userDefaults.userKeyboards?.forEach { kb in
let kbState = stateForKeyboard(withID: kb.id)
if kbState == .needsUpdate {
if(!kbIDs.contains(kb.id)) {
kbIDs.insert(kb.id)
updateKbdQueue?.append(kb)
}
}
}
// Execute the keyboard update queue
if !updateKbdQueue!.isEmpty {
let langID = updateKbdQueue![0].languageID
let kbID = updateKbdQueue![0].id
downloadKeyboard(withID: kbID, languageID: langID, isUpdate: true)
}
}
private func updateLexicalModels() {
// Build the lexical model update queue
updateLexQueue = []
var lmIDs = Set<String>()
Storage.active.userDefaults.userLexicalModels?.forEach { lm in
let lmState = stateForLexicalModel(withID: lm.id)
if lmState == .needsUpdate {
if !lmIDs.contains(lm.id) {
lmIDs.insert(lm.id)
updateLexQueue!.append(lm)
}
}
}
// Execute the lexical model update queue
if !updateLexQueue!.isEmpty {
let langID = updateLexQueue![0].languageID
let lmID = updateLexQueue![0].id
downloadLexicalModel(withID: lmID, languageID: langID, isUpdate: true)
}
}
// MARK: - Downloading keyboards and lexical models
private func checkCanDownload(keyboards: [InstallableKeyboard]) -> Bool {
guard reachability.connection != Reachability.Connection.none else {
let error = NSError(domain: "Keyman", code: 0,
userInfo: [NSLocalizedDescriptionKey: "No internet connection"])
downloadFailed(forKeyboards: keyboards, error: error)
return false
}
// At this stage, we now have everything needed to generate download requests.
guard downloadQueue == nil else {
let error = NSError(domain: "Keyman", code: 0,
userInfo: [NSLocalizedDescriptionKey: "Download queue is busy"])
downloadFailed(forKeyboards: keyboards, error: error)
return false
}
return true
}
private func checkCanDownload(lexicalModels: [InstallableLexicalModel]) -> Bool {
// Original implementation actually used the 'forKeyboards' versions seen below.
guard reachability.connection != Reachability.Connection.none else {
let error = NSError(domain: "Keyman", code: 0,
userInfo: [NSLocalizedDescriptionKey: "No internet connection"])
downloadFailed(forKeyboards: [], error: error)
return false
}
// At this stage, we now have everything needed to generate download requests.
guard downloadQueue == nil else {
let error = NSError(domain: "Keyman", code: 0,
userInfo: [NSLocalizedDescriptionKey: "Download queue is busy"])
downloadFailed(forKeyboards: [], error: error)
return false
}
return true
}
private func queueDownloadBatch(_ batch: DownloadBatch) {
downloadQueue = HTTPDownloader(self)
downloadQueue!.userInfo = [Key.downloadBatch: batch]
batch.tasks.forEach { task in
downloadQueue!.addRequest(task.request)
}
currentBatch = batch
downloadQueue!.run()
}
/// Asynchronously fetches the .js file for the keyboard with given IDs.
/// See `Notifications` for notification on success/failiure.
/// - Parameters:
/// - isUpdate: Keep the keyboard files on failure
/// - fetchRepositoryIfNeeded: Fetch the list of keyboards from the API if necessary.
public func downloadKeyboard(withID keyboardID: String,
languageID: String,
isUpdate: Bool,
fetchRepositoryIfNeeded: Bool = true) {
guard let keyboards = Manager.shared.apiKeyboardRepository.keyboards,
let options = Manager.shared.apiKeyboardRepository.options
else {
if fetchRepositoryIfNeeded {
log.info("Fetching repository from API for keyboard download")
Manager.shared.apiKeyboardRepository.fetch { error in
if let error = error {
self.downloadFailed(forKeyboards: [], error: error)
} else {
log.info("Fetched repository. Continuing with keyboard download.")
self.downloadKeyboard(withID: keyboardID,
languageID: languageID,
isUpdate: isUpdate,
fetchRepositoryIfNeeded: false)
}
}
return
}
let message = "Keyboard repository not yet fetched"
let error = NSError(domain: "Keyman", code: 0, userInfo: [NSLocalizedDescriptionKey: message])
downloadFailed(forKeyboards: [], error: error)
return
}
// Grab info for the relevant API version of the keyboard.
guard let keyboard = Manager.shared.apiKeyboardRepository.installableKeyboard(withID: keyboardID, languageID: languageID),
let filename = keyboards[keyboardID]?.filename
else {
let message = "Keyboard not found with id: \(keyboardID), languageID: \(languageID)"
let error = NSError(domain: "Keyman", code: 0,
userInfo: [NSLocalizedDescriptionKey: message])
downloadFailed(forKeyboards: [], error: error)
return
}
// Perform common 'can download' check. We need positive reachability and no prior download queue.
if !checkCanDownload(keyboards: [keyboard]) {
return
}
if let dlBatch = buildKeyboardDownloadBatch(for: keyboard, withOptions: options, withFilename: filename, asActivity: isUpdate ? .update : .download) {
queueDownloadBatch(dlBatch)
self.downloadLexicalModelsForLanguageIfExists(languageID: languageID)
}
}
private func keyboardFontURLs(forFont font: Font?, options: Options) -> [URL] {
guard let font = font else {
return []
}
return font.source.filter({ $0.hasFontExtension })
.map({ options.fontBaseURL.appendingPathComponent($0) })
}
/// Assumes that Keyboard has font and oskFont set and ignores fonts contained in Language.
private func downloadKeyboard(_ keyboardAPI: KeyboardAPICall) {
let keyboard = keyboardAPI.keyboard
let installableKeyboards = keyboard.languages!.map { language in
InstallableKeyboard(keyboard: keyboard, language: language, isCustom: true)
}
let filename = keyboard.filename
// Perform the standard 'ready-to-download' checks.
if !checkCanDownload(keyboards: installableKeyboards) {
return
}
let isUpdate = Storage.active.userDefaults.userKeyboards?.contains { $0.id == keyboard.id } ?? false
downloadQueue = HTTPDownloader(self)
// We're only installing a single keyboard, even if for multiple languages. We should only do the actual 'download' task once.
if let dlBatch = buildKeyboardDownloadBatch(for: installableKeyboards[0], withOptions: keyboardAPI.options, withFilename: filename,
asActivity: isUpdate ? .update : .download) {
// Since we're working from a direct-KMP install, we actually want to denote ALL language variants the KMP lists.
// This differs from cloud installs.
dlBatch.tasks.forEach { task in
task.resources = installableKeyboards
}
queueDownloadBatch(dlBatch)
}
}
private func buildKeyboardDownloadBatch(for keyboard: InstallableKeyboard, withOptions options: Options, withFilename filename: String,
asActivity activity: DownloadBatch.Activity) -> DownloadBatch? {
let keyboardURL = options.keyboardBaseURL.appendingPathComponent(filename)
let fontURLs = Array(Set(keyboardFontURLs(forFont: keyboard.font, options: options) +
keyboardFontURLs(forFont: keyboard.oskFont, options: options)))
do {
try FileManager.default.createDirectory(at: Storage.active.keyboardDir(forID: keyboard.id),
withIntermediateDirectories: true)
} catch {
log.error("Could not create dir for download: \(error)")
return nil
}
var request = HTTPDownloadRequest(url: keyboardURL, userInfo: [:])
request.destinationFile = Storage.active.keyboardURL(forID: keyboard.id, version: keyboard.version).path
request.tag = 0
let keyboardTask = DownloadTask(do: request, for: [keyboard], type: .keyboard)
var batchTasks: [DownloadTask] = [ keyboardTask ]
for (i, url) in fontURLs.enumerated() {
request = HTTPDownloadRequest(url: url, userInfo: [:])
request.destinationFile = Storage.active.fontURL(forKeyboardID: keyboard.id, filename: url.lastPathComponent).path
request.tag = i + 1
let fontTask = DownloadTask(do: request, for: nil, type: .other)
batchTasks.append(fontTask)
}
let batch = DownloadBatch(do: batchTasks, as: activity, ofType: .keyboard)
batch.tasks.forEach { task in
task.request.userInfo[Key.downloadBatch] = batch
task.request.userInfo[Key.downloadTask] = task
}
return batch
}
/// Downloads a custom keyboard from the URL
/// - Parameters:
/// - url: URL to a JSON description of the keyboard
public func downloadKeyboard(from url: URL) {
guard reachability.connection != Reachability.Connection.none else {
let error = NSError(domain: "Keyman", code: 0,
userInfo: [NSLocalizedDescriptionKey: "No connection"])
downloadFailed(forKeyboards: [], error: error)
return
}
guard let data = try? Data(contentsOf: url) else {
let error = NSError(domain: "Keyman", code: 0,
userInfo: [NSLocalizedDescriptionKey: "Failed to fetch JSON file"])
downloadFailed(forKeyboards: [], error: error)
return
}
decodeKeyboardData(data, decodingStrategy: .ios8601WithFallback)
}
private func decodeKeyboardData(_ data: Data, decodingStrategy : JSONDecoder.DateDecodingStrategy) {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = decodingStrategy
if let keyboard = try? decoder.decode(KeyboardAPICall.self, from: data) {
downloadKeyboard(keyboard)
} else {
decoder.dateDecodingStrategy = .iso8601WithoutTimezone
if let keyboard = try? decoder.decode(KeyboardAPICall.self, from: data) {
downloadKeyboard(keyboard)
} else {
decoder.dateDecodingStrategy = .ios8601WithMilliseconds
do {
let keyboard = try decoder.decode(KeyboardAPICall.self, from: data)
downloadKeyboard(keyboard)
} catch {
downloadFailed(forKeyboards: [], error: error)
}
}
}
}
/// - Returns: The current state for a keyboard
public func stateForKeyboard(withID keyboardID: String) -> KeyboardState {
// Needs validation - I don't think this if-condition can be met in Keyman's current state
// (as of 2019-08-16)
if keyboardIdForCurrentRequest() == keyboardID {
return .downloading
}
let userKeyboards = Storage.active.userDefaults.userKeyboards
guard let userKeyboard = userKeyboards?.first(where: { $0.id == keyboardID }) else {
return .needsDownload
}
// Check version
if let repositoryVersionString = Manager.shared.apiKeyboardRepository.keyboards?[keyboardID]?.version {
let downloadedVersion = Version(userKeyboard.version) ?? Version.fallback
let repositoryVersion = Version(repositoryVersionString) ?? Version.fallback
if downloadedVersion < repositoryVersion {
return .needsUpdate
}
}
return .upToDate
}
// Needs validation - I don't think this function is practically utilized (as of 2019-08-16)
func keyboardIdForCurrentRequest() -> String? {
if let currentRequest = currentRequest {
let tmpStr = currentRequest.url.lastPathComponent
if tmpStr.hasJavaScriptExtension {
return String(tmpStr.dropLast(3))
}
} else if let downloadQueue = downloadQueue {
let kbInfo = downloadQueue.userInfo[Key.keyboardInfo]
if let keyboards = kbInfo as? [InstallableKeyboard], let keyboard = keyboards.first {
return keyboard.id
}
}
return nil
}
// return a lexical model so caller can use it in a downloadSucceeded call
// is called by other class funcs
func installLexicalModelPackage(downloadedPackageFile: URL) -> InstallableLexicalModel? {
var installedLexicalModel: InstallableLexicalModel? = nil
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
var destination = documentsDirectory
destination.appendPathComponent("temp/\(downloadedPackageFile.lastPathComponent)")
KeymanPackage.extract(fileUrl: downloadedPackageFile, destination: destination, complete: { kmp in
if let kmp = kmp as! LexicalModelKeymanPackage? {
do {
try Manager.shared.parseLMKMP(kmp.sourceFolder)
log.info("successfully parsed the lexical model in: \(kmp.sourceFolder)")
installedLexicalModel = kmp.models[0].installableLexicalModels[0]
//this can fail gracefully and not show errors to users
try FileManager.default.removeItem(at: downloadedPackageFile)
} catch {
log.error("Error installing the lexical model: \(error)")
}
} else {
log.error("Error extracting the lexical model from the package: \(KMPError.invalidPackage)")
}
})
return installedLexicalModel
}
// Actively used in Settings
func downloadLexicalModelPackage(url lexicalModelPackageURL: URL) -> Void {
//determine where to put the data (local file URL)
var destinationUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
destinationUrl.appendPathComponent("\(lexicalModelPackageURL.lastPathComponent).zip")
//callback to handle the data downloaded
func lexicalModelDownloaded(data: Data?,
response: URLResponse?,
dest: URL,
error: Error?) {
if let error = error {
log.error("Failed to fetch lexical model KMP file")
downloadFailed(forLexicalModelPackage: "\(lexicalModelPackageURL)", error: error)
} else {
do {
try data!.write(to: dest)
} catch {
log.error("Error writing the lexical model download data: \(error)")
}
if let lm = installLexicalModelPackage(downloadedPackageFile: dest) {
downloadSucceeded(forLexicalModel: lm)
} else {
let installError = NSError(domain: "Keyman", code: 0,
userInfo: [NSLocalizedDescriptionKey: "installError"])
downloadFailed(forLexicalModelPackage: "\(lexicalModelPackageURL)", error: installError )
}
}
}
log.info("downloading lexical model from Keyman cloud: \(lexicalModelPackageURL).")
let task = URLSession.shared.dataTask(with: lexicalModelPackageURL) { (data, response, error) in
DispatchQueue.main.async {
lexicalModelDownloaded(data: data, response: response, dest: destinationUrl, error: error)
}
}
task.resume()
}
// Can be called by the cloud keyboard downloader and utilized.
/// Starts the process of fetching the package file of the lexical model for the given language ID
/// first it fetches the list of lexical models for the given language
/// then it takes the first of the list and download the KMP package file and asks the app to open it (like adhoc download)
/// - Parameters:
/// - languageID: the bcp47 string of the desired language
public func downloadLexicalModelsForLanguageIfExists(languageID: String) {
//get list of lexical models for this languageID /?q=bcp47:en
func listCompletionHandler(lexicalModels: [LexicalModel]?, error: Error?) -> Void {
if let error = error {
log.info("Failed to fetch lexical model list for "+languageID+". error: "+(error as! String))
self.downloadFailed(forLanguageID: languageID, error: error) //???forKeyboards
} else if nil == lexicalModels {
//TODO: put up an alert instead
log.info("No lexical models available for language \(languageID) (nil)")
} else if 0 == lexicalModels?.count {
log.info("No lexical models available for language \(languageID) (empty)")
} else {
log.info("Fetched lexical model list for "+languageID+".")
// choose which of the lexical models to download
// for now, this just downloads the first one
let chosenIndex = 0
if let lexicalModel = lexicalModels?[chosenIndex] {
downloadLexicalModelPackage(url: URL.init(string: lexicalModel.packageFilename)!)
} else {
log.info("no error, but no lexical model in list, either!")
}
}
}
Manager.shared.apiLexicalModelRepository.fetchList(languageID: languageID, completionHandler: listCompletionHandler)
}
/// Asynchronously fetches the .js file for the lexical model with given IDs.
/// See `Notifications` for notification on success/failiure.
/// - Parameters:
/// - isUpdate: Keep the lexical model files on failure
/// - fetchRepositoryIfNeeded: Fetch the list of lexical models from the API if necessary.
public func downloadLexicalModel(withID lexicalModelID: String,
languageID: String,
isUpdate: Bool,
fetchRepositoryIfNeeded: Bool = true) {
// Everything here was already commented-out.
}
private func lexicalModelFontURLs(forFont font: Font?, options: Options) -> [URL] {
guard let font = font else {
return []
}
return font.source.filter({ $0.hasFontExtension })
.map({ options.fontBaseURL.appendingPathComponent($0) })
}
/// Downloads a custom lexical model from the URL
/// - Parameters:
/// - url: URL to a JSON description of the lexical model
public func downloadLexicalModel(from url: URL) {
guard reachability.connection != Reachability.Connection.none else {
let error = NSError(domain: "Keyman", code: 0,
userInfo: [NSLocalizedDescriptionKey: "No connection"])
downloadFailed(forKeyboards: [], error: error) //??? forLexicalModels
return
}
guard let data = try? Data(contentsOf: url) else {
let error = NSError(domain: "Keyman", code: 0,
userInfo: [NSLocalizedDescriptionKey: "Failed to fetch JSON file"])
downloadFailed(forKeyboards: [], error: error) //??? forLexicalModels
return
}
decodeLexicalModelData(data, decodingStrategy: .ios8601WithFallback)
}
private func decodeLexicalModelData(_ data: Data, decodingStrategy : JSONDecoder.DateDecodingStrategy) {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = decodingStrategy
if let lexicalModel = try? decoder.decode(LexicalModelAPICall.self, from: data) {
downloadLexicalModel(lexicalModel)
} else {
decoder.dateDecodingStrategy = .iso8601WithoutTimezone
if let lexicalModel = try? decoder.decode(LexicalModelAPICall.self, from: data) {
downloadLexicalModel(lexicalModel)
} else {
decoder.dateDecodingStrategy = .ios8601WithMilliseconds
do {
let lexicalModel = try decoder.decode(LexicalModelAPICall.self, from: data)
downloadLexicalModel(lexicalModel)
} catch {
downloadFailed(forKeyboards: [], error: error) //??? forLexicalModels
}
}
}
}
private func downloadLexicalModel(_ lexicalModelAPI: LexicalModelAPICall) {
let lexicalModel = lexicalModelAPI.lexicalModels[0]
let installableLexicalModels = lexicalModel.languages.map { language in
InstallableLexicalModel(lexicalModel: lexicalModel, languageID: language, isCustom: true)
}
let packageFilename = lexicalModel.packageFilename
let lexicalModelURL = URL(string: "https://api.keyman.com/model")!.appendingPathComponent(packageFilename)
if !checkCanDownload(lexicalModels: installableLexicalModels) {
return
}
do {
try FileManager.default.createDirectory(at: Storage.active.lexicalModelDir(forID: lexicalModel.id),
withIntermediateDirectories: true)
} catch {
log.error("Could not create dir for download: \(error)")
return
}
let isUpdate = Storage.active.userDefaults.userLexicalModels?.contains { $0.id == lexicalModel.id } ?? false
downloadQueue = HTTPDownloader(self)
let commonUserData: [String: Any] = [
Key.lexicalModelInfo: installableLexicalModels,
Key.update: isUpdate
]
downloadQueue!.userInfo = commonUserData
let request = HTTPDownloadRequest(url: lexicalModelURL, userInfo: commonUserData)
request.destinationFile = Storage.active.lexicalModelURL(forID: lexicalModel.id, version: lexicalModel.version ?? InstallableConstants.defaultVersion).path
request.tag = 0
downloadQueue!.addRequest(request)
downloadQueue!.run()
}
/// - Returns: The current state for a lexical model
//TODO: rename KeyboardState to ResourceState? so it can be used with both keybaoards and lexical models without confusion
public func stateForLexicalModel(withID lexicalModelID: String) -> KeyboardState {
if lexicalModelIdForCurrentRequest() == lexicalModelID {
return .downloading
}
let userLexicalModels = Storage.active.userDefaults.userLexicalModels
guard let userLexicalModel = userLexicalModels?.first(where: { $0.id == lexicalModelID }) else {
return .needsDownload
}
// Check version
if let repositoryVersionString = Manager.shared.apiLexicalModelRepository.lexicalModels?[lexicalModelID]?.version {
let downloadedVersion = Version(userLexicalModel.version) ?? Version.fallback
let repositoryVersion = Version(repositoryVersionString) ?? Version.fallback
if downloadedVersion < repositoryVersion {
return .needsUpdate
}
}
return .upToDate
}
func lexicalModelIdForCurrentRequest() -> String? {
if let currentRequest = currentRequest {
let tmpStr = currentRequest.url.lastPathComponent
if tmpStr.hasJavaScriptExtension {
return String(tmpStr.dropLast(3))
}
} else if let downloadQueue = downloadQueue {
let kbInfo = downloadQueue.userInfo[Key.lexicalModelInfo]
if let lexicalModels = kbInfo as? [InstallableLexicalModel], let lexicalModel = lexicalModels.first {
return lexicalModel.id
}
}
return nil
}
// MARK - deprecated helper/handler methods - they help avoid errors for now.
private func downloadFailed(forKeyboards keyboards: [InstallableKeyboard], error: Error) {
let notification = KeyboardDownloadFailedNotification(keyboards: keyboards, error: error)
NotificationCenter.default.post(name: Notifications.keyboardDownloadFailed,
object: self,
value: notification)
}
private func downloadFailed(forLanguageID languageID: String, error: Error) {
let notification = LexicalModelDownloadFailedNotification(lmOrLanguageID: languageID, error: error)
NotificationCenter.default.post(name: Notifications.lexicalModelDownloadFailed,
object: self,
value: notification)
}
private func downloadFailed(forLexicalModelPackage packageURL: String, error: Error) {
let notification = LexicalModelDownloadFailedNotification(lmOrLanguageID: packageURL, error: error)
NotificationCenter.default.post(name: Notifications.lexicalModelDownloadFailed,
object: self,
value: notification)
}
private func downloadSucceeded(forLexicalModel lm: InstallableLexicalModel) {
let notification = LexicalModelDownloadCompletedNotification([lm])
NotificationCenter.default.post(name: Notifications.lexicalModelDownloadCompleted,
object: self,
value: notification)
}
//MARK: - HTTPDownloadDelegate methods
func downloadQueueFinished(_ queue: HTTPDownloader) {
// We can use the properties of the current "batch" to generate specialized notifications.
currentBatch = nil
}
func downloadRequestStarted(_ request: HTTPDownloadRequest) {
// If we're downloading a new keyboard.
// The extra check is there to filter out other potential request types in the future.
if request.tag == 0 {
let task = request.userInfo[Key.downloadTask] as! DownloadTask
if task.type == .keyboard {
NotificationCenter.default.post(name: Notifications.keyboardDownloadStarted,
object: self,
value: task.resources as! [InstallableKeyboard])
} else if task.type == .lexicalModel {
NotificationCenter.default.post(name: Notifications.lexicalModelDownloadStarted,
object: self,
value: task.resources as! [InstallableLexicalModel])
}
}
}
func downloadRequestFinished(_ request: HTTPDownloadRequest) {
let batch = request.userInfo[Key.downloadBatch] as! DownloadBatch
let isUpdate = batch.activity == .update
let task = request.userInfo[Key.downloadTask] as! DownloadTask
// FIXME
if let statusCode = request.responseStatusCode, statusCode == 200 {
if task.type == .keyboard {
// The request has succeeded.
if downloadQueue!.requestsCount == 0 {
let keyboards = task.resources as? [InstallableKeyboard]
// Download queue finished.
downloadQueue = nil
FontManager.shared.registerCustomFonts()
log.info("Downloaded keyboard: \(keyboards![0].id).")
NotificationCenter.default.post(name: Notifications.keyboardDownloadCompleted,
object: self,
value: keyboards!)
// TODO: Trigger by notification. Needs to be done on Manager.swift, not this class.
// if isUpdate {
// shouldReloadKeyboard = true
// inputViewController.reload()
// }
let userDefaults = Storage.active.userDefaults
userDefaults.set([Date()], forKey: Key.synchronizeSWKeyboard)
userDefaults.synchronize()
}
}
} else { // Possible request error (400 Bad Request, 404 Not Found, etc.)
if task.type == .keyboard {
let keyboards = task.resources as? [InstallableKeyboard]
downloadQueue!.cancelAllOperations()
downloadQueue = nil
let errorMessage = "\(request.responseStatusMessage ?? ""): \(request.url)"
let error = NSError(domain: "Keyman", code: 0,
userInfo: [NSLocalizedDescriptionKey: errorMessage])
log.error("Keyboard download failed: \(error).")
if !isUpdate {
// Clean up keyboard file if anything fails
// TODO: Also clean up remaining fonts
try? FileManager.default.removeItem(at: Storage.active.keyboardURL(for: keyboards![0]))
}
// TODO:
//Manager.shared.downloadFailed(forKeyboards: keyboards, error: error)
}
}
}
//func downloadRequestSuccess(
func downloadRequestFailed(_ request: HTTPDownloadRequest) {
switch request.typeCode {
case .downloadFile:
downloadQueue = nil
let error = request.error!
log.error("Keyboard download failed: \(error).")
let task = request.userInfo[Key.downloadTask] as! DownloadTask
let batch = request.userInfo[Key.downloadBatch] as! DownloadBatch
let isUpdate = batch.activity == .update
if task.type == .keyboard {
// FIXME
let keyboards = task.resources as? [InstallableKeyboard]
if !isUpdate {
// Clean up keyboard file if anything fails
// TODO: Also clean up remaining fonts
try? FileManager.default.removeItem(at: Storage.active.keyboardURL(for: keyboards![0]))
}
// TODO:
//Manager.shared.downloadFailed(forKeyboards: keyboards, error: error as NSError)
}
}
}
// End REWORK THIS SECTION ------
}