mirror of
https://github.com/keymanapp/keyman.git
synced 2026-08-12 11:55:32 +00:00
838 lines
33 KiB
Swift
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 ------
|
|
}
|