Cancel network activity when told to shutdown by the OS. Issue #1232

This commit is contained in:
Maurice Parker 2019-11-04 20:24:21 -06:00
parent 219e5751a1
commit c6e3ed6692
14 changed files with 232 additions and 71 deletions

View File

@ -341,10 +341,20 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
self.delegate.refreshAll(for: self, completion: completion)
}
public func syncArticleStatus(completion: (() -> Void)? = nil) {
delegate.sendArticleStatus(for: self) { [unowned self] in
self.delegate.refreshArticleStatus(for: self) {
completion?()
public func syncArticleStatus(completion: ((Result<Void, Error>) -> Void)? = nil) {
delegate.sendArticleStatus(for: self) { [unowned self] result in
switch result {
case .success:
self.delegate.refreshArticleStatus(for: self) { result in
switch result {
case .success:
completion?(.success(()))
case .failure(let error):
completion?(.failure(error))
}
}
case .failure(let error):
completion?(.failure(error))
}
}
}
@ -369,6 +379,11 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
}
public func suspend() {
delegate.cancelAll(for: self)
save()
}
public func save() {
metadataFile.save()
feedMetadataFile.save()

View File

@ -22,9 +22,10 @@ protocol AccountDelegate {
var refreshProgress: DownloadProgress { get }
func cancelAll(for account: Account)
func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void)
func sendArticleStatus(for account: Account, completion: @escaping (() -> Void))
func refreshArticleStatus(for account: Account, completion: @escaping (() -> Void))
func sendArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void))
func refreshArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void))
func importOPML(for account:Account, opmlFile: URL, completion: @escaping (Result<Void, Error>) -> Void)

View File

@ -160,6 +160,10 @@ public final class AccountManager: UnreadCountProvider {
return accountsDictionary[accountID]
}
public func suspendAll() {
accounts.forEach { $0.suspend() }
}
public func refreshAll(errorHandler: @escaping (Error) -> Void, completion: (() ->Void)? = nil) {
let group = DispatchGroup()
@ -187,7 +191,7 @@ public final class AccountManager: UnreadCountProvider {
activeAccounts.forEach {
group.enter()
$0.syncArticleStatus() {
$0.syncArticleStatus() { _ in
group.leave()
}
}

View File

@ -32,6 +32,8 @@ final class TestTransport: Transport {
return HTTPURLResponse(url: url, statusCode: statusCode, httpVersion: "HTTP/1.1", headerFields: nil)!
}
func cancelAll() { }
func send(request: URLRequest, completion: @escaping (Result<(HTTPURLResponse, Data?), Error>) -> Void) {
guard let url = request.url, let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {

View File

@ -42,6 +42,10 @@ final class FeedbinAPICaller: NSObject {
self.transport = transport
}
func cancelAll() {
transport.cancelAll()
}
func validateCredentials(completion: @escaping (Result<Credentials?, Error>) -> Void) {
let callURL = feedbinBaseURL.appendingPathComponent("authentication.json")

View File

@ -15,6 +15,7 @@ import os.log
public enum FeedbinAccountDelegateError: String, Error {
case invalidParameter = "There was an invalid parameter passed."
case unknown = "An unknown error occurred."
}
final class FeedbinAccountDelegate: AccountDelegate {
@ -72,6 +73,10 @@ final class FeedbinAccountDelegate: AccountDelegate {
var refreshProgress = DownloadProgress(numberOfTasks: 0)
func cancelAll(for account: Account) {
caller.cancelAll()
}
func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
retrieveCredentialsIfNecessary(account)
@ -80,16 +85,16 @@ final class FeedbinAccountDelegate: AccountDelegate {
refreshAccount(account) { result in
switch result {
case .success():
self.sendArticleStatus(for: account) {
self.refreshArticleStatus(for: account) {
self.refreshArticles(account) {
self.refreshMissingArticles(account) {
self.refreshProgress.clear()
DispatchQueue.main.async {
completion(.success(()))
}
}
self.refreshArticlesAndStatuses(account) { result in
switch result {
case .success():
completion(.success(()))
case .failure(let error):
DispatchQueue.main.async {
self.refreshProgress.clear()
let wrappedError = AccountError.wrappedError(error: error, account: account)
completion(.failure(wrappedError))
}
}
}
@ -106,7 +111,7 @@ final class FeedbinAccountDelegate: AccountDelegate {
}
func sendArticleStatus(for account: Account, completion: @escaping (() -> Void)) {
func sendArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
retrieveCredentialsIfNecessary(account)
os_log(.debug, log: log, "Sending article statuses...")
@ -118,41 +123,59 @@ final class FeedbinAccountDelegate: AccountDelegate {
let deleteStarredStatuses = syncStatuses.filter { $0.key == ArticleStatus.Key.starred && $0.flag == false }
let group = DispatchGroup()
var errorOccurred = false
group.enter()
sendArticleStatuses(createUnreadStatuses, apiCall: caller.createUnreadEntries) {
sendArticleStatuses(createUnreadStatuses, apiCall: caller.createUnreadEntries) { result in
group.leave()
if case .failure = result {
errorOccurred = true
}
}
group.enter()
sendArticleStatuses(deleteUnreadStatuses, apiCall: caller.deleteUnreadEntries) {
sendArticleStatuses(deleteUnreadStatuses, apiCall: caller.deleteUnreadEntries) { result in
group.leave()
if case .failure = result {
errorOccurred = true
}
}
group.enter()
sendArticleStatuses(createStarredStatuses, apiCall: caller.createStarredEntries) {
sendArticleStatuses(createStarredStatuses, apiCall: caller.createStarredEntries) { result in
group.leave()
if case .failure = result {
errorOccurred = true
}
}
group.enter()
sendArticleStatuses(deleteStarredStatuses, apiCall: caller.deleteStarredEntries) {
sendArticleStatuses(deleteStarredStatuses, apiCall: caller.deleteStarredEntries) { result in
group.leave()
if case .failure = result {
errorOccurred = true
}
}
group.notify(queue: DispatchQueue.main) {
os_log(.debug, log: self.log, "Done sending article statuses.")
completion()
if errorOccurred {
completion(.failure(FeedbinAccountDelegateError.unknown))
} else {
completion(.success(()))
}
}
}
func refreshArticleStatus(for account: Account, completion: @escaping (() -> Void)) {
func refreshArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
retrieveCredentialsIfNecessary(account)
os_log(.debug, log: log, "Refreshing article statuses...")
let group = DispatchGroup()
var errorOccurred = false
group.enter()
caller.retrieveUnreadEntries() { result in
switch result {
@ -160,6 +183,7 @@ final class FeedbinAccountDelegate: AccountDelegate {
self.syncArticleReadState(account: account, articleIDs: articleIDs)
group.leave()
case .failure(let error):
errorOccurred = true
os_log(.info, log: self.log, "Retrieving unread entries failed: %@.", error.localizedDescription)
group.leave()
}
@ -173,6 +197,7 @@ final class FeedbinAccountDelegate: AccountDelegate {
self.syncArticleStarredState(account: account, articleIDs: articleIDs)
group.leave()
case .failure(let error):
errorOccurred = true
os_log(.info, log: self.log, "Retrieving starred entries failed: %@.", error.localizedDescription)
group.leave()
}
@ -181,7 +206,11 @@ final class FeedbinAccountDelegate: AccountDelegate {
group.notify(queue: DispatchQueue.main) {
os_log(.debug, log: self.log, "Done refreshing article statuses.")
completion()
if errorOccurred {
completion(.failure(FeedbinAccountDelegateError.unknown))
} else {
completion(.success(()))
}
}
}
@ -515,7 +544,7 @@ final class FeedbinAccountDelegate: AccountDelegate {
database.insertStatuses(syncStatuses)
if database.selectPendingCount() > 100 {
sendArticleStatus(for: account) {}
sendArticleStatus(for: account) { _ in }
}
return account.update(articles, statusKey: statusKey, flag: flag)
@ -639,6 +668,49 @@ private extension FeedbinAccountDelegate {
}
func refreshArticlesAndStatuses(_ account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
self.sendArticleStatus(for: account) { result in
switch result {
case .success:
self.refreshArticleStatus(for: account) { result in
switch result {
case .success:
self.refreshArticles(account) { result in
switch result {
case .success:
self.refreshMissingArticles(account) { result in
switch result {
case .success:
DispatchQueue.main.async {
self.refreshProgress.clear()
completion(.success(()))
}
case .failure(let error):
completion(.failure(error))
}
}
case .failure(let error):
completion(.failure(error))
}
}
case .failure(let error):
completion(.failure(error))
}
}
case .failure(let error):
completion(.failure(error))
}
}
}
// This function can be deleted if Feedbin updates their taggings.json service to
// show a change when a tag is renamed.
func forceExpireFolderFeedRelationship(_ account: Account, _ tags: [FeedbinTag]?) {
@ -843,14 +915,15 @@ private extension FeedbinAccountDelegate {
func sendArticleStatuses(_ statuses: [SyncStatus],
apiCall: ([Int], @escaping (Result<Void, Error>) -> Void) -> Void,
completion: @escaping (() -> Void)) {
completion: @escaping ((Result<Void, Error>) -> Void)) {
guard !statuses.isEmpty else {
completion()
completion(.success(()))
return
}
let group = DispatchGroup()
var errorOccurred = false
let articleIDs = statuses.compactMap { Int($0.articleID) }
let articleIDGroups = articleIDs.chunked(into: 1000)
@ -863,6 +936,7 @@ private extension FeedbinAccountDelegate {
self.database.deleteSelectedForProcessing(articleIDGroup.map { String($0) } )
group.leave()
case .failure(let error):
errorOccurred = true
os_log(.error, log: self.log, "Article status sync call failed: %@.", error.localizedDescription)
self.database.resetSelectedForProcessing(articleIDGroup.map { String($0) } )
group.leave()
@ -872,7 +946,11 @@ private extension FeedbinAccountDelegate {
}
group.notify(queue: DispatchQueue.main) {
completion()
if errorOccurred {
completion(.failure(FeedbinAccountDelegateError.unknown))
} else {
completion(.success(()))
}
}
}
@ -973,15 +1051,38 @@ private extension FeedbinAccountDelegate {
case .success(let (entries, page)):
self.processEntries(account: account, entries: entries) {
self.refreshArticleStatus(for: account) {
self.refreshArticles(account, page: page, updateFetchDate: nil) {
self.refreshProgress.completeTask()
self.refreshMissingArticles(account) {
self.refreshProgress.completeTask()
DispatchQueue.main.async {
completion(.success(feed))
self.refreshArticleStatus(for: account) { result in
switch result {
case .success:
self.refreshArticles(account, page: page, updateFetchDate: nil) { result in
switch result {
case .success:
self.refreshProgress.completeTask()
self.refreshMissingArticles(account) { result in
switch result {
case .success:
self.refreshProgress.completeTask()
DispatchQueue.main.async {
completion(.success(feed))
}
case .failure(let error):
completion(.failure(error))
}
}
case .failure(let error):
completion(.failure(error))
}
}
case .failure(let error):
completion(.failure(error))
}
}
}
@ -994,7 +1095,7 @@ private extension FeedbinAccountDelegate {
}
func refreshArticles(_ account: Account, completion: @escaping (() -> Void)) {
func refreshArticles(_ account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
os_log(.debug, log: log, "Refreshing articles...")
@ -1010,25 +1111,30 @@ private extension FeedbinAccountDelegate {
self.processEntries(account: account, entries: entries) {
self.refreshProgress.completeTask()
self.refreshArticles(account, page: page, updateFetchDate: updateFetchDate) {
self.refreshArticles(account, page: page, updateFetchDate: updateFetchDate) { result in
os_log(.debug, log: self.log, "Done refreshing articles.")
completion()
switch result {
case .success:
completion(.success(()))
case .failure(let error):
completion(.failure(error))
}
}
}
case .failure(let error):
os_log(.error, log: self.log, "Refresh articles failed: %@.", error.localizedDescription)
completion()
completion(.failure(error))
}
}
}
func refreshMissingArticles(_ account: Account, completion: @escaping (() -> Void)) {
func refreshMissingArticles(_ account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
os_log(.debug, log: log, "Refreshing missing articles...")
let group = DispatchGroup()
var errorOccurred = false
let fetchedArticleIDs = account.fetchArticleIDsForStatusesWithoutArticles()
let articleIDs = Array(fetchedArticleIDs)
@ -1046,6 +1152,7 @@ private extension FeedbinAccountDelegate {
}
case .failure(let error):
errorOccurred = true
os_log(.error, log: self.log, "Refresh missing articles failed: %@.", error.localizedDescription)
group.leave()
}
@ -1055,16 +1162,20 @@ private extension FeedbinAccountDelegate {
group.notify(queue: DispatchQueue.main) {
self.refreshProgress.completeTask()
os_log(.debug, log: self.log, "Done refreshing missing articles.")
completion()
if errorOccurred {
completion(.failure(FeedbinAccountDelegateError.unknown))
} else {
completion(.success(()))
}
}
}
func refreshArticles(_ account: Account, page: String?, updateFetchDate: Date?, completion: @escaping (() -> Void)) {
func refreshArticles(_ account: Account, page: String?, updateFetchDate: Date?, completion: @escaping ((Result<Void, Error>) -> Void)) {
guard let page = page else {
if let lastArticleFetch = updateFetchDate {
self.accountMetadata?.lastArticleFetch = lastArticleFetch
}
completion()
completion(.success(()))
return
}
@ -1079,8 +1190,7 @@ private extension FeedbinAccountDelegate {
}
case .failure(let error):
os_log(.error, log: self.log, "Refresh articles for additional pages failed: %@.", error.localizedDescription)
completion()
completion(.failure(error))
}
}
}

View File

@ -53,6 +53,10 @@ final class FeedlyAPICaller {
return baseUrlComponents.host
}
func cancelAll() {
transport.cancelAll()
}
func importOpml(_ opmlData: Data, completionHandler: @escaping (Result<Void, Error>) -> ()) {
guard let accessToken = credentials?.secret else {
return DispatchQueue.main.async {

View File

@ -78,6 +78,10 @@ final class FeedlyAccountDelegate: AccountDelegate {
// MARK: Account API
func cancelAll(for account: Account) {
// TODO: Implement me please
}
func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
assert(Thread.isMainThread)
@ -113,12 +117,12 @@ final class FeedlyAccountDelegate: AccountDelegate {
OperationQueue.main.addOperation(operation)
}
func sendArticleStatus(for account: Account, completion: @escaping (() -> Void)) {
func sendArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
// Ensure remote articles have the same status as they do locally.
let send = FeedlySendArticleStatusesOperation(database: database, service: caller, log: log)
send.completionBlock = {
DispatchQueue.main.async {
completion()
completion(.success(()))
}
}
OperationQueue.main.addOperation(send)
@ -134,9 +138,9 @@ final class FeedlyAccountDelegate: AccountDelegate {
///
/// - Parameter account: The account whose articles have a remote status.
/// - Parameter completion: Call on the main queue.
func refreshArticleStatus(for account: Account, completion: @escaping (() -> Void)) {
func refreshArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
guard let credentials = credentials else {
return completion()
return completion(.success(()))
}
let group = DispatchGroup()
@ -157,7 +161,7 @@ final class FeedlyAccountDelegate: AccountDelegate {
}
group.notify(queue: .main) {
completion()
completion(.success(()))
}
OperationQueue.main.addOperations([getUnread, getStarred], waitUntilFinished: false)
@ -448,7 +452,7 @@ final class FeedlyAccountDelegate: AccountDelegate {
os_log(.debug, log: log, "Marking %@ as %@.", articles.map { $0.title }, syncStatuses)
if database.selectPendingCount() > 100 {
sendArticleStatus(for: account) { }
sendArticleStatus(for: account) { _ in }
}
return account.update(articles, statusKey: statusKey, flag: flag)

View File

@ -31,18 +31,22 @@ final class LocalAccountDelegate: AccountDelegate {
return refresher.progress
}
func cancelAll(for account: Account) {
// TODO: implement me
}
func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
refresher.refreshFeeds(account.flattenedFeeds()) {
completion(.success(()))
}
}
func sendArticleStatus(for account: Account, completion: @escaping (() -> Void)) {
completion()
func sendArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
completion(.success(()))
}
func refreshArticleStatus(for account: Account, completion: @escaping (() -> Void)) {
completion()
func refreshArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
completion(.success(()))
}
func importOPML(for account:Account, opmlFile: URL, completion: @escaping (Result<Void, Error>) -> Void) {

View File

@ -19,7 +19,7 @@ public enum ReaderAPIAccountDelegateError: String, Error {
}
final class ReaderAPIAccountDelegate: AccountDelegate {
private let database: SyncDatabase
private let caller: ReaderAPICaller
@ -79,6 +79,10 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
var refreshProgress = DownloadProgress(numberOfTasks: 0)
func cancelAll(for account: Account) {
caller.cancelAll()
}
func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
refreshProgress.addToNumberOfTasksAndRemaining(6)
@ -87,8 +91,8 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
switch result {
case .success():
self.sendArticleStatus(for: account) {
self.refreshArticleStatus(for: account) {
self.sendArticleStatus(for: account) { _ in
self.refreshArticleStatus(for: account) { _ in
self.refreshArticles(account) {
self.refreshMissingArticles(account) {
self.refreshProgress.clear()
@ -112,7 +116,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
}
func sendArticleStatus(for account: Account, completion: @escaping (() -> Void)) {
func sendArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
os_log(.debug, log: log, "Sending article statuses...")
@ -146,12 +150,12 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
group.notify(queue: DispatchQueue.main) {
os_log(.debug, log: self.log, "Done sending article statuses.")
completion()
completion(.success(()))
}
}
func refreshArticleStatus(for account: Account, completion: @escaping (() -> Void)) {
func refreshArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
os_log(.debug, log: log, "Refreshing article statuses...")
@ -185,7 +189,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
group.notify(queue: DispatchQueue.main) {
os_log(.debug, log: self.log, "Done refreshing article statuses.")
completion()
completion(.success(()))
}
}
@ -403,7 +407,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
database.insertStatuses(syncStatuses)
if database.selectPendingCount() > 100 {
sendArticleStatus(for: account) {}
sendArticleStatus(for: account) { _ in }
}
return account.update(articles, statusKey: statusKey, flag: flag)
@ -755,7 +759,7 @@ private extension ReaderAPIAccountDelegate {
case .success(let (entries, page)):
self.processEntries(account: account, entries: entries) {
self.refreshArticleStatus(for: account) {
self.refreshArticleStatus(for: account) { _ in
self.refreshArticles(account, page: page) {
self.refreshMissingArticles(account) {
DispatchQueue.main.async {

View File

@ -78,6 +78,10 @@ final class ReaderAPICaller: NSObject {
self.transport = transport
}
func cancelAll() {
transport.cancelAll()
}
func validateCredentials(endpoint: URL, completion: @escaping (Result<Credentials?, Error>) -> Void) {
guard let credentials = credentials else {
completion(.failure(CredentialsError.incompleteCredentials))

View File

@ -109,6 +109,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
func applicationWillTerminate(_ application: UIApplication) {
shuttingDown = true
AccountManager.shared.suspendAll()
}
// MARK: Notifications
@ -243,7 +244,7 @@ private extension AppDelegate {
func waitForProgressToFinish() {
let completeProcessing = { [unowned self] in
AccountManager.shared.saveAll()
AccountManager.shared.suspendAll()
UIApplication.shared.endBackgroundTask(self.waitBackgroundUpdateTask)
self.waitBackgroundUpdateTask = UIBackgroundTaskIdentifier.invalid
}

View File

@ -16,7 +16,11 @@ struct ErrorHandler {
public static func present(_ viewController: UIViewController) -> (Error) -> () {
return { [weak viewController] error in
viewController?.presentError(error)
if UIApplication.shared.applicationState == .active {
viewController?.presentError(error)
} else {
ErrorHandler.log(error)
}
}
}

@ -1 +1 @@
Subproject commit 1ea5f5ccfc3646ffdf2891abbc5ea63e3d449def
Subproject commit 262a220230ef393edb7c003132b6cdc16a17cb5e