2018-02-05 22:29:46 +01:00
|
|
|
//
|
|
|
|
// AppDelegate.swift
|
2019-04-15 22:03:05 +02:00
|
|
|
// NetNewsWire
|
2018-02-05 22:29:46 +01:00
|
|
|
//
|
2019-04-15 22:03:05 +02:00
|
|
|
// Created by Maurice Parker on 4/8/19.
|
|
|
|
// Copyright © 2019 Ranchero Software. All rights reserved.
|
2018-02-05 22:29:46 +01:00
|
|
|
//
|
|
|
|
|
|
|
|
import UIKit
|
2019-04-15 22:03:05 +02:00
|
|
|
import RSCore
|
2019-04-24 14:30:35 +02:00
|
|
|
import RSWeb
|
2019-04-15 22:03:05 +02:00
|
|
|
import Account
|
2019-04-23 14:48:22 +02:00
|
|
|
import UserNotifications
|
2019-06-19 17:26:03 +02:00
|
|
|
import BackgroundTasks
|
2019-04-26 14:44:00 +02:00
|
|
|
import os.log
|
2019-04-15 22:03:05 +02:00
|
|
|
|
|
|
|
var appDelegate: AppDelegate!
|
2018-02-05 22:29:46 +01:00
|
|
|
|
|
|
|
@UIApplicationMain
|
2019-04-15 22:03:05 +02:00
|
|
|
class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDelegate, UnreadCountProvider {
|
2019-06-19 17:26:03 +02:00
|
|
|
|
2019-06-20 01:09:42 +02:00
|
|
|
private var syncBackgroundUpdateTask = UIBackgroundTaskIdentifier.invalid
|
|
|
|
|
2019-05-20 20:51:08 +02:00
|
|
|
var syncTimer: ArticleStatusSyncTimer?
|
2019-04-24 14:30:35 +02:00
|
|
|
|
2019-05-20 20:51:08 +02:00
|
|
|
var shuttingDown = false {
|
|
|
|
didSet {
|
|
|
|
if shuttingDown {
|
|
|
|
syncTimer?.shuttingDown = shuttingDown
|
|
|
|
syncTimer?.invalidate()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2019-06-19 17:26:03 +02:00
|
|
|
|
2019-04-26 14:44:00 +02:00
|
|
|
var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "application")
|
2019-06-19 17:26:03 +02:00
|
|
|
|
2019-04-15 22:03:05 +02:00
|
|
|
var faviconDownloader: FaviconDownloader!
|
|
|
|
var imageDownloader: ImageDownloader!
|
|
|
|
var authorAvatarDownloader: AuthorAvatarDownloader!
|
|
|
|
var feedIconDownloader: FeedIconDownloader!
|
2019-06-19 17:26:03 +02:00
|
|
|
|
2019-04-15 22:03:05 +02:00
|
|
|
var unreadCount = 0 {
|
|
|
|
didSet {
|
|
|
|
if unreadCount != oldValue {
|
|
|
|
postUnreadCountDidChangeNotification()
|
2019-04-23 14:48:22 +02:00
|
|
|
UIApplication.shared.applicationIconBadgeNumber = unreadCount
|
2019-04-15 22:03:05 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
override init() {
|
|
|
|
|
|
|
|
super.init()
|
|
|
|
appDelegate = self
|
2019-09-12 17:59:26 +02:00
|
|
|
|
2019-09-21 03:33:28 +02:00
|
|
|
// Force lazy initialization of the web view provider so that it can warm up the queue of prepared web views
|
|
|
|
let _ = DetailViewControllerWebViewProvider.shared
|
|
|
|
|
2019-09-22 20:09:06 +02:00
|
|
|
let accountsURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.\(Bundle.main.bundleIdentifier!)")
|
|
|
|
let accountsFolder = accountsURL!.appendingPathComponent("Accounts").absoluteString
|
|
|
|
let accountsFolderPath = accountsFolder.suffix(from: accountsFolder.index(accountsFolder.startIndex, offsetBy: 7))
|
|
|
|
AccountManager.shared = AccountManager(accountsFolder: String(accountsFolderPath))
|
|
|
|
|
|
|
|
AppDefaults.shared = UserDefaults.init(suiteName: "group.\(Bundle.main.bundleIdentifier!)")!
|
2019-04-15 22:03:05 +02:00
|
|
|
|
2019-08-15 02:48:18 +02:00
|
|
|
registerBackgroundTasks()
|
|
|
|
|
2019-04-15 22:03:05 +02:00
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil)
|
2019-04-26 22:24:39 +02:00
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(accountRefreshDidFinish(_:)), name: .AccountRefreshDidFinish, object: nil)
|
2019-04-24 14:30:35 +02:00
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil)
|
|
|
|
|
2019-04-15 22:03:05 +02:00
|
|
|
}
|
2019-06-19 17:26:03 +02:00
|
|
|
|
2019-04-15 20:30:10 +02:00
|
|
|
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
2019-04-15 22:03:05 +02:00
|
|
|
|
|
|
|
AppDefaults.registerDefaults()
|
|
|
|
let isFirstRun = AppDefaults.isFirstRun
|
|
|
|
if isFirstRun {
|
2019-04-26 14:44:00 +02:00
|
|
|
os_log("Is first run.", log: log, type: .info)
|
2019-04-15 22:03:05 +02:00
|
|
|
}
|
|
|
|
|
2019-05-01 12:53:18 +02:00
|
|
|
let localAccount = AccountManager.shared.defaultAccount
|
2019-04-15 22:03:05 +02:00
|
|
|
DefaultFeedsImporter.importIfNeeded(isFirstRun, account: localAccount)
|
|
|
|
|
2019-09-01 23:54:07 +02:00
|
|
|
initializeDownloaders()
|
|
|
|
initializeHomeScreenQuickActions()
|
2019-06-19 17:26:03 +02:00
|
|
|
|
2019-04-15 22:03:05 +02:00
|
|
|
DispatchQueue.main.async {
|
|
|
|
self.unreadCount = AccountManager.shared.unreadCount
|
|
|
|
}
|
|
|
|
|
2019-04-27 20:54:52 +02:00
|
|
|
UNUserNotificationCenter.current().requestAuthorization(options:[.badge, .sound, .alert]) { (granted, error) in
|
2019-04-23 14:48:22 +02:00
|
|
|
if granted {
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
UIApplication.shared.registerForRemoteNotifications()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2019-05-20 20:51:08 +02:00
|
|
|
|
|
|
|
syncTimer = ArticleStatusSyncTimer()
|
2019-06-19 17:26:03 +02:00
|
|
|
|
2019-05-20 20:51:08 +02:00
|
|
|
#if DEBUG
|
2019-06-19 17:26:03 +02:00
|
|
|
syncTimer!.update()
|
2019-05-20 20:51:08 +02:00
|
|
|
#endif
|
|
|
|
|
2018-02-05 22:29:46 +01:00
|
|
|
return true
|
2019-04-15 22:03:05 +02:00
|
|
|
|
2018-02-05 22:29:46 +01:00
|
|
|
}
|
2019-06-19 17:26:03 +02:00
|
|
|
|
2019-06-28 17:28:02 +02:00
|
|
|
func applicationWillTerminate(_ application: UIApplication) {
|
|
|
|
shuttingDown = true
|
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: Notifications
|
|
|
|
|
|
|
|
@objc func unreadCountDidChange(_ note: Notification) {
|
|
|
|
if note.object is AccountManager {
|
|
|
|
unreadCount = AccountManager.shared.unreadCount
|
|
|
|
}
|
2018-02-05 22:29:46 +01:00
|
|
|
}
|
2019-06-19 17:26:03 +02:00
|
|
|
|
2019-06-28 17:28:02 +02:00
|
|
|
@objc func userDefaultsDidChange(_ note: Notification) {
|
|
|
|
scheduleBackgroundFeedRefresh()
|
|
|
|
}
|
|
|
|
|
|
|
|
@objc func accountRefreshDidFinish(_ note: Notification) {
|
|
|
|
AppDefaults.lastRefresh = Date()
|
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: - API
|
|
|
|
|
|
|
|
func prepareAccountsForBackground() {
|
2019-05-20 20:51:08 +02:00
|
|
|
syncTimer?.invalidate()
|
|
|
|
|
2019-06-19 17:26:03 +02:00
|
|
|
// Schedule background app refresh
|
|
|
|
scheduleBackgroundFeedRefresh()
|
2019-06-20 01:09:42 +02:00
|
|
|
|
|
|
|
// Sync article status
|
|
|
|
let completeProcessing = { [unowned self] in
|
|
|
|
UIApplication.shared.endBackgroundTask(self.syncBackgroundUpdateTask)
|
|
|
|
self.syncBackgroundUpdateTask = UIBackgroundTaskIdentifier.invalid
|
|
|
|
}
|
|
|
|
|
|
|
|
DispatchQueue.global(qos: .background).async {
|
|
|
|
self.syncBackgroundUpdateTask = UIApplication.shared.beginBackgroundTask {
|
|
|
|
completeProcessing()
|
|
|
|
os_log("Accounts sync processing terminated for running too long.", log: self.log, type: .info)
|
|
|
|
}
|
|
|
|
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
AccountManager.shared.syncArticleStatusAll() {
|
|
|
|
completeProcessing()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-02-05 22:29:46 +01:00
|
|
|
}
|
2019-06-19 17:26:03 +02:00
|
|
|
|
2019-06-28 17:28:02 +02:00
|
|
|
func prepareAccountsForForeground() {
|
2019-04-26 22:24:39 +02:00
|
|
|
if let lastRefresh = AppDefaults.lastRefresh {
|
|
|
|
if Date() > lastRefresh.addingTimeInterval(15 * 60) {
|
2019-06-27 21:21:07 +02:00
|
|
|
AccountManager.shared.refreshAll(errorHandler: ErrorHandler.log)
|
2019-06-28 17:28:02 +02:00
|
|
|
} else {
|
|
|
|
AccountManager.shared.syncArticleStatusAll()
|
|
|
|
syncTimer?.update()
|
2019-04-26 22:24:39 +02:00
|
|
|
}
|
|
|
|
} else {
|
2019-06-27 21:21:07 +02:00
|
|
|
AccountManager.shared.refreshAll(errorHandler: ErrorHandler.log)
|
2019-04-26 22:24:39 +02:00
|
|
|
}
|
2018-02-05 22:29:46 +01:00
|
|
|
}
|
2019-06-19 17:26:03 +02:00
|
|
|
|
2019-04-15 22:03:05 +02:00
|
|
|
func logMessage(_ message: String, type: LogItem.ItemType) {
|
2019-04-26 14:44:00 +02:00
|
|
|
print("logMessage: \(message) - \(type)")
|
2019-04-15 22:03:05 +02:00
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
func logDebugMessage(_ message: String) {
|
|
|
|
logMessage(message, type: .debug)
|
|
|
|
}
|
|
|
|
|
2018-02-05 22:29:46 +01:00
|
|
|
}
|
|
|
|
|
2019-09-01 23:54:07 +02:00
|
|
|
// MARK: App Initialization
|
|
|
|
|
|
|
|
private extension AppDelegate {
|
|
|
|
|
|
|
|
private func initializeDownloaders() {
|
|
|
|
let tempDir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
|
|
|
|
let faviconsFolderURL = tempDir.appendingPathComponent("Favicons")
|
2019-09-17 00:09:49 +02:00
|
|
|
let imagesFolderURL = tempDir.appendingPathComponent("Images")
|
|
|
|
let homePageToIconURL = tempDir.appendingPathComponent("HomePageToIconURLCache.plist")
|
|
|
|
|
|
|
|
// If the image disk cache hasn't been flushed for 3 days and the network is available, delete it
|
|
|
|
if let flushDate = AppDefaults.lastImageCacheFlushDate, flushDate.addingTimeInterval(3600*24*3) < Date() {
|
|
|
|
if let reachability = try? Reachability(hostname: "apple.com") {
|
|
|
|
if reachability.connection != .unavailable {
|
|
|
|
for tempItem in [faviconsFolderURL, imagesFolderURL, homePageToIconURL] {
|
|
|
|
do {
|
|
|
|
os_log(.info, log: self.log, "Removing cache file: %@", tempItem.absoluteString)
|
|
|
|
try FileManager.default.removeItem(at: tempItem)
|
|
|
|
} catch {
|
|
|
|
os_log(.error, log: self.log, "Could not delete cache file: %@", error.localizedDescription)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
AppDefaults.lastImageCacheFlushDate = Date()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-09-01 23:54:07 +02:00
|
|
|
try! FileManager.default.createDirectory(at: faviconsFolderURL, withIntermediateDirectories: true, attributes: nil)
|
|
|
|
let faviconsFolder = faviconsFolderURL.absoluteString
|
|
|
|
let faviconsFolderPath = faviconsFolder.suffix(from: faviconsFolder.index(faviconsFolder.startIndex, offsetBy: 7))
|
|
|
|
faviconDownloader = FaviconDownloader(folder: String(faviconsFolderPath))
|
|
|
|
|
|
|
|
let imagesFolder = imagesFolderURL.absoluteString
|
|
|
|
let imagesFolderPath = imagesFolder.suffix(from: imagesFolder.index(imagesFolder.startIndex, offsetBy: 7))
|
|
|
|
try! FileManager.default.createDirectory(at: imagesFolderURL, withIntermediateDirectories: true, attributes: nil)
|
|
|
|
imageDownloader = ImageDownloader(folder: String(imagesFolderPath))
|
|
|
|
|
|
|
|
authorAvatarDownloader = AuthorAvatarDownloader(imageDownloader: imageDownloader)
|
|
|
|
|
|
|
|
let tempFolder = tempDir.absoluteString
|
|
|
|
let tempFolderPath = tempFolder.suffix(from: tempFolder.index(tempFolder.startIndex, offsetBy: 7))
|
|
|
|
feedIconDownloader = FeedIconDownloader(imageDownloader: imageDownloader, folder: String(tempFolderPath))
|
|
|
|
}
|
|
|
|
|
|
|
|
private func initializeHomeScreenQuickActions() {
|
|
|
|
let unreadTitle = NSLocalizedString("First Unread", comment: "First Unread")
|
|
|
|
let unreadIcon = UIApplicationShortcutIcon(systemImageName: "arrow.down.circle")
|
|
|
|
let unreadItem = UIApplicationShortcutItem(type: "com.ranchero.NetNewsWire.FirstUnread", localizedTitle: unreadTitle, localizedSubtitle: nil, icon: unreadIcon, userInfo: nil)
|
|
|
|
|
|
|
|
let searchTitle = NSLocalizedString("Search", comment: "Search")
|
|
|
|
let searchIcon = UIApplicationShortcutIcon(systemImageName: "magnifyingglass")
|
|
|
|
let searchItem = UIApplicationShortcutItem(type: "com.ranchero.NetNewsWire.ShowSearch", localizedTitle: searchTitle, localizedSubtitle: nil, icon: searchIcon, userInfo: nil)
|
|
|
|
|
2019-09-02 22:45:09 +02:00
|
|
|
let addTitle = NSLocalizedString("Add Feed", comment: "Add Feed")
|
2019-09-02 22:14:26 +02:00
|
|
|
let addIcon = UIApplicationShortcutIcon(systemImageName: "plus")
|
|
|
|
let addItem = UIApplicationShortcutItem(type: "com.ranchero.NetNewsWire.ShowAdd", localizedTitle: addTitle, localizedSubtitle: nil, icon: addIcon, userInfo: nil)
|
|
|
|
|
2019-09-02 23:05:55 +02:00
|
|
|
UIApplication.shared.shortcutItems = [addItem, searchItem, unreadItem]
|
2019-09-01 23:54:07 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: Background Tasks
|
2019-06-19 17:26:03 +02:00
|
|
|
|
2019-04-27 20:54:52 +02:00
|
|
|
private extension AppDelegate {
|
2019-06-28 17:28:02 +02:00
|
|
|
|
2019-06-19 17:26:03 +02:00
|
|
|
/// Register all background tasks.
|
|
|
|
func registerBackgroundTasks() {
|
|
|
|
// Register background feed refresh.
|
|
|
|
BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.ranchero.NetNewsWire.FeedRefresh", using: nil) { (task) in
|
|
|
|
self.performBackgroundFeedRefresh(with: task as! BGAppRefreshTask)
|
2019-04-29 16:29:57 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-06-19 17:26:03 +02:00
|
|
|
/// Schedules a background app refresh based on `AppDefaults.refreshInterval`.
|
|
|
|
func scheduleBackgroundFeedRefresh() {
|
|
|
|
let request = BGAppRefreshTaskRequest(identifier: "com.ranchero.NetNewsWire.FeedRefresh")
|
|
|
|
request.earliestBeginDate = Date(timeIntervalSinceNow: AppDefaults.refreshInterval.inSeconds())
|
|
|
|
do {
|
|
|
|
try BGTaskScheduler.shared.submit(request)
|
|
|
|
} catch {
|
2019-06-20 01:09:42 +02:00
|
|
|
os_log(.error, log: self.log, "Could not schedule app refresh: %@", error.localizedDescription)
|
2019-06-19 17:26:03 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Performs background feed refresh.
|
|
|
|
/// - Parameter task: `BGAppRefreshTask`
|
|
|
|
/// - Warning: As of Xcode 11 beta 2, when triggered from the debugger this doesn't work.
|
|
|
|
func performBackgroundFeedRefresh(with task: BGAppRefreshTask) {
|
|
|
|
|
|
|
|
scheduleBackgroundFeedRefresh() // schedule next refresh
|
|
|
|
|
|
|
|
var startingUnreadCount = 0
|
|
|
|
|
|
|
|
DispatchQueue.global(qos: .background).async { [unowned self] in
|
|
|
|
|
|
|
|
os_log("Woken to perform account refresh.", log: self.log, type: .info)
|
|
|
|
|
|
|
|
os_log("Getting unread count.", log: self.log, type: .info)
|
|
|
|
while(!AccountManager.shared.isUnreadCountsInitialized) {
|
|
|
|
os_log("Waiting for unread counts to be initialized...", log: self.log, type: .info)
|
|
|
|
sleep(1)
|
|
|
|
}
|
|
|
|
os_log(.info, log: self.log, "Got unread count: %i", self.unreadCount)
|
|
|
|
startingUnreadCount = self.unreadCount
|
|
|
|
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
AccountManager.shared.refreshAll(errorHandler: ErrorHandler.log)
|
|
|
|
}
|
|
|
|
os_log("Accounts requested to begin refresh.", log: self.log, type: .info)
|
|
|
|
|
|
|
|
sleep(1)
|
|
|
|
while (!AccountManager.shared.combinedRefreshProgress.isComplete) {
|
|
|
|
os_log("Waiting for account refresh processing to complete...", log: self.log, type: .info)
|
|
|
|
sleep(1)
|
|
|
|
}
|
|
|
|
|
|
|
|
if startingUnreadCount < self.unreadCount {
|
|
|
|
os_log("Updating unread count badge, posting notification.", log: self.log, type: .info)
|
|
|
|
self.sendReceivedArticlesUserNotification(newArticleCount: self.unreadCount - startingUnreadCount)
|
|
|
|
task.setTaskCompleted(success: true)
|
|
|
|
} else {
|
|
|
|
os_log("Account refresh operation completed.", log: self.log, type: .info)
|
|
|
|
task.setTaskCompleted(success: true)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// set expiration handler
|
|
|
|
task.expirationHandler = {
|
|
|
|
os_log("Accounts refresh processing terminated for running too long.", log: self.log, type: .info)
|
|
|
|
task.setTaskCompleted(success: false)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
2019-04-27 20:54:52 +02:00
|
|
|
|
2019-06-19 17:26:03 +02:00
|
|
|
private extension AppDelegate {
|
|
|
|
|
|
|
|
func sendReceivedArticlesUserNotification(newArticleCount: Int) {
|
|
|
|
|
2019-04-27 20:54:52 +02:00
|
|
|
let content = UNMutableNotificationContent()
|
|
|
|
content.title = NSLocalizedString("Article Download", comment: "New Articles")
|
|
|
|
|
|
|
|
let body: String = {
|
|
|
|
if newArticleCount == 1 {
|
|
|
|
return NSLocalizedString("You have downloaded 1 new article.", comment: "Article Downloaded")
|
|
|
|
} else {
|
|
|
|
let formatString = NSLocalizedString("You have downloaded %d new articles.", comment: "Articles Downloaded")
|
|
|
|
return NSString.localizedStringWithFormat(formatString as NSString, newArticleCount) as String
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
content.body = body
|
|
|
|
content.sound = UNNotificationSound.default
|
|
|
|
|
|
|
|
let request = UNNotificationRequest.init(identifier: "NewArticlesReceived", content: content, trigger: nil)
|
|
|
|
UNUserNotificationCenter.current().add(request)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|