/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import Foundation import WebKit private let ReadabilityServiceSharedInstance = ReadabilityService() private let ReadabilityTaskDefaultTimeout = 15 private let ReadabilityServiceDefaultConcurrency = 1 enum ReadabilityOperationResult { case success(ReadabilityResult) case error(NSError) case timeout } class ReadabilityOperation: Operation, WKNavigationDelegate, ReadabilityTabHelperDelegate { var url: URL var semaphore: DispatchSemaphore var result: ReadabilityOperationResult? var tab: Tab! var readerModeCache: ReaderModeCache init(url: URL, readerModeCache: ReaderModeCache) { self.url = url self.semaphore = DispatchSemaphore(value: 0) self.readerModeCache = readerModeCache } override func main() { if self.isCancelled { return } // Setup a tab, attach a Readability helper. Kick all this off on the main thread since UIKit // and WebKit are not safe from other threads. DispatchQueue.main.async(execute: { () -> Void in let configuration = WKWebViewConfiguration() self.tab = Tab(configuration: configuration) self.tab.createWebview() self.tab.navigationDelegate = self if let readabilityTabHelper = ReadabilityTabHelper(tab: self.tab) { readabilityTabHelper.delegate = self self.tab.addContentScript(readabilityTabHelper, name: ReadabilityTabHelper.name()) } // Load the page in the webview. This either fails with a navigation error, or we get a readability // callback. Or it takes too long, in which case the semaphore times out. self.tab.loadRequest(URLRequest(url: self.url)) }) let timeout = DispatchTime.now() + Double(Int64(Double(16) * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC) if semaphore.wait(timeout: timeout) == .timedOut { result = ReadabilityOperationResult.timeout } // Maybe this is where we should store stuff in the cache / run a callback? if let result = self.result { switch result { case .timeout: // Don't do anything on timeout break case .success(let readabilityResult): do { try readerModeCache.put(url, readabilityResult) } catch let error as NSError { print("Failed to store readability results in the cache: \(error.localizedDescription)") // TODO Fail } case .error(_): // TODO Not entitely sure what to do on error. Needs UX discussion and followup bug. break } } } func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { result = ReadabilityOperationResult.error(error as NSError) semaphore.signal() } func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { result = ReadabilityOperationResult.error(error as NSError) semaphore.signal() } func readabilityTabHelper(_ readabilityTabHelper: ReadabilityTabHelper, didFinishWithReadabilityResult readabilityResult: ReadabilityResult) { result = ReadabilityOperationResult.success(readabilityResult) semaphore.signal() } } class ReadabilityService { class var sharedInstance: ReadabilityService { return ReadabilityServiceSharedInstance } var queue: OperationQueue init() { queue = OperationQueue() queue.maxConcurrentOperationCount = ReadabilityServiceDefaultConcurrency } func process(_ url: URL, cache: ReaderModeCache) { queue.addOperation(ReadabilityOperation(url: url, readerModeCache: cache)) } }