Skip to content

Commit

Permalink
Merge pull request #3277 from keymanapp/refactor/ios/engine/download-…
Browse files Browse the repository at this point in the history
…installation-completion-blocks

refactor(ios/engine): Resource-download installation closures
  • Loading branch information
jahorton committed Jun 25, 2020
2 parents 7b08471 + fcdaac1 commit cf3c050
Show file tree
Hide file tree
Showing 8 changed files with 140 additions and 95 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -164,8 +164,12 @@ class LanguageDetailViewController: UITableViewController, UIAlertViewDelegate {
}

func downloadHandler(_ keyboardIndex: Int) {
ResourceDownloadManager.shared.downloadKeyboard(withID: language.keyboards![keyboardIndex].id,
languageID: language.id, isUpdate: isUpdate)
let fullID = FullKeyboardID(keyboardID: language.keyboards![keyboardIndex].id, languageID: language.id)
let installClosure = ResourceDownloadManager.shared.standardKeyboardInstallCompletionBlock(forFullID: fullID, withModel: true)
ResourceDownloadManager.shared.downloadKeyboard(withID: fullID.keyboardID,
languageID: fullID.languageID,
isUpdate: isUpdate,
completionBlock: installClosure)
}

func showActivityView() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,12 @@ class LanguageLMDetailViewController: UITableViewController, UIAlertViewDelegate
//NOTE: there is no need for a CompletedObserver, as our parent LexicalModelPickerViewController
// is registered for that and deals with it by popping us out to root.
private var lexicalModelDownloadFailedObserver: NotificationObserver?

private var onSuccessClosure: ((InstallableLexicalModel) -> Void)?

init(language: Language) {
init(language: Language, onSuccess: ((InstallableLexicalModel) -> Void)?) {
self.language = language
self.onSuccessClosure = onSuccess
super.init(nibName: nil, bundle: nil)
}

Expand Down Expand Up @@ -138,7 +141,15 @@ class LanguageLMDetailViewController: UITableViewController, UIAlertViewDelegate

func downloadHandler(_ lexicalModelIndex: Int) {
let lexicalModel = lexicalModels![lexicalModelIndex]
ResourceDownloadManager.shared.downloadLexicalModel(withID: lexicalModel.id, languageID: language.id, isUpdate: false)
let lmFullID = FullLexicalModelID(lexicalModelID: lexicalModel.id, languageID: language.id)
let completionClosure: ResourceDownloadManager.CompletionHandler<InstallableLexicalModel> = { package, error in
ResourceDownloadManager.shared.standardLexicalModelInstallCompletionBlock(forFullID: lmFullID)(package, error)

if let lm = package?.findResource(withID: lmFullID) {
self.onSuccessClosure?(lm)
}
}
ResourceDownloadManager.shared.downloadLexicalModel(withID: lexicalModel.id, languageID: language.id, isUpdate: false, completionBlock: completionClosure)
}

private func lexicalModelDownloadStarted() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -235,8 +235,9 @@ class LanguageViewController: UITableViewController, UIAlertViewDelegate {

func downloadHandler(_ keyboardIndex: Int) {
let language = languages[selectedSection]
let keyboard = language.keyboards![keyboardIndex]
ResourceDownloadManager.shared.downloadKeyboard(withID: keyboard.id, languageID: language.id, isUpdate: isUpdate)
let fullID = FullKeyboardID(keyboardID: language.keyboards![keyboardIndex].id, languageID: language.id)
let installClosure = ResourceDownloadManager.shared.standardKeyboardInstallCompletionBlock(forFullID: fullID, withModel: true)
ResourceDownloadManager.shared.downloadKeyboard(withID: fullID.keyboardID, languageID: fullID.languageID, isUpdate: isUpdate, completionBlock: installClosure)
}

private func keyboardDownloadStarted() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,13 +193,6 @@ class LexicalModelPickerViewController: UITableViewController, UIAlertViewDelega
navigationItem.rightBarButtonItem?.isEnabled = true
}

// Add lexicalModel.
for lexicalModel in lexicalModels {
if lexicalModel.languageID == language?.id {
switchLexicalModel(lexicalModel)
}
}

navigationController?.popToRootViewController(animated: true)
}

Expand Down Expand Up @@ -315,7 +308,9 @@ class LexicalModelPickerViewController: UITableViewController, UIAlertViewDelega
DispatchQueue.main.async {
let button: UIButton? = (self.navigationController?.toolbar?.viewWithTag(toolbarButtonTag) as? UIButton)
button?.isEnabled = false
let vc = LanguageLMDetailViewController(language: self.language)
let vc = LanguageLMDetailViewController(language: self.language, onSuccess: { lm in
self.switchLexicalModel(lm)
})
vc.lexicalModels = lexicalModels!
self.navigationController?.pushViewController(vc, animated: true)
}
Expand Down
10 changes: 8 additions & 2 deletions ios/engine/KMEI/KeymanEngine/Classes/Manager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -838,18 +838,24 @@ public class Manager: NSObject, UIGestureRecognizerDelegate {
*/

public func downloadKeyboard(withID: String, languageID: String, isUpdate: Bool, fetchRepositoryIfNeeded: Bool = true) {
let kbdFullID = FullKeyboardID(keyboardID: withID, languageID: languageID)
let completionBlock = ResourceDownloadManager.shared.standardKeyboardInstallCompletionBlock(forFullID: kbdFullID, withModel: true)
ResourceDownloadManager.shared.downloadKeyboard(withID: withID,
languageID: languageID,
isUpdate: isUpdate,
fetchRepositoryIfNeeded: fetchRepositoryIfNeeded)
fetchRepositoryIfNeeded: fetchRepositoryIfNeeded,
completionBlock: completionBlock)
}

// A new API, but it so closely parallels downloadKeyboard that we should add a 'helper' handler here.
public func downloadLexicalModel(withID: String, languageID: String, isUpdate: Bool, fetchRepositoryIfNeeded: Bool = true) {
let lmFullID = FullLexicalModelID(lexicalModelID: withID, languageID: languageID)
let completionBlock = ResourceDownloadManager.shared.standardLexicalModelInstallCompletionBlock(forFullID: lmFullID)
ResourceDownloadManager.shared.downloadLexicalModel(withID: withID,
languageID: languageID,
isUpdate: isUpdate,
fetchRepositoryIfNeeded: fetchRepositoryIfNeeded)
fetchRepositoryIfNeeded: fetchRepositoryIfNeeded,
completionBlock: completionBlock)
}

public func stateForKeyboard(withID keyboardID: String) -> KeyboardState {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,10 +166,12 @@ public class ResourceDownloadManager {
else {
return
}

if let _ = downloadKeyboardCore(withMetadata: [keyboard], asActivity: isUpdate ? .update : .download, withFilename: filename, withOptions: options) {
self.downloadLexicalModelsForLanguageIfExists(languageID: languageID)
}

let _ = downloadKeyboardCore(withMetadata: [keyboard],
asActivity: isUpdate ? .update : .download,
withFilename: filename,
withOptions: options,
completionBlock: completionBlock)
}

private func keyboardFontURLs(forFont font: Font?, options: Options) -> [URL] {
Expand Down Expand Up @@ -315,7 +317,13 @@ public class ResourceDownloadManager {
if let lexicalModel = lexicalModels?[chosenIndex] {
//downloadLexicalModelPackage(url: URL.init(string: lexicalModel.packageFilename)!)
// We've already fetched part of the repository to do this.
downloadLexicalModel(withID: lexicalModel.id, languageID: languageID, isUpdate: false, fetchRepositoryIfNeeded: false)
let lmFullID = FullLexicalModelID(lexicalModelID: lexicalModel.id, languageID: languageID)
let completionClosure = standardLexicalModelInstallCompletionBlock(forFullID: lmFullID)
downloadLexicalModel(withID: lexicalModel.id,
languageID: languageID,
isUpdate: false,
fetchRepositoryIfNeeded: false,
completionBlock: completionClosure)
} else {
log.info("no error, but no lexical model in list, either!")
}
Expand Down Expand Up @@ -365,7 +373,10 @@ public class ResourceDownloadManager {
return
}

_ = downloadLexicalModelCore(withMetadata: [lexicalModel], asActivity: isUpdate ? .update : .download, fromPath: URL.init(string: filename)!, completionBlock: completionBlock)
_ = downloadLexicalModelCore(withMetadata: [lexicalModel],
asActivity: isUpdate ? .update : .download,
fromPath: URL.init(string: filename)!,
completionBlock: completionBlock)
}

/// - Returns: The current state for a lexical model
Expand Down Expand Up @@ -488,9 +499,21 @@ public class ResourceDownloadManager {
return updateQueue
}

@available(*, deprecated)
public func installLexicalModelPackage(at packageURL: URL) -> InstallableLexicalModel? {
let (lm, _) = downloader.installLexicalModelPackage(downloadedPackageFile: packageURL)
return lm
do {
if let package = try ResourceFileManager.shared.prepareKMPInstall(from: packageURL) as? LexicalModelKeymanPackage {
try ResourceFileManager.shared.finalizePackageInstall(package, isCustom: false)
// The reason we're deprecating it; only returns the first model, even if more language pairings are installed.
return package.installables[0][0]
} else {
log.error("Specified package (at \(packageURL)) does not contain lexical models: \(KMPError.invalidPackage)")
return nil
}
} catch {
log.error("Error occurred while attempting to install package from \(packageURL): \(String(describing: error))")
return nil
}
}

// MARK - Completion handlers.
Expand Down Expand Up @@ -535,12 +558,27 @@ public class ResourceDownloadManager {
return { package, error in
if let package = package {
// Do not send notifications for individual resource updates.
// let resourceIDs: [Resource.FullID] = resources.map { $0.typedFullID }
// do {
// try ResourceFileManager.shared.install(resourcesWithIDs: resourceIDs, from: package)
// } catch {
// log.error("Error updating resources from package \(package.id)")
// }
let resourceIDs: [Resource.FullID] = resources.map { $0.typedFullID }
do {
if let keyboards = resources as? [InstallableKeyboard], let package = package as? KeyboardKeymanPackage {
// TEMP: currently required because downloaded keyboards aren't actually in packages.
keyboards.forEach { keyboard in
if let updatedKbd = package.findResource(withID: keyboard.typedFullID) {
Manager.shared.updateUserKeyboards(with: updatedKbd)

if Manager.shared.currentKeyboard?.fullID == keyboard.fullID {
// Issue: does not actually trigger a reload if the user isn't within the Settings view hierarchy
// Fixing this requires a refactor of `shouldReloadKeyboard`.
Manager.shared.shouldReloadKeyboard = true
}
}
}
} else if let _ = resources as? [InstallableLexicalModel] {
try ResourceFileManager.shared.install(resourcesWithIDs: resourceIDs, from: package)
}
} catch {
log.error("Error updating resources from package \(package.id)")
}

// After the custom handler operates, ensure that any changes it made are synchronized for use
// with the app extension, too.
Expand All @@ -551,6 +589,55 @@ public class ResourceDownloadManager {
}
}

public func standardKeyboardInstallCompletionBlock(forFullID fullID: FullKeyboardID, withModel: Bool = true) -> CompletionHandler<InstallableKeyboard> {
return { package, error in
if let package = package {
// The "keyboard package" is actually a cloud resource that was already directly installed.
// So, we don't 'install it from the package'; we just note that it's already present.
let fullID = FullKeyboardID(keyboardID: fullID.keyboardID, languageID: fullID.languageID)

// Since we're installing from a cloud resource, the files are already appropriately placed.
// We just need to actually add the resource.
if let keyboard = package.findResource(withID: fullID) {
ResourceFileManager.shared.addResource(keyboard)
_ = Manager.shared.setKeyboard(keyboard)

if withModel {
self.downloadLexicalModelsForLanguageIfExists(languageID: fullID.languageID)
}
} else {
log.error("Expected resource \(fullID.description) download failed; resource unexpectedly missing")
}
} else if let error = error {
log.error("Installation failed: \(String(describing: error))")
} else {
log.error("Unknown error when attempting to install \(fullID.description))")
}
}
}

public func standardLexicalModelInstallCompletionBlock(forFullID fullID: FullLexicalModelID) -> CompletionHandler<InstallableLexicalModel> {
return { package, error in
if let package = package {
do {
// A raw port of the queue's old installation method for lexical models.
try ResourceFileManager.shared.finalizePackageInstall(package, isCustom: false)
log.info("successfully parsed the lexical model in: \(package.sourceFolder)")

if let installedLexicalModel = package.findResource(withID: fullID) {
_ = Manager.shared.registerLexicalModel(installedLexicalModel)
}
} catch {
log.error("Error installing the lexical model: \(String(describing: error))")
}
} else if let error = error {
log.error("Error downloading the lexical model \(String(describing: error))")
} else {
log.error("Unknown error when attempting to install \(fullID.description)")
}
}
}

// MARK - Notifications
internal func resourceDownloadStarted<Resource: LanguageResource>(for resources: [Resource]) {
if let keyboards = resources as? [InstallableKeyboard] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -427,8 +427,8 @@ class ResourceDownloadQueue: HTTPDownloadDelegate {
func downloadQueueFinished(_ queue: HTTPDownloader) {
// We can use the properties of the current "batch" to generate specialized notifications.
let batch = queue.userInfo[Key.downloadBatch] as! AnyDownloadBatch
let isUpdate = batch.activity == .update

// Really, "if resource is installed from the cloud, only packaged locally"
if let batch = batch as? DownloadBatch<InstallableKeyboard> {
// batch.downloadTasks[0].request.destinationFile - currently, the downloaded keyboard .js.
// Once downlading KMPs, will be the downloaded .kmp.
Expand All @@ -440,30 +440,20 @@ class ResourceDownloadQueue: HTTPDownloadDelegate {

// TEMP: wrap the newly-downloaded resources with a kmp.json.
// Serves as a bridge until we're downloading actual .kmps for keyboards.
let wrappedKeyboards = Migrations.migrateToKMPFormat(keyboards)

wrappedKeyboards.forEach { keyboard in
ResourceFileManager.shared.addResource(keyboard)
}
let _ = Migrations.migrateToKMPFormat(keyboards)

if let package: KeyboardKeymanPackage = ResourceFileManager.shared.getInstalledPackage(for: keyboards[0]) {
batch.completionBlock?(package, nil)
} else {
log.error("Could not load metadata for newly-installed keyboard")
}
}
// else "if resource is installed from an actual KMP"
// - in other words, the long-term target.
} else if let batch = batch as? DownloadBatch<InstallableLexicalModel> {
let task = batch.downloadTasks[0] // It's always at this index.
let (lm, package) = installLexicalModelPackage(downloadedPackageFile: URL(fileURLWithPath: task.request.destinationFile!))
if let lm = lm, let package = package {
if !isUpdate {
// Since we don't generate the notification as above, we need to manually update
// the lexical model's metadata.
Manager.shared.updateUserLexicalModels(with: lm)
}

batch.completionBlock?(package, nil)
}
let packagePath = URL(fileURLWithPath: task.request.destinationFile!)
batch.completeWithPackage(fromKMP: packagePath)
}

// Completing the queue means having completed a batch. We should only move forward in this class's
Expand Down Expand Up @@ -523,39 +513,4 @@ class ResourceDownloadQueue: HTTPDownloadDelegate {
batch.completeWithError(error: error)
downloader!.cancelAllOperations()
}

// MARK - Language resource installation methods

// Processes fetched lexical models.
// return a lexical model so caller can use it in a downloadSucceeded call
// is called by other class funcs
public func installLexicalModelPackage(downloadedPackageFile: URL) -> (InstallableLexicalModel?, LexicalModelKeymanPackage?) {
var installedLexicalModel: InstallableLexicalModel? = nil
var kmp: LexicalModelKeymanPackage?

do {
let package = try ResourceFileManager.shared.prepareKMPInstall(from: downloadedPackageFile)
kmp = package as? LexicalModelKeymanPackage
if let kmp = kmp {
// Now that we have the package, we do the actual lexical model installation
// Marked here as prep for the next stage.

do {
try ResourceFileManager.shared.finalizePackageInstall(kmp, isCustom: false)
log.info("successfully parsed the lexical model in: \(kmp.sourceFolder)")
installedLexicalModel = kmp.models[0].installableLexicalModels[0]
} catch {
log.error("Error installing the lexical model: \(String(describing: error))")
}

// End of block to be spun off.
} else {
log.error("Provided package did not contain lexical models.")
}
} catch {
log.error("Error extracting the lexical model from the package: \(String(describing: error))")
}

return (installedLexicalModel, kmp)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -312,30 +312,16 @@ public class InstalledLanguagesViewController: UITableViewController, UIAlertVie
log.info("keyboardDownloadCompleted: InstalledLanguagesViewController")
Manager.shared.shouldReloadKeyboard = true

// Update keyboard version
for keyboard in keyboards {
Manager.shared.updateUserKeyboards(with: keyboard)
}

if let toolbar = navigationController?.toolbar as? ResourceDownloadStatusToolbar {
toolbar.displayStatus("Keyboard successfully downloaded!", withIndicator: false, duration: 3.0)
}
restoreNavigation()

// Add keyboard.
for keyboard in keyboards {
_ = Manager.shared.setKeyboard(keyboard)
}

navigationController?.popToRootViewController(animated: true)
}

private func lexicalModelDownloadCompleted(_ lexicalModels: [InstallableLexicalModel]) {
log.info("lexicalModelDownloadCompleted: InstalledLanguagesViewController")
// Add models.
for lexicalModel in lexicalModels {
_ = Manager.shared.registerLexicalModel(lexicalModel)
}

if let toolbar = navigationController?.toolbar as? ResourceDownloadStatusToolbar {
toolbar.displayStatus("Dictionary successfully downloaded!", withIndicator: false, duration: 3.0)
Expand Down

0 comments on commit cf3c050

Please sign in to comment.