diff --git a/baRSS.xcodeproj/project.pbxproj b/baRSS.xcodeproj/project.pbxproj index 6f5bac5..06bd2d3 100644 --- a/baRSS.xcodeproj/project.pbxproj +++ b/baRSS.xcodeproj/project.pbxproj @@ -29,8 +29,9 @@ 54A07A82220E723D00082C51 /* MapUnreadTotal.m in Sources */ = {isa = PBXBuildFile; fileRef = 54A07A81220E723D00082C51 /* MapUnreadTotal.m */; }; 54A2D63922EF81A4007C61F3 /* QLOPML.qlgenerator in CopyFiles */ = {isa = PBXBuildFile; fileRef = 54A2D63822EF8193007C61F3 /* QLOPML.qlgenerator */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 54ACC28C21061B3C0020715F /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 54ACC28B21061B3C0020715F /* main.m */; }; - 54ACC29521061E270020715F /* FeedDownload.m in Sources */ = {isa = PBXBuildFile; fileRef = 54ACC29421061E270020715F /* FeedDownload.m */; }; + 54ACC29521061E270020715F /* UpdateScheduler.m in Sources */ = {isa = PBXBuildFile; fileRef = 54ACC29421061E270020715F /* UpdateScheduler.m */; }; 54ACC29821061FBA0020715F /* Preferences.m in Sources */ = {isa = PBXBuildFile; fileRef = 54ACC29721061FBA0020715F /* Preferences.m */; }; + 54AD4E0023005297000AE386 /* WebFeed.m in Sources */ = {isa = PBXBuildFile; fileRef = 54AD4DFF23005297000AE386 /* WebFeed.m */; }; 54B51704226DC339006C1B29 /* ModalFeedEditView.m in Sources */ = {isa = PBXBuildFile; fileRef = 54B51703226DC339006C1B29 /* ModalFeedEditView.m */; }; 54B517072270E990006C1B29 /* NSView+Ext.m in Sources */ = {isa = PBXBuildFile; fileRef = 54B517062270E92A006C1B29 /* NSView+Ext.m */; }; 54B749DA2204A85C0022CC6D /* BarStatusItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 54B749D92204A85C0022CC6D /* BarStatusItem.m */; }; @@ -135,10 +136,12 @@ 54ACC28321061B3B0020715F /* DBv1.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = DBv1.xcdatamodel; sourceTree = ""; }; 54ACC28A21061B3C0020715F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 54ACC28B21061B3C0020715F /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; - 54ACC29321061E270020715F /* FeedDownload.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FeedDownload.h; sourceTree = ""; }; - 54ACC29421061E270020715F /* FeedDownload.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FeedDownload.m; sourceTree = ""; }; + 54ACC29321061E270020715F /* UpdateScheduler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UpdateScheduler.h; sourceTree = ""; }; + 54ACC29421061E270020715F /* UpdateScheduler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UpdateScheduler.m; sourceTree = ""; }; 54ACC29621061FBA0020715F /* Preferences.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Preferences.h; sourceTree = ""; }; 54ACC29721061FBA0020715F /* Preferences.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Preferences.m; sourceTree = ""; }; + 54AD4DFE23005297000AE386 /* WebFeed.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WebFeed.h; sourceTree = ""; }; + 54AD4DFF23005297000AE386 /* WebFeed.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = WebFeed.m; sourceTree = ""; }; 54B51702226DC339006C1B29 /* ModalFeedEditView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ModalFeedEditView.h; sourceTree = ""; }; 54B51703226DC339006C1B29 /* ModalFeedEditView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ModalFeedEditView.m; sourceTree = ""; }; 54B517052270E8C6006C1B29 /* NSView+Ext.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSView+Ext.h"; sourceTree = ""; }; @@ -201,8 +204,6 @@ children = ( 54209E922117325100F3B5EF /* DrawImage.h */, 54209E932117325100F3B5EF /* DrawImage.m */, - 54ACC29321061E270020715F /* FeedDownload.h */, - 54ACC29421061E270020715F /* FeedDownload.m */, 54BB048721FD2AB500C303A5 /* NSDate+Ext.h */, 54BB048821FD2AB500C303A5 /* NSDate+Ext.m */, 54B517052270E8C6006C1B29 /* NSView+Ext.h */, @@ -290,6 +291,7 @@ 544936F721F1E51E00DEE9AA /* Helper */, 541A90EF21257D4F002680A6 /* Status Bar Menu */, 54A07A8322105E0800082C51 /* Core Data */, + 54AD4E04230084FD000AE386 /* Feed Import */, 546FC44D2118B357007CC3A3 /* Preferences */, 54ACC28A21061B3C0020715F /* Info.plist */, 54F7101322EE0DDA006985D1 /* Artwork */, @@ -299,6 +301,19 @@ path = baRSS; sourceTree = ""; }; + 54AD4E04230084FD000AE386 /* Feed Import */ = { + isa = PBXGroup; + children = ( + 54ACC29321061E270020715F /* UpdateScheduler.h */, + 54ACC29421061E270020715F /* UpdateScheduler.m */, + 54AD4DFE23005297000AE386 /* WebFeed.h */, + 54AD4DFF23005297000AE386 /* WebFeed.m */, + 54F6025B21C1D4170006D338 /* OpmlFile.h */, + 54F6025C21C1D4170006D338 /* OpmlFile.m */, + ); + path = "Feed Import"; + sourceTree = ""; + }; 54D857CF228022AB001BA1C8 /* General Tab */ = { isa = PBXGroup; children = ( @@ -339,8 +354,6 @@ 546FC43C21188AD5007CC3A3 /* SettingsFeeds.m */, 54D55D7122E624CD00057B98 /* SettingsFeeds+DragDrop.h */, 54D55D7222E624CD00057B98 /* SettingsFeeds+DragDrop.m */, - 54F6025B21C1D4170006D338 /* OpmlFile.h */, - 54F6025C21C1D4170006D338 /* OpmlFile.m */, 54E8831D211B509D00064188 /* ModalFeedEdit.h */, 54E8831E211B509D00064188 /* ModalFeedEdit.m */, 5478DF02225A7AE200D30C64 /* SettingsFeedsView.h */, @@ -491,6 +504,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 54AD4E0023005297000AE386 /* WebFeed.m in Sources */, 54B51704226DC339006C1B29 /* ModalFeedEditView.m in Sources */, 546A6A2C22C584AF0034E806 /* SettingsAppearanceView.m in Sources */, 54E9CF32225914300023696F /* SettingsAbout.m in Sources */, @@ -500,7 +514,7 @@ 544B011D2114EE9100386E5C /* AppHook.m in Sources */, 546FC44321189975007CC3A3 /* SettingsGeneral.m in Sources */, 546A6A2922C583390034E806 /* SettingsGeneralView.m in Sources */, - 54ACC29521061E270020715F /* FeedDownload.m in Sources */, + 54ACC29521061E270020715F /* UpdateScheduler.m in Sources */, 54BB048921FD2AB500C303A5 /* NSDate+Ext.m in Sources */, 5477D34E21233C62002BA27F /* FeedGroup+Ext.m in Sources */, 5478DF04225A7AE200D30C64 /* SettingsFeedsView.m in Sources */, diff --git a/baRSS/AppHook.m b/baRSS/AppHook.m index b92494f..b188adf 100644 --- a/baRSS/AppHook.m +++ b/baRSS/AppHook.m @@ -22,7 +22,8 @@ #import "AppHook.h" #import "BarStatusItem.h" -#import "FeedDownload.h" +#import "WebFeed.h" +#import "UpdateScheduler.h" #import "Preferences.h" #import "DrawImage.h" #import "SettingsFeeds+DragDrop.h" @@ -52,15 +53,15 @@ - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { [_statusItem asyncReloadUnreadCount]; - [FeedDownload registerNetworkChangeNotification]; // will call update scheduler + [UpdateScheduler registerNetworkChangeNotification]; // will call update scheduler if ([StoreCoordinator isEmpty]) { [_statusItem showWelcomeMessage]; - [FeedDownload autoDownloadAndParseUpdateURL]; + [WebFeed autoDownloadAndParseUpdateURL]; } } - (void)applicationWillTerminate:(NSNotification *)aNotification { - [FeedDownload unregisterNetworkChangeNotification]; + [UpdateScheduler unregisterNetworkChangeNotification]; } - (void)handleGetURLEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent { @@ -72,7 +73,7 @@ url = [url substringFromIndex:2]; } if ([scheme isEqualToString:@"feed"]) { - [FeedDownload autoDownloadAndParseURL:url addAnyway:NO modify:nil]; + [WebFeed autoDownloadAndParseURL:url addAnyway:NO modify:nil]; } } @@ -107,7 +108,7 @@ - (void)preferencesClosed:(id)sender { [[NSNotificationCenter defaultCenter] removeObserver:self name:NSWindowWillCloseNotification object:self.prefWindow.window]; self.prefWindow = nil; - [FeedDownload scheduleUpdateForUpcomingFeeds]; + [UpdateScheduler scheduleNextFeed]; } /// Close previous preferences window and re-open at the same position (will drop undo manager stack!) diff --git a/baRSS/Preferences/Feeds Tab/OpmlFile.h b/baRSS/Feed Import/OpmlFile.h similarity index 100% rename from baRSS/Preferences/Feeds Tab/OpmlFile.h rename to baRSS/Feed Import/OpmlFile.h diff --git a/baRSS/Preferences/Feeds Tab/OpmlFile.m b/baRSS/Feed Import/OpmlFile.m similarity index 99% rename from baRSS/Preferences/Feeds Tab/OpmlFile.m rename to baRSS/Feed Import/OpmlFile.m index 576f7dc..8754c0d 100644 --- a/baRSS/Preferences/Feeds Tab/OpmlFile.m +++ b/baRSS/Feed Import/OpmlFile.m @@ -24,11 +24,11 @@ #import "FeedMeta+Ext.h" #import "FeedGroup+Ext.h" #import "StoreCoordinator.h" -#import "FeedDownload.h" #import "Constants.h" #import "NSDate+Ext.h" #import "NSView+Ext.h" +#import #pragma mark - Helper diff --git a/baRSS/Feed Import/UpdateScheduler.h b/baRSS/Feed Import/UpdateScheduler.h new file mode 100644 index 0000000..fd12c38 --- /dev/null +++ b/baRSS/Feed Import/UpdateScheduler.h @@ -0,0 +1,38 @@ +// +// The MIT License (MIT) +// Copyright (c) 2019 Oleg Geier +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#import + +@interface UpdateScheduler : NSObject +@property (class, readonly) NSDate *dateScheduled; +@property (class, readonly) BOOL allowNetworkConnection; +@property (class, readonly) BOOL isUpdating; +@property (class, setter=setPaused:) BOOL isPaused; + ++ (void)beginUpdate; ++ (void)endUpdate; ++ (void)scheduleNextFeed; ++ (void)forceUpdateAllFeeds; +// Register for network change notifications ++ (void)registerNetworkChangeNotification; ++ (void)unregisterNetworkChangeNotification; +@end diff --git a/baRSS/Feed Import/UpdateScheduler.m b/baRSS/Feed Import/UpdateScheduler.m new file mode 100644 index 0000000..7c5a285 --- /dev/null +++ b/baRSS/Feed Import/UpdateScheduler.m @@ -0,0 +1,210 @@ +// +// The MIT License (MIT) +// Copyright (c) 2019 Oleg Geier +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#import "UpdateScheduler.h" +#import "WebFeed.h" +#import "Constants.h" +#import "StoreCoordinator.h" + +#import + +static NSTimer *_timer; +static SCNetworkReachabilityRef _reachability = NULL; +static BOOL _isReachable = NO; +static BOOL _isUpdating = NO; +static BOOL _updatePaused = NO; +static BOOL _nextUpdateIsForced = NO; + + +@implementation UpdateScheduler + +#pragma mark - User Interaction + +/// @return Date when background update will fire. If updates are paused, date is @c distantFuture. ++ (NSDate *)dateScheduled { return _timer.fireDate; } + +/// @return @c YES if current network state is reachable and updates are not paused by user. ++ (BOOL)allowNetworkConnection { return (_isReachable && !_updatePaused); } + +/// @return @c YES if batch update is running ++ (BOOL)isUpdating { return _isUpdating; } + +/// @return @c YES if update is paused by user. ++ (BOOL)isPaused { return _updatePaused; } + +/// Set paused flag and cancel timer regardless of network connectivity. ++ (void)setPaused:(BOOL)flag { + _updatePaused = flag; + if (_updatePaused) + [self pauseUpdates]; + else + [self resumeUpdates]; +} + +/// Cancel current timer and stop any updates until enabled again. ++ (void)pauseUpdates { + [self scheduleTimer:nil]; +} + +/// Start normal (non forced) schedule if network is reachable. ++ (void)resumeUpdates { + if (_isReachable) + [self scheduleNextFeed]; +} + +/// Set @c isUpdating @c = @c YES ++ (void)beginUpdate { _isUpdating = YES; } + +/// Set @c isUpdating @c = @c NO ++ (void)endUpdate { _isUpdating = NO; } + + +#pragma mark - Update Feed Timer + + +/** + Get date of next up feed and start the timer. + */ ++ (void)scheduleNextFeed { + if (![self allowNetworkConnection]) // timer will restart once connection exists + return; + NSDate *nextTime = [StoreCoordinator nextScheduledUpdate]; // if nextTime = nil, then no feeds to update + if (nextTime && [nextTime timeIntervalSinceNow] < 1) { // mostly, if app was closed for a long time + nextTime = [NSDate dateWithTimeIntervalSinceNow:1]; + } + [self scheduleTimer:nextTime]; +} + +/** + Start download of all feeds (immediatelly) regardless of @c .scheduled property. + */ ++ (void)forceUpdateAllFeeds { + if (![self allowNetworkConnection]) // timer will restart once connection exists + return; + _nextUpdateIsForced = YES; + [self scheduleTimer:[NSDate dateWithTimeIntervalSinceNow:0.05]]; +} + +/** + Set new @c .fireDate and @c .tolerance for update timer. + + @param nextTime If @c nil timer will be disabled with a @c .fireDate very far in the future. + */ ++ (void)scheduleTimer:(NSDate*)nextTime { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + _timer = [NSTimer timerWithTimeInterval:NSTimeIntervalSince1970 target:[self class] selector:@selector(updateTimerCallback) userInfo:nil repeats:YES]; + [[NSRunLoop mainRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes]; + }); + if (!nextTime) + nextTime = [NSDate distantFuture]; + NSTimeInterval tolerance = [nextTime timeIntervalSinceNow] * 0.15; + _timer.tolerance = (tolerance < 1 ? 1 : tolerance); // at least 1 sec + _timer.fireDate = nextTime; +} + +/** + Called when schedule timer runs out (earliest @c .schedule date). Or if forced by user request. + */ ++ (void)updateTimerCallback { +#ifdef DEBUG + NSLog(@"fired"); +#endif + BOOL updateAll = _nextUpdateIsForced; + _nextUpdateIsForced = NO; + if (updateAll) + [WebFeed setRequestsAreUrgent:YES]; + + NSManagedObjectContext *moc = [StoreCoordinator createChildContext]; + NSArray *list = [StoreCoordinator getListOfFeedsThatNeedUpdate:updateAll inContext:moc]; + //NSAssert(list.count > 0, @"ERROR: Something went wrong, timer fired too early."); + if (![self allowNetworkConnection]) { + [WebFeed setRequestsAreUrgent:NO]; + [moc reset]; + return; + } + [WebFeed batchDownloadFeeds:list favicons:updateAll showErrorAlert:NO finally:^{ + [WebFeed setRequestsAreUrgent:NO]; + [StoreCoordinator saveContext:moc andParent:YES]; // save parents too ... + [moc reset]; + [self resumeUpdates]; // always reset the timer + }]; +} + + +#pragma mark - Network Connection & Reachability + + +/// Set callback on @c self to listen for network reachability changes. ++ (void)registerNetworkChangeNotification { + // https://stackoverflow.com/questions/11240196/notification-when-wifi-connected-os-x + if (_reachability != NULL) return; + _reachability = SCNetworkReachabilityCreateWithName(NULL, "1.1.1.1"); + if (_reachability == NULL) return; + // If reachability information is available now, we don't get a callback later + SCNetworkConnectionFlags flags; + if (SCNetworkReachabilityGetFlags(_reachability, &flags)) + networkReachabilityCallback(_reachability, flags, NULL); + if (!SCNetworkReachabilitySetCallback(_reachability, networkReachabilityCallback, NULL) || + !SCNetworkReachabilityScheduleWithRunLoop(_reachability, [[NSRunLoop currentRunLoop] getCFRunLoop], kCFRunLoopCommonModes)) + { + CFRelease(_reachability); + _reachability = NULL; + } +} + +/// Remove @c self callback (network reachability changes). ++ (void)unregisterNetworkChangeNotification { + if (_reachability != NULL) { + SCNetworkReachabilitySetCallback(_reachability, nil, nil); + SCNetworkReachabilitySetDispatchQueue(_reachability, nil); + CFRelease(_reachability); + _reachability = NULL; + } +} + +/// Called when network interface or reachability changes. +static void networkReachabilityCallback(SCNetworkReachabilityRef target, SCNetworkConnectionFlags flags, void *object) { + if (_reachability == NULL) return; + _isReachable = [UpdateScheduler hasConnectivity:flags]; + PostNotification(kNotificationNetworkStatusChanged, @(_isReachable)); + if (_isReachable) { + [UpdateScheduler resumeUpdates]; + } else { + [UpdateScheduler pauseUpdates]; + } +} + +/// @return @c YES if network connection established. ++ (BOOL)hasConnectivity:(SCNetworkReachabilityFlags)flags { + if ((flags & kSCNetworkReachabilityFlagsReachable) == 0) + return NO; + if ((flags & kSCNetworkReachabilityFlagsConnectionRequired) == 0) + return YES; + if ((flags & kSCNetworkReachabilityFlagsInterventionRequired) == 0 && + ((flags & kSCNetworkReachabilityFlagsConnectionOnDemand) != 0 || + (flags & kSCNetworkReachabilityFlagsConnectionOnTraffic) != 0)) + return YES; // no-intervention AND ( on-demand OR on-traffic ) + return NO; +} + +@end diff --git a/baRSS/Helper/FeedDownload.h b/baRSS/Feed Import/WebFeed.h similarity index 82% rename from baRSS/Helper/FeedDownload.h rename to baRSS/Feed Import/WebFeed.h index a2a3348..8540bb6 100644 --- a/baRSS/Helper/FeedDownload.h +++ b/baRSS/Feed Import/WebFeed.h @@ -20,23 +20,13 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -#import +#import #import @class Feed; -@interface FeedDownload : NSObject -@property (class, readonly) NSDate *dateScheduled; -@property (class, readonly) BOOL allowNetworkConnection; -@property (class, readonly) BOOL isUpdating; -@property (class, setter=setPaused:) BOOL isPaused; - -// Register for network change notifications -+ (void)registerNetworkChangeNotification; -+ (void)unregisterNetworkChangeNotification; -// Scheduling -+ (void)scheduleUpdateForUpcomingFeeds; -+ (void)forceUpdateAllFeeds; +@interface WebFeed : NSObject ++ (void)setRequestsAreUrgent:(BOOL)flag; // Downloading + (void)newFeed:(NSString *)urlStr askUser:(nonnull NSString*(^)(RSHTMLMetadata *meta))askUser block:(nonnull void(^)(RSParsedFeed *parsed, NSError *error, NSHTTPURLResponse *response))block; + (void)autoDownloadAndParseURL:(NSString*)urlStr addAnyway:(BOOL)flag modify:(nullable void(^)(Feed *feed))block; diff --git a/baRSS/Helper/FeedDownload.m b/baRSS/Feed Import/WebFeed.m similarity index 73% rename from baRSS/Helper/FeedDownload.m rename to baRSS/Feed Import/WebFeed.m index d77915b..7e4059d 100644 --- a/baRSS/Helper/FeedDownload.m +++ b/baRSS/Feed Import/WebFeed.m @@ -20,7 +20,8 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -#import "FeedDownload.h" +#import "WebFeed.h" +#import "UpdateScheduler.h" #import "Constants.h" #import "StoreCoordinator.h" #import "Feed+Ext.h" @@ -28,124 +29,16 @@ #import "FeedGroup+Ext.h" #import "NSDate+Ext.h" -#import - -static NSTimer *_timer; -static SCNetworkReachabilityRef _reachability = NULL; -static BOOL _isReachable = NO; -static BOOL _isUpdating = NO; -static BOOL _updatePaused = NO; -static BOOL _nextUpdateIsForced = NO; +static BOOL _requestsAreUrgent = NO; -@implementation FeedDownload +@implementation WebFeed + +/// Disables @c NSURLNetworkServiceTypeBackground (ideally only temporarily) ++ (void)setRequestsAreUrgent:(BOOL)flag { _requestsAreUrgent = flag; } -#pragma mark - User Interaction - - -/// @return Date when background update will fire. If updates are paused, date is @c distantFuture. -+ (NSDate *)dateScheduled { return _timer.fireDate; } - -/// @return @c YES if current network state is reachable and updates are not paused by user. -+ (BOOL)allowNetworkConnection { return (_isReachable && !_updatePaused); } - -/// @return @c YES if batch update is running -+ (BOOL)isUpdating { return _isUpdating; } - -/// @return @c YES if update is paused by user. -+ (BOOL)isPaused { return _updatePaused; } - -/// Set paused flag and cancel timer regardless of network connectivity. -+ (void)setPaused:(BOOL)flag { - _updatePaused = flag; - if (_updatePaused) - [self pauseUpdates]; - else - [self resumeUpdates]; -} - -/// Cancel current timer and stop any updates until enabled again. -+ (void)pauseUpdates { - [self scheduleTimer:nil]; -} - -/// Start normal (non forced) schedule if network is reachable. -+ (void)resumeUpdates { - if (_isReachable) - [self scheduleUpdateForUpcomingFeeds]; -} - - -#pragma mark - Update Feed Timer - - - -/** - Get date of next up feed and start the timer. - */ -+ (void)scheduleUpdateForUpcomingFeeds { - if (![self allowNetworkConnection]) // timer will restart once connection exists - return; - NSDate *nextTime = [StoreCoordinator nextScheduledUpdate]; // if nextTime = nil, then no feeds to update - if (nextTime && [nextTime timeIntervalSinceNow] < 1) { // mostly, if app was closed for a long time - nextTime = [NSDate dateWithTimeIntervalSinceNow:1]; - } - [self scheduleTimer:nextTime]; -} - -/** - Start download of all feeds (immediatelly) regardless of @c .scheduled property. - */ -+ (void)forceUpdateAllFeeds { - if (![self allowNetworkConnection]) // timer will restart once connection exists - return; - _nextUpdateIsForced = YES; - [self scheduleTimer:[NSDate dateWithTimeIntervalSinceNow:0.05]]; -} - -/** - Set new @c .fireDate and @c .tolerance for update timer. - - @param nextTime If @c nil timer will be disabled with a @c .fireDate very far in the future. - */ -+ (void)scheduleTimer:(NSDate*)nextTime { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - _timer = [NSTimer timerWithTimeInterval:NSTimeIntervalSince1970 target:[self class] selector:@selector(updateTimerCallback) userInfo:nil repeats:YES]; - [[NSRunLoop mainRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes]; - }); - if (!nextTime) - nextTime = [NSDate distantFuture]; - NSTimeInterval tolerance = [nextTime timeIntervalSinceNow] * 0.15; - _timer.tolerance = (tolerance < 1 ? 1 : tolerance); // at least 1 sec - _timer.fireDate = nextTime; -} - -/** - Called when schedule timer runs out (earliest @c .schedule date). Or if forced by user request. - */ -+ (void)updateTimerCallback { -#ifdef DEBUG - NSLog(@"fired"); -#endif - BOOL updateAll = _nextUpdateIsForced; - _nextUpdateIsForced = NO; - - NSManagedObjectContext *moc = [StoreCoordinator createChildContext]; - NSArray *list = [StoreCoordinator getListOfFeedsThatNeedUpdate:updateAll inContext:moc]; - //NSAssert(list.count > 0, @"ERROR: Something went wrong, timer fired too early."); - if (![self allowNetworkConnection]) { - [moc reset]; - return; - } - [self batchDownloadFeeds:list favicons:updateAll showErrorAlert:NO finally:^{ - [StoreCoordinator saveContext:moc andParent:YES]; // save parents too ... - [moc reset]; - [self resumeUpdates]; // always reset the timer - }]; -} - - -#pragma mark - Request Generator - +#pragma mark - Request Generator /// @return Base URL part. E.g., https://stackoverflow.com/a/15897956/10616114 ==> https://stackoverflow.com/ @@ -176,7 +69,7 @@ static BOOL _nextUpdateIsForced = NO; else if (meta.modified.length > 0) [req setValue:meta.modified forHTTPHeaderField:@"If-Modified-Since"]; } - if (!_nextUpdateIsForced) // any request that is not forced, is a background update + if (!_requestsAreUrgent) // any request that is not forced, is a background update req.networkServiceType = NSURLNetworkServiceTypeBackground; return req; } @@ -229,7 +122,7 @@ static BOOL _nextUpdateIsForced = NO; } -#pragma mark - Download RSS Feed - +#pragma mark - Download RSS Feed /** @@ -264,7 +157,7 @@ static BOOL _nextUpdateIsForced = NO; /** Perform feed download request from URL alone. Not updating any @c Feed item. - + @note @c askUser will not be called if url is XML already. @param urlStr XML URL or HTTP URL that will be parsed to find feed URLs. @@ -357,7 +250,7 @@ static BOOL _nextUpdateIsForced = NO; [moc reset]; if (successful) { PostNotification(kNotificationGroupInserted, f.group.objectID); - [self scheduleUpdateForUpcomingFeeds]; + [UpdateScheduler scheduleNextFeed]; } }]; } @@ -391,14 +284,14 @@ static BOOL _nextUpdateIsForced = NO; /** Start download of all feeds in list. Either with or without favicons. - + @param list Download list using @c feed.meta.url as download url. (while reusing etag and modified headers) @param fav If @c YES continue with favicon download after xml download finished. @param alert If @c YES display Error Popup to user. @param block Called after all downloads finished. */ + (void)batchDownloadFeeds:(NSArray *)list favicons:(BOOL)fav showErrorAlert:(BOOL)alert finally:(nullable os_block_t)block { - _isUpdating = YES; + [UpdateScheduler beginUpdate]; PostNotification(kNotificationBackgroundUpdateInProgress, @(list.count)); dispatch_group_t group = dispatch_group_create(); for (Feed *f in list) { @@ -409,13 +302,13 @@ static BOOL _nextUpdateIsForced = NO; } dispatch_group_notify(group, dispatch_get_main_queue(), ^{ if (block) block(); - _isUpdating = NO; + [UpdateScheduler endUpdate]; PostNotification(kNotificationBackgroundUpdateInProgress, @(0)); }); } -#pragma mark - Download Favicon - +#pragma mark - Download Favicon /** @@ -457,7 +350,7 @@ static BOOL _nextUpdateIsForced = NO; }]; } -/// Download html page and parse all icon urls. Starting a successive request on the url of the smallest icon. +/// Download html page and parse all icon urls. Starting a successive request on the favicon url. + (void)downloadFaviconByParsingHTML:(NSString*)hostURL finished:(void(^)(NSImage * _Nullable img))block { [self asyncRequest:[self newRequestURL:hostURL] block:^(NSData * _Nullable htmlData, NSError * _Nullable error, NSHTTPURLResponse *response) { if (htmlData) { @@ -524,61 +417,4 @@ static BOOL _nextUpdateIsForced = NO; }]; } - -#pragma mark - Network Connection & Reachability - - - -/// Set callback on @c self to listen for network reachability changes. -+ (void)registerNetworkChangeNotification { - // https://stackoverflow.com/questions/11240196/notification-when-wifi-connected-os-x - if (_reachability != NULL) return; - _reachability = SCNetworkReachabilityCreateWithName(NULL, "1.1.1.1"); - if (_reachability == NULL) return; - // If reachability information is available now, we don't get a callback later - SCNetworkConnectionFlags flags; - if (SCNetworkReachabilityGetFlags(_reachability, &flags)) - networkReachabilityCallback(_reachability, flags, NULL); - if (!SCNetworkReachabilitySetCallback(_reachability, networkReachabilityCallback, NULL) || - !SCNetworkReachabilityScheduleWithRunLoop(_reachability, [[NSRunLoop currentRunLoop] getCFRunLoop], kCFRunLoopCommonModes)) - { - CFRelease(_reachability); - _reachability = NULL; - } -} - -/// Remove @c self callback (network reachability changes). -+ (void)unregisterNetworkChangeNotification { - if (_reachability != NULL) { - SCNetworkReachabilitySetCallback(_reachability, nil, nil); - SCNetworkReachabilitySetDispatchQueue(_reachability, nil); - CFRelease(_reachability); - _reachability = NULL; - } -} - -/// Called when network interface or reachability changes. -static void networkReachabilityCallback(SCNetworkReachabilityRef target, SCNetworkConnectionFlags flags, void *object) { - if (_reachability == NULL) return; - _isReachable = [FeedDownload hasConnectivity:flags]; - PostNotification(kNotificationNetworkStatusChanged, @(_isReachable)); - if (_isReachable) { - [FeedDownload resumeUpdates]; - } else { - [FeedDownload pauseUpdates]; - } -} - -/// @return @c YES if network connection established. -+ (BOOL)hasConnectivity:(SCNetworkReachabilityFlags)flags { - if ((flags & kSCNetworkReachabilityFlagsReachable) == 0) - return NO; - if ((flags & kSCNetworkReachabilityFlagsConnectionRequired) == 0) - return YES; - if ((flags & kSCNetworkReachabilityFlagsInterventionRequired) == 0 && - ((flags & kSCNetworkReachabilityFlagsConnectionOnDemand) != 0 || - (flags & kSCNetworkReachabilityFlagsConnectionOnTraffic) != 0)) - return YES; // no-intervention AND ( on-demand OR on-traffic ) - return NO; -} - @end diff --git a/baRSS/Info.plist b/baRSS/Info.plist index dae1ac5..3116f85 100644 --- a/baRSS/Info.plist +++ b/baRSS/Info.plist @@ -60,7 +60,7 @@ CFBundleVersion - 10180 + 10198 LSApplicationCategoryType public.app-category.news LSMinimumSystemVersion diff --git a/baRSS/Preferences/Feeds Tab/ModalFeedEdit.m b/baRSS/Preferences/Feeds Tab/ModalFeedEdit.m index 6cd295d..0bcc1e9 100644 --- a/baRSS/Preferences/Feeds Tab/ModalFeedEdit.m +++ b/baRSS/Preferences/Feeds Tab/ModalFeedEdit.m @@ -21,7 +21,7 @@ // SOFTWARE. #import "ModalFeedEdit.h" -#import "FeedDownload.h" +#import "WebFeed.h" #import "StoreCoordinator.h" #import "Feed+Ext.h" #import "FeedMeta+Ext.h" @@ -158,8 +158,8 @@ if (self.modalSheet.didCloseAndCancel) return; [self preDownload]; - [FeedDownload newFeed:self.previousURL askUser:^NSString *(RSHTMLMetadata *meta) { - self.faviconURL = [FeedDownload faviconUrlForMetadata:meta]; // we can re-use favicon url if we find one + [WebFeed newFeed:self.previousURL askUser:^NSString *(RSHTMLMetadata *meta) { + self.faviconURL = [WebFeed faviconUrlForMetadata:meta]; // we can re-use favicon url if we find one return [self letUserChooseXmlUrlFromList:meta.feedLinks]; } block:^(RSParsedFeed *result, NSError *error, NSHTTPURLResponse* response) { if (self.modalSheet.didCloseAndCancel) @@ -239,7 +239,7 @@ self.faviconURL = self.feedResult.link; if (self.faviconURL.length == 0) self.faviconURL = responseURL; - [FeedDownload downloadFavicon:self.faviconURL finished:^(NSImage * _Nullable img) { + [WebFeed downloadFavicon:self.faviconURL finished:^(NSImage * _Nullable img) { if (self.modalSheet.didCloseAndCancel) return; self.view.favicon.image = img; diff --git a/baRSS/Preferences/Feeds Tab/SettingsFeeds+DragDrop.m b/baRSS/Preferences/Feeds Tab/SettingsFeeds+DragDrop.m index fa4bc72..268095c 100644 --- a/baRSS/Preferences/Feeds Tab/SettingsFeeds+DragDrop.m +++ b/baRSS/Preferences/Feeds Tab/SettingsFeeds+DragDrop.m @@ -23,7 +23,7 @@ #import "SettingsFeeds+DragDrop.h" #import "StoreCoordinator.h" #import "Constants.h" -#import "FeedDownload.h" +#import "WebFeed.h" #import "FeedGroup+Ext.h" // Pasteboard type used during internal row reordering @@ -160,7 +160,7 @@ const NSPasteboardType dragReorder = @"de.relikd.baRSS.drag-reorder"; [StoreCoordinator saveContext:moc andParent:YES]; if (selection.count > 0) [self.dataStore setSelectionIndexPaths:selection]; - [FeedDownload batchDownloadFeeds:feedsList favicons:YES showErrorAlert:YES finally:^{ + [WebFeed batchDownloadFeeds:feedsList favicons:YES showErrorAlert:YES finally:^{ [self endCoreDataChangeUndoEmpty:NO forceUndo:NO]; [self someDatesChangedScheduleUpdateTimer]; }]; diff --git a/baRSS/Preferences/Feeds Tab/SettingsFeeds.m b/baRSS/Preferences/Feeds Tab/SettingsFeeds.m index 2ece0a2..4411827 100644 --- a/baRSS/Preferences/Feeds Tab/SettingsFeeds.m +++ b/baRSS/Preferences/Feeds Tab/SettingsFeeds.m @@ -25,7 +25,7 @@ #import "StoreCoordinator.h" #import "ModalFeedEdit.h" #import "FeedGroup+Ext.h" -#import "FeedDownload.h" +#import "UpdateScheduler.h" #import "SettingsFeedsView.h" #import "NSDate+Ext.h" @@ -65,7 +65,7 @@ self.timerStatusInfo = [NSTimer timerWithTimeInterval:NSTimeIntervalSince1970 target:self selector:@selector(keepTimerRunning) userInfo:nil repeats:YES]; [[NSRunLoop mainRunLoop] addTimer:self.timerStatusInfo forMode:NSRunLoopCommonModes]; // start spinner if update is in progress when preferences open - [self activateSpinner:([FeedDownload isUpdating] ? -1 : 0)]; + [self activateSpinner:([UpdateScheduler isUpdating] ? -1 : 0)]; } /// Timer cleanup @@ -147,7 +147,7 @@ /// Query core data for next update date and set bottom status message - (void)someDatesChangedScheduleUpdateTimer { - [FeedDownload scheduleUpdateForUpcomingFeeds]; + [UpdateScheduler scheduleNextFeed]; [self.timerStatusInfo fire]; } @@ -174,7 +174,7 @@ /// Callback method to update status info. Will be called more often when interval is getting shorter. - (void)keepTimerRunning { - NSDate *date = [FeedDownload dateScheduled]; + NSDate *date = [UpdateScheduler dateScheduled]; if (date) { double nextFire = fabs(date.timeIntervalSinceNow); if (nextFire > 1e9) { // distance future, over 31 years @@ -280,9 +280,9 @@ /// Returning @c NO will result in a Action-Not-Available-Buzzer sound - (BOOL)respondsToSelector:(SEL)aSelector { if (aSelector == @selector(undo:)) - return [self.undoManager canUndo] && self.undoManager.groupingLevel == 0 && ![FeedDownload isUpdating]; + return [self.undoManager canUndo] && self.undoManager.groupingLevel == 0 && ![UpdateScheduler isUpdating]; if (aSelector == @selector(redo:)) - return [self.undoManager canRedo] && self.undoManager.groupingLevel == 0 && ![FeedDownload isUpdating]; + return [self.undoManager canRedo] && self.undoManager.groupingLevel == 0 && ![UpdateScheduler isUpdating]; if (aSelector == @selector(copy:) || aSelector == @selector(remove:)) return ([self userSelectionFirst] != nil); if (aSelector == @selector(editSelectedItem)) { diff --git a/baRSS/Status Bar Menu/BarStatusItem.m b/baRSS/Status Bar Menu/BarStatusItem.m index f31d191..0c9f0b0 100644 --- a/baRSS/Status Bar Menu/BarStatusItem.m +++ b/baRSS/Status Bar Menu/BarStatusItem.m @@ -22,7 +22,7 @@ #import "BarStatusItem.h" #import "Constants.h" -#import "FeedDownload.h" +#import "UpdateScheduler.h" #import "StoreCoordinator.h" #import "UserPrefs.h" #import "BarMenu.h" @@ -117,7 +117,7 @@ /// Update menu bar icon and text according to unread count and user preferences. - (void)updateBarIcon { dispatch_async(dispatch_get_main_queue(), ^{ - BOOL hasNet = [FeedDownload allowNetworkConnection]; + BOOL hasNet = [UpdateScheduler allowNetworkConnection]; BOOL tint = (self.unreadCountTotal > 0 && hasNet && [UserPrefs defaultYES:@"globalTintMenuBarIcon"]); self.statusItem.image = [NSImage imageNamed:(hasNet ? RSSImageMenuBarIconActive : RSSImageMenuBarIconPaused)]; self.statusItem.image.template = !tint; @@ -169,13 +169,13 @@ // 'Pause Updates' item NSMenuItem *pause = [menu addItemWithTitle:NSLocalizedString(@"Pause Updates", nil) action:@selector(pauseUpdates) keyEquivalent:@""]; pause.target = self; - if ([FeedDownload isPaused]) + if ([UpdateScheduler isPaused]) pause.title = NSLocalizedString(@"Resume Updates", nil); // 'Update all feeds' item if ([UserPrefs defaultYES:@"globalUpdateAll"]) { NSMenuItem *updateAll = [menu addItemWithTitle:NSLocalizedString(@"Update all feeds", nil) action:@selector(updateAllFeeds) keyEquivalent:@""]; updateAll.target = self; - updateAll.enabled = [FeedDownload allowNetworkConnection]; + updateAll.enabled = [UpdateScheduler allowNetworkConnection]; self.updateAllItem = updateAll; } // Separator between main header and default header @@ -184,14 +184,14 @@ /// Called when user clicks on 'Pause Updates' (main menu only). - (void)pauseUpdates { - [FeedDownload setPaused:![FeedDownload isPaused]]; + [UpdateScheduler setPaused:![UpdateScheduler isPaused]]; [self updateBarIcon]; } /// Called when user clicks on 'Update all feeds' (main menu only). - (void)updateAllFeeds { // [self asyncReloadUnreadCount]; // should not be necessary - [FeedDownload forceUpdateAllFeeds]; + [UpdateScheduler forceUpdateAllFeeds]; } @end