mirror of
https://github.com/samuelclay/NewsBlur.git
synced 2025-08-31 21:41:33 +00:00
Refactoring the living crap out of the offline network operations. Now work in order and all cancelable with a single call. Still need to fix the network operation queue for downloading images and uploading the progress bar.
This commit is contained in:
parent
bcac92bcd7
commit
5745044927
10 changed files with 569 additions and 430 deletions
|
@ -952,7 +952,7 @@ def unread_story_hashes(request):
|
||||||
|
|
||||||
logging.user(request, "~FYLoading ~FCunread story hashes~FY: ~SB%s feeds~SN (%s story hashes)" %
|
logging.user(request, "~FYLoading ~FCunread story hashes~FY: ~SB%s feeds~SN (%s story hashes)" %
|
||||||
(len(feed_ids), len(story_hashes)))
|
(len(feed_ids), len(story_hashes)))
|
||||||
|
time.sleep(1)
|
||||||
return dict(unread_feed_story_hashes=story_hashes)
|
return dict(unread_feed_story_hashes=story_hashes)
|
||||||
|
|
||||||
@ajax_login_required
|
@ajax_login_required
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
#import <UIKit/UIKit.h>
|
#import <UIKit/UIKit.h>
|
||||||
#import "BaseViewController.h"
|
#import "BaseViewController.h"
|
||||||
#import "FMDatabaseQueue.h"
|
#import "FMDatabaseQueue.h"
|
||||||
#import "ASINetworkQueue.h"
|
|
||||||
|
|
||||||
#define FEED_DETAIL_VIEW_TAG 1000001
|
#define FEED_DETAIL_VIEW_TAG 1000001
|
||||||
#define STORY_DETAIL_VIEW_TAG 1000002
|
#define STORY_DETAIL_VIEW_TAG 1000002
|
||||||
|
@ -138,10 +137,10 @@
|
||||||
NSMutableArray * dictFoldersArray;
|
NSMutableArray * dictFoldersArray;
|
||||||
|
|
||||||
FMDatabaseQueue *database;
|
FMDatabaseQueue *database;
|
||||||
|
NSOperationQueue *offlineQueue;
|
||||||
NSArray *categories;
|
NSArray *categories;
|
||||||
NSDictionary *categoryFeeds;
|
NSDictionary *categoryFeeds;
|
||||||
UIImageView *splashView;
|
UIImageView *splashView;
|
||||||
ASINetworkQueue *operationQueue;
|
|
||||||
NSMutableDictionary *activeCachedImages;
|
NSMutableDictionary *activeCachedImages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -186,7 +185,6 @@
|
||||||
@property (nonatomic, readwrite) BOOL isSocialRiverView;
|
@property (nonatomic, readwrite) BOOL isSocialRiverView;
|
||||||
@property (nonatomic, readwrite) BOOL isTryFeedView;
|
@property (nonatomic, readwrite) BOOL isTryFeedView;
|
||||||
@property (nonatomic, readwrite) BOOL inFindingStoryMode;
|
@property (nonatomic, readwrite) BOOL inFindingStoryMode;
|
||||||
@property (nonatomic, readwrite) BOOL hasQueuedReadStories;
|
|
||||||
@property (nonatomic) NSString *tryFeedStoryId;
|
@property (nonatomic) NSString *tryFeedStoryId;
|
||||||
@property (nonatomic) NSString *tryFeedCategory;
|
@property (nonatomic) NSString *tryFeedCategory;
|
||||||
@property (nonatomic, readwrite) BOOL popoverHasFeedView;
|
@property (nonatomic, readwrite) BOOL popoverHasFeedView;
|
||||||
|
@ -238,8 +236,9 @@
|
||||||
@property (nonatomic) NSArray *categories;
|
@property (nonatomic) NSArray *categories;
|
||||||
@property (nonatomic) NSDictionary *categoryFeeds;
|
@property (nonatomic) NSDictionary *categoryFeeds;
|
||||||
@property (readwrite) FMDatabaseQueue *database;
|
@property (readwrite) FMDatabaseQueue *database;
|
||||||
@property (readwrite) ASINetworkQueue *operationQueue;
|
@property (nonatomic) NSOperationQueue *offlineQueue;
|
||||||
@property (nonatomic) NSMutableDictionary *activeCachedImages;
|
@property (nonatomic) NSMutableDictionary *activeCachedImages;
|
||||||
|
@property (nonatomic, readwrite) BOOL hasQueuedReadStories;
|
||||||
|
|
||||||
+ (NewsBlurAppDelegate*) sharedAppDelegate;
|
+ (NewsBlurAppDelegate*) sharedAppDelegate;
|
||||||
- (void)startupAnimationDone:(NSString *)animationID finished:(NSNumber *)finished context:(void *)context;
|
- (void)startupAnimationDone:(NSString *)animationID finished:(NSNumber *)finished context:(void *)context;
|
||||||
|
@ -340,19 +339,14 @@
|
||||||
- (int)databaseSchemaVersion:(FMDatabase *)db;
|
- (int)databaseSchemaVersion:(FMDatabase *)db;
|
||||||
- (void)createDatabaseConnection;
|
- (void)createDatabaseConnection;
|
||||||
- (void)setupDatabase:(FMDatabase *)db;
|
- (void)setupDatabase:(FMDatabase *)db;
|
||||||
- (void)fetchUnreadHashes;
|
- (void)startOfflineQueue;
|
||||||
- (void)storeUnreadHashes:(ASIHTTPRequest *)request;
|
- (void)startOfflineFetchStories;
|
||||||
- (void)fetchAllUnreadStories;
|
- (void)startOfflineFetchImages;
|
||||||
- (void)storeAllUnreadStories:(ASIHTTPRequest *)request;
|
|
||||||
- (void)flushQueuedReadStories:(BOOL)forceCheck withCallback:(void(^)())callback;
|
- (void)flushQueuedReadStories:(BOOL)forceCheck withCallback:(void(^)())callback;
|
||||||
- (void)syncQueuedReadStories:(FMDatabase *)db withStories:(NSDictionary *)hashes withCallback:(void(^)())callback;
|
- (void)syncQueuedReadStories:(FMDatabase *)db withStories:(NSDictionary *)hashes withCallback:(void(^)())callback;
|
||||||
- (void)deleteAllCachedImages;
|
|
||||||
- (NSArray *)uncachedImageUrls;
|
|
||||||
- (void)fetchAllUncachedImages;
|
|
||||||
- (void)storeCachedImage:(ASIHTTPRequest *)request;
|
|
||||||
- (void)cachedImageQueueFinished:(ASINetworkQueue *)queue;
|
|
||||||
- (void)flushOldCachedImages;
|
|
||||||
- (void)prepareActiveCachedImages:(FMDatabase *)db;
|
- (void)prepareActiveCachedImages:(FMDatabase *)db;
|
||||||
|
- (void)flushOldCachedImages;
|
||||||
|
- (void)deleteAllCachedImages;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
|
|
@ -44,6 +44,9 @@
|
||||||
#import "FMDatabaseAdditions.h"
|
#import "FMDatabaseAdditions.h"
|
||||||
#import "JSON.h"
|
#import "JSON.h"
|
||||||
#import "IASKAppSettingsViewController.h"
|
#import "IASKAppSettingsViewController.h"
|
||||||
|
#import "OfflineSyncUnreads.h"
|
||||||
|
#import "OfflineFetchStories.h"
|
||||||
|
#import "OfflineFetchImages.h"
|
||||||
|
|
||||||
@implementation NewsBlurAppDelegate
|
@implementation NewsBlurAppDelegate
|
||||||
|
|
||||||
|
@ -94,7 +97,6 @@
|
||||||
@synthesize isTryFeedView;
|
@synthesize isTryFeedView;
|
||||||
|
|
||||||
@synthesize inFindingStoryMode;
|
@synthesize inFindingStoryMode;
|
||||||
@synthesize hasQueuedReadStories;
|
|
||||||
@synthesize tryFeedStoryId;
|
@synthesize tryFeedStoryId;
|
||||||
@synthesize tryFeedCategory;
|
@synthesize tryFeedCategory;
|
||||||
@synthesize popoverHasFeedView;
|
@synthesize popoverHasFeedView;
|
||||||
|
@ -146,8 +148,9 @@
|
||||||
@synthesize database;
|
@synthesize database;
|
||||||
@synthesize categories;
|
@synthesize categories;
|
||||||
@synthesize categoryFeeds;
|
@synthesize categoryFeeds;
|
||||||
@synthesize operationQueue;
|
|
||||||
@synthesize activeCachedImages;
|
@synthesize activeCachedImages;
|
||||||
|
@synthesize hasQueuedReadStories;
|
||||||
|
@synthesize offlineQueue;
|
||||||
|
|
||||||
+ (NewsBlurAppDelegate*) sharedAppDelegate {
|
+ (NewsBlurAppDelegate*) sharedAppDelegate {
|
||||||
return (NewsBlurAppDelegate*) [UIApplication sharedApplication].delegate;
|
return (NewsBlurAppDelegate*) [UIApplication sharedApplication].delegate;
|
||||||
|
@ -2224,8 +2227,34 @@
|
||||||
NSLog(@"Create db %d: %@", [db lastErrorCode], [db lastErrorMessage]);
|
NSLog(@"Create db %d: %@", [db lastErrorCode], [db lastErrorMessage]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)startOfflineQueue {
|
||||||
|
if (!offlineQueue) {
|
||||||
|
offlineQueue = [NSOperationQueue new];
|
||||||
|
}
|
||||||
|
offlineQueue.name = @"Offline Queue";
|
||||||
|
[offlineQueue cancelAllOperations];
|
||||||
|
[offlineQueue setMaxConcurrentOperationCount:1];
|
||||||
|
|
||||||
|
OfflineSyncUnreads *operationSyncUnreads = [[OfflineSyncUnreads alloc] init];
|
||||||
|
|
||||||
|
[offlineQueue addOperation:operationSyncUnreads];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)startOfflineFetchStories {
|
||||||
|
OfflineFetchStories *operationFetchStories = [[OfflineFetchStories alloc] init];
|
||||||
|
|
||||||
|
[offlineQueue addOperation:operationFetchStories];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)startOfflineFetchImages {
|
||||||
|
OfflineFetchImages *operationFetchImages = [[OfflineFetchImages alloc] init];
|
||||||
|
|
||||||
|
[offlineQueue addOperation:operationFetchImages];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
- (void)flushQueuedReadStories:(BOOL)forceCheck withCallback:(void(^)())callback {
|
- (void)flushQueuedReadStories:(BOOL)forceCheck withCallback:(void(^)())callback {
|
||||||
if (hasQueuedReadStories || forceCheck) {
|
if (self.hasQueuedReadStories || forceCheck) {
|
||||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,
|
||||||
(unsigned long)NULL), ^(void) {
|
(unsigned long)NULL), ^(void) {
|
||||||
[self flushOldCachedImages];
|
[self flushOldCachedImages];
|
||||||
|
@ -2242,7 +2271,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
if ([[hashes allKeys] count]) {
|
if ([[hashes allKeys] count]) {
|
||||||
hasQueuedReadStories = NO;
|
self.hasQueuedReadStories = NO;
|
||||||
[self syncQueuedReadStories:db withStories:hashes withCallback:callback];
|
[self syncQueuedReadStories:db withStories:hashes withCallback:callback];
|
||||||
} else {
|
} else {
|
||||||
if (callback) callback();
|
if (callback) callback();
|
||||||
|
@ -2274,419 +2303,12 @@
|
||||||
}];
|
}];
|
||||||
[request setFailedBlock:^{
|
[request setFailedBlock:^{
|
||||||
NSLog(@"Failed mark read queued.");
|
NSLog(@"Failed mark read queued.");
|
||||||
hasQueuedReadStories = YES;
|
self.hasQueuedReadStories = YES;
|
||||||
if (callback) callback();
|
if (callback) callback();
|
||||||
}];
|
}];
|
||||||
[request startAsynchronous];
|
[request startAsynchronous];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)fetchUnreadHashes {
|
|
||||||
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"%@/reader/unread_story_hashes?include_timestamps=true",
|
|
||||||
NEWSBLUR_URL]];
|
|
||||||
ASIHTTPRequest *_request = [ASIHTTPRequest requestWithURL:url];
|
|
||||||
__weak ASIHTTPRequest *request = _request;
|
|
||||||
[request setResponseEncoding:NSUTF8StringEncoding];
|
|
||||||
[request setDefaultResponseEncoding:NSUTF8StringEncoding];
|
|
||||||
[request setFailedBlock:^(void) {
|
|
||||||
NSLog(@"Failed fetch all story hashes.");
|
|
||||||
}];
|
|
||||||
[request setCompletionBlock:^(void) {
|
|
||||||
[self storeUnreadHashes:request];
|
|
||||||
}];
|
|
||||||
[request setTimeOutSeconds:30];
|
|
||||||
[request startAsynchronous];
|
|
||||||
|
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
|
||||||
[self.feedsViewController showSyncingNotifier];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)storeUnreadHashes:(ASIHTTPRequest *)request {
|
|
||||||
NSString *responseString = [request responseString];
|
|
||||||
NSData *responseData=[responseString dataUsingEncoding:NSUTF8StringEncoding];
|
|
||||||
NSError *error;
|
|
||||||
NSDictionary *results = [NSJSONSerialization
|
|
||||||
JSONObjectWithData:responseData
|
|
||||||
options:kNilOptions
|
|
||||||
error:&error];
|
|
||||||
__block __typeof__(self) _self = self;
|
|
||||||
|
|
||||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,
|
|
||||||
(unsigned long)NULL), ^(void) {
|
|
||||||
[_self.database inTransaction:^(FMDatabase *db, BOOL *rollback) {
|
|
||||||
[db executeUpdate:@"DROP TABLE unread_hashes"];
|
|
||||||
[_self setupDatabase:db];
|
|
||||||
NSDictionary *hashes = [results objectForKey:@"unread_feed_story_hashes"];
|
|
||||||
for (NSString *feed in [hashes allKeys]) {
|
|
||||||
NSArray *story_hashes = [hashes objectForKey:feed];
|
|
||||||
for (NSArray *story_hash_tuple in story_hashes) {
|
|
||||||
[db executeUpdate:@"INSERT into unread_hashes"
|
|
||||||
"(story_feed_id, story_hash, story_timestamp) VALUES "
|
|
||||||
"(?, ?, ?)",
|
|
||||||
feed,
|
|
||||||
[story_hash_tuple objectAtIndex:0],
|
|
||||||
[story_hash_tuple objectAtIndex:1]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}];
|
|
||||||
[_self.database inTransaction:^(FMDatabase *db, BOOL *rollback) {
|
|
||||||
// Once all unread hashes are in, only keep under preference for offline limit
|
|
||||||
NSInteger offlineLimit = [[NSUserDefaults standardUserDefaults] integerForKey:@"offline_store_limit"];
|
|
||||||
NSString *order;
|
|
||||||
NSString *orderComp;
|
|
||||||
if ([[[NSUserDefaults standardUserDefaults] objectForKey:@"default_order"] isEqualToString:@"oldest"]) {
|
|
||||||
order = @"ASC";
|
|
||||||
orderComp = @">";
|
|
||||||
} else {
|
|
||||||
order = @"DESC";
|
|
||||||
orderComp = @"<";
|
|
||||||
}
|
|
||||||
FMResultSet *cursor = [db executeQuery:[NSString stringWithFormat:@"SELECT story_timestamp FROM unread_hashes ORDER BY story_timestamp %@ LIMIT 1 OFFSET %d", order, offlineLimit]];
|
|
||||||
int offlineLimitTimestamp = 0;
|
|
||||||
while ([cursor next]) {
|
|
||||||
offlineLimitTimestamp = [cursor intForColumn:@"story_timestamp"];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
NSLog(@"Deleting stories over limit: %d - %d", offlineLimit, offlineLimitTimestamp);
|
|
||||||
[db executeUpdate:[NSString stringWithFormat:@"DELETE FROM unread_hashes WHERE story_timestamp %@ %d", orderComp, offlineLimitTimestamp]];
|
|
||||||
}];
|
|
||||||
|
|
||||||
_self.totalUnfetchedStoryCount = 0;
|
|
||||||
_self.remainingUnfetchedStoryCount = 0;
|
|
||||||
_self.latestFetchedStoryDate = 0;
|
|
||||||
_self.totalUncachedImagesCount = 0;
|
|
||||||
_self.remainingUncachedImagesCount = 0;
|
|
||||||
[_self fetchAllUnreadStories];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
- (NSArray *)unfetchedStoryHashes {
|
|
||||||
NSMutableArray *hashes = [NSMutableArray array];
|
|
||||||
|
|
||||||
[self.database inDatabase:^(FMDatabase *db) {
|
|
||||||
NSString *commonQuery = @"FROM unread_hashes u "
|
|
||||||
"LEFT OUTER JOIN stories s ON (s.story_hash = u.story_hash) "
|
|
||||||
"WHERE s.story_hash IS NULL";
|
|
||||||
int count = [db intForQuery:[NSString stringWithFormat:@"SELECT COUNT(1) %@", commonQuery]];
|
|
||||||
if (self.totalUnfetchedStoryCount == 0) {
|
|
||||||
self.totalUnfetchedStoryCount = count;
|
|
||||||
self.remainingUnfetchedStoryCount = self.totalUnfetchedStoryCount;
|
|
||||||
} else {
|
|
||||||
self.remainingUnfetchedStoryCount = count;
|
|
||||||
}
|
|
||||||
|
|
||||||
int limit = 100;
|
|
||||||
NSString *order;
|
|
||||||
if ([[[NSUserDefaults standardUserDefaults] objectForKey:@"default_order"] isEqualToString:@"oldest"]) {
|
|
||||||
order = @"ASC";
|
|
||||||
} else {
|
|
||||||
order = @"DESC";
|
|
||||||
}
|
|
||||||
FMResultSet *cursor = [db executeQuery:[NSString stringWithFormat:@"SELECT u.story_hash %@ ORDER BY u.story_timestamp %@ LIMIT %d", commonQuery, order, limit]];
|
|
||||||
|
|
||||||
while ([cursor next]) {
|
|
||||||
[hashes addObject:[cursor objectForColumnName:@"story_hash"]];
|
|
||||||
}
|
|
||||||
int start = (int)[[NSDate date] timeIntervalSince1970];
|
|
||||||
int end = self.latestFetchedStoryDate;
|
|
||||||
int seconds = start - (end ? end : start);
|
|
||||||
__block int hours = (int)round(seconds / 60.f / 60.f);
|
|
||||||
|
|
||||||
__block float progress = 0.f;
|
|
||||||
if (self.totalUnfetchedStoryCount) {
|
|
||||||
progress = 1.f - ((float)self.remainingUnfetchedStoryCount /
|
|
||||||
(float)self.totalUnfetchedStoryCount);
|
|
||||||
}
|
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
|
||||||
if (![[[NSUserDefaults standardUserDefaults]
|
|
||||||
objectForKey:@"offline_allowed"] boolValue]) {
|
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
|
||||||
[self.feedsViewController showDoneNotifier];
|
|
||||||
[self.feedsViewController hideNotifier];
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
[self.feedsViewController showSyncingNotifier:progress hoursBack:hours];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}];
|
|
||||||
|
|
||||||
return hashes;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)fetchAllUnreadStories {
|
|
||||||
NSArray *hashes = [self unfetchedStoryHashes];
|
|
||||||
|
|
||||||
if (![[[NSUserDefaults standardUserDefaults]
|
|
||||||
objectForKey:@"offline_allowed"] boolValue]) {
|
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
|
||||||
[self.feedsViewController hideNotifier];
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
} else if ([hashes count] == 0) {
|
|
||||||
NSLog(@"Finished downloading unread stories. %d total", self.totalUnfetchedStoryCount);
|
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
|
||||||
if (![[[NSUserDefaults standardUserDefaults]
|
|
||||||
objectForKey:@"offline_image_download"] boolValue]) {
|
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
|
||||||
[self.feedsViewController showDoneNotifier];
|
|
||||||
[self.feedsViewController hideNotifier];
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
[self.feedsViewController showCachingNotifier:0 hoursBack:1];
|
|
||||||
[self fetchAllUncachedImages];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"%@/reader/river_stories?page=0&h=%@",
|
|
||||||
NEWSBLUR_URL, [hashes componentsJoinedByString:@"&h="]]];
|
|
||||||
ASIHTTPRequest *_request = [ASIHTTPRequest requestWithURL:url];
|
|
||||||
__weak ASIHTTPRequest *request = _request;
|
|
||||||
[request setResponseEncoding:NSUTF8StringEncoding];
|
|
||||||
[request setDefaultResponseEncoding:NSUTF8StringEncoding];
|
|
||||||
[request setFailedBlock:^(void) {
|
|
||||||
NSLog(@"Failed fetch all unreads.");
|
|
||||||
}];
|
|
||||||
[request setCompletionBlock:^(void) {
|
|
||||||
[self storeAllUnreadStories:request];
|
|
||||||
}];
|
|
||||||
[request setTimeOutSeconds:30];
|
|
||||||
[request startAsynchronous];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)storeAllUnreadStories:(ASIHTTPRequest *)request {
|
|
||||||
NSString *responseString = [request responseString];
|
|
||||||
NSData *responseData=[responseString dataUsingEncoding:NSUTF8StringEncoding];
|
|
||||||
NSError *error;
|
|
||||||
NSDictionary *results = [NSJSONSerialization
|
|
||||||
JSONObjectWithData:responseData
|
|
||||||
options:kNilOptions
|
|
||||||
error:&error];
|
|
||||||
__block BOOL anySuccess = NO;
|
|
||||||
__block __typeof__(self) _self = self;
|
|
||||||
|
|
||||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,
|
|
||||||
(unsigned long)NULL), ^(void) {
|
|
||||||
[_self.database inTransaction:^(FMDatabase *db, BOOL *rollback) {
|
|
||||||
for (NSDictionary *story in [results objectForKey:@"stories"]) {
|
|
||||||
BOOL inserted = [db executeUpdate:@"INSERT into stories "
|
|
||||||
"(story_feed_id, story_hash, story_timestamp, story_json) VALUES "
|
|
||||||
"(?, ?, ?, ?)",
|
|
||||||
[story objectForKey:@"story_feed_id"],
|
|
||||||
[story objectForKey:@"story_hash"],
|
|
||||||
[story objectForKey:@"story_timestamp"],
|
|
||||||
[story JSONRepresentation]
|
|
||||||
];
|
|
||||||
if ([[story objectForKey:@"image_urls"] class] != [NSNull class] &&
|
|
||||||
[[story objectForKey:@"image_urls"] count]) {
|
|
||||||
for (NSString *imageUrl in [story objectForKey:@"image_urls"]) {
|
|
||||||
[db executeUpdate:@"INSERT INTO cached_images "
|
|
||||||
"(story_feed_id, story_hash, image_url) VALUES "
|
|
||||||
"(?, ?, ?)",
|
|
||||||
[story objectForKey:@"story_feed_id"],
|
|
||||||
[story objectForKey:@"story_hash"],
|
|
||||||
imageUrl
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!anySuccess && inserted) anySuccess = YES;
|
|
||||||
}
|
|
||||||
if (anySuccess) {
|
|
||||||
_self.latestFetchedStoryDate = [[[[results objectForKey:@"stories"] lastObject]
|
|
||||||
objectForKey:@"story_timestamp"] intValue];
|
|
||||||
}
|
|
||||||
}];
|
|
||||||
|
|
||||||
if (anySuccess) {
|
|
||||||
[_self fetchAllUnreadStories];
|
|
||||||
} else {
|
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
|
||||||
[_self.feedsViewController hideNotifier];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#pragma mark -- Offline - Image Cache
|
|
||||||
|
|
||||||
- (NSArray *)uncachedImageUrls {
|
|
||||||
NSMutableArray *urls = [NSMutableArray array];
|
|
||||||
|
|
||||||
[self.database inDatabase:^(FMDatabase *db) {
|
|
||||||
NSString *commonQuery = @"FROM cached_images c "
|
|
||||||
"INNER JOIN unread_hashes u ON (c.story_hash = u.story_hash) "
|
|
||||||
"WHERE c.image_cached is null ";
|
|
||||||
int count = [db intForQuery:[NSString stringWithFormat:@"SELECT COUNT(1) %@", commonQuery]];
|
|
||||||
if (self.totalUncachedImagesCount == 0) {
|
|
||||||
self.totalUncachedImagesCount = count;
|
|
||||||
self.remainingUncachedImagesCount = self.totalUncachedImagesCount;
|
|
||||||
} else {
|
|
||||||
self.remainingUncachedImagesCount = count;
|
|
||||||
}
|
|
||||||
|
|
||||||
int limit = 96;
|
|
||||||
NSString *order;
|
|
||||||
if ([[[NSUserDefaults standardUserDefaults] objectForKey:@"default_order"] isEqualToString:@"oldest"]) {
|
|
||||||
order = @"ASC";
|
|
||||||
} else {
|
|
||||||
order = @"DESC";
|
|
||||||
}
|
|
||||||
NSString *sql = [NSString stringWithFormat:@"SELECT c.image_url, c.story_hash, u.story_timestamp %@ ORDER BY u.story_timestamp %@ LIMIT %d", commonQuery, order, limit];
|
|
||||||
FMResultSet *cursor = [db executeQuery:sql];
|
|
||||||
|
|
||||||
while ([cursor next]) {
|
|
||||||
[urls addObject:@[[cursor objectForColumnName:@"image_url"],
|
|
||||||
[cursor objectForColumnName:@"story_hash"],
|
|
||||||
[cursor objectForColumnName:@"story_timestamp"]]];
|
|
||||||
}
|
|
||||||
int start = (int)[[NSDate date] timeIntervalSince1970];
|
|
||||||
int end = [[[urls lastObject] objectAtIndex:2] intValue];
|
|
||||||
int seconds = start - (end ? end : start);
|
|
||||||
__block int hours = (int)round(seconds / 60.f / 60.f);
|
|
||||||
|
|
||||||
__block float progress = 0.f;
|
|
||||||
if (self.totalUncachedImagesCount) {
|
|
||||||
progress = 1.f - ((float)self.remainingUncachedImagesCount /
|
|
||||||
(float)self.totalUncachedImagesCount);
|
|
||||||
}
|
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
|
||||||
[self.feedsViewController showCachingNotifier:progress hoursBack:hours];
|
|
||||||
});
|
|
||||||
}];
|
|
||||||
|
|
||||||
return urls;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)deleteAllCachedImages {
|
|
||||||
NSFileManager *fileManager = [[NSFileManager alloc] init];
|
|
||||||
NSError *error = nil;
|
|
||||||
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
|
|
||||||
NSString *cacheDirectory = [[paths objectAtIndex:0] stringByAppendingPathComponent:@"story_images"];
|
|
||||||
NSArray *directoryContents = [fileManager contentsOfDirectoryAtPath:cacheDirectory error:&error];
|
|
||||||
int removed = 0;
|
|
||||||
|
|
||||||
if (error == nil) {
|
|
||||||
for (NSString *path in directoryContents) {
|
|
||||||
NSString *fullPath = [cacheDirectory stringByAppendingPathComponent:path];
|
|
||||||
BOOL removeSuccess = [fileManager removeItemAtPath:fullPath error:&error];
|
|
||||||
removed++;
|
|
||||||
if (!removeSuccess) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NSLog(@"Deleted %d images.", removed);
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)fetchAllUncachedImages {
|
|
||||||
NSArray *urls = [self uncachedImageUrls];
|
|
||||||
operationQueue = [[ASINetworkQueue alloc] init];
|
|
||||||
operationQueue.maxConcurrentOperationCount = [[NSUserDefaults standardUserDefaults]
|
|
||||||
integerForKey:@"offline_image_concurrency"];
|
|
||||||
operationQueue.delegate = self;
|
|
||||||
|
|
||||||
if (![[[NSUserDefaults standardUserDefaults]
|
|
||||||
objectForKey:@"offline_image_download"] boolValue] ||
|
|
||||||
[urls count] == 0) {
|
|
||||||
NSLog(@"Finished caching images. %d total", self.totalUncachedImagesCount);
|
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
|
||||||
[self.feedsViewController showDoneNotifier];
|
|
||||||
[self.feedsViewController hideNotifier];
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (NSArray *urlArray in urls) {
|
|
||||||
NSURL *url = [NSURL URLWithString:[urlArray objectAtIndex:0]];
|
|
||||||
NSString *storyHash = [urlArray objectAtIndex:1];
|
|
||||||
|
|
||||||
ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
|
|
||||||
[request setUserInfo:@{@"story_hash": storyHash}];
|
|
||||||
[request setDelegate:self];
|
|
||||||
[request setDidFinishSelector:@selector(storeCachedImage:)];
|
|
||||||
[request setDidFailSelector:@selector(storeCachedImage:)];
|
|
||||||
[request setTimeOutSeconds:5];
|
|
||||||
[operationQueue addOperation:request];
|
|
||||||
}
|
|
||||||
|
|
||||||
[operationQueue setQueueDidFinishSelector:@selector(cachedImageQueueFinished:)];
|
|
||||||
[operationQueue setShouldCancelAllRequestsOnFailure:NO];
|
|
||||||
[operationQueue go];
|
|
||||||
|
|
||||||
// dispatch_async(dispatch_get_main_queue(), ^{
|
|
||||||
// [self.feedsViewController hideNotifier];
|
|
||||||
// });
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)storeCachedImage:(ASIHTTPRequest *)request {
|
|
||||||
NSString *storyHash = [[request userInfo] objectForKey:@"story_hash"];
|
|
||||||
|
|
||||||
if ([request responseStatusCode] == 200) {
|
|
||||||
NSData *responseData = [request responseData];
|
|
||||||
NSString *md5Url = [Utilities md5:[[request originalURL] absoluteString]];
|
|
||||||
NSLog(@"Storing image: %@ (%d bytes - %d in queue)", storyHash, [responseData length], [operationQueue requestsCount]);
|
|
||||||
if ([responseData length] <= 43) {
|
|
||||||
NSLog(@" ---> Image url: %@", [request url]);
|
|
||||||
}
|
|
||||||
|
|
||||||
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
||||||
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
|
|
||||||
NSString *cacheDirectory = [[paths objectAtIndex:0] stringByAppendingPathComponent:@"story_images"];
|
|
||||||
NSString *fullPath = [cacheDirectory stringByAppendingPathComponent:[NSString stringWithFormat:@"%@", md5Url]];
|
|
||||||
|
|
||||||
[fileManager createFileAtPath:fullPath contents:responseData attributes:nil];
|
|
||||||
} else {
|
|
||||||
NSLog(@"Failed to fetch: %@ / %@", [[request originalURL] absoluteString], storyHash);
|
|
||||||
}
|
|
||||||
|
|
||||||
[self.database inDatabase:^(FMDatabase *db) {
|
|
||||||
[db executeUpdate:@"UPDATE cached_images SET "
|
|
||||||
"image_cached = 1 WHERE story_hash = ?",
|
|
||||||
storyHash];
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)cachedImageQueueFinished:(ASINetworkQueue *)queue {
|
|
||||||
NSLog(@"Queue finished: %d total (%d remaining)", self.totalUncachedImagesCount, self.remainingUncachedImagesCount);
|
|
||||||
[self fetchAllUncachedImages];
|
|
||||||
// dispatch_async(dispatch_get_main_queue(), ^{
|
|
||||||
// [self.feedsViewController hideNotifier];
|
|
||||||
// });
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)flushOldCachedImages {
|
|
||||||
int deleted = 0;
|
|
||||||
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
||||||
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
|
|
||||||
NSString *cacheDirectory = [[paths objectAtIndex:0] stringByAppendingPathComponent:@"story_images"];
|
|
||||||
NSDirectoryEnumerator* en = [fileManager enumeratorAtPath:cacheDirectory];
|
|
||||||
|
|
||||||
NSString* file;
|
|
||||||
while (file = [en nextObject])
|
|
||||||
{
|
|
||||||
NSError *error = nil;
|
|
||||||
NSString *filepath = [NSString stringWithFormat:[cacheDirectory stringByAppendingString:@"/%@"],file];
|
|
||||||
NSDate *creationDate = [[fileManager attributesOfItemAtPath:filepath error:nil] fileCreationDate];
|
|
||||||
NSDate *d = [[NSDate date] dateByAddingTimeInterval:-14*24*60*60];
|
|
||||||
NSDateFormatter *df = [[NSDateFormatter alloc] init]; // = [NSDateFormatter initWithDateFormat:@"yyyy-MM-dd"];
|
|
||||||
[df setDateFormat:@"EEEE d"];
|
|
||||||
|
|
||||||
if ([creationDate compare:d] == NSOrderedAscending) {
|
|
||||||
[[NSFileManager defaultManager]
|
|
||||||
removeItemAtPath:[cacheDirectory stringByAppendingPathComponent:file]
|
|
||||||
error:&error];
|
|
||||||
deleted += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
NSLog(@"Deleted %d old cached images", deleted);
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)prepareActiveCachedImages:(FMDatabase *)db {
|
- (void)prepareActiveCachedImages:(FMDatabase *)db {
|
||||||
activeCachedImages = [NSMutableDictionary dictionary];
|
activeCachedImages = [NSMutableDictionary dictionary];
|
||||||
NSDate *start = [NSDate date];
|
NSDate *start = [NSDate date];
|
||||||
|
@ -2718,6 +2340,55 @@
|
||||||
NSLog(@"prepareActiveCachedImages time: %f", ([[NSDate date] timeIntervalSinceDate:start]));
|
NSLog(@"prepareActiveCachedImages time: %f", ([[NSDate date] timeIntervalSinceDate:start]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)flushOldCachedImages {
|
||||||
|
int deleted = 0;
|
||||||
|
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||||||
|
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
|
||||||
|
NSString *cacheDirectory = [[paths objectAtIndex:0] stringByAppendingPathComponent:@"story_images"];
|
||||||
|
NSDirectoryEnumerator* en = [fileManager enumeratorAtPath:cacheDirectory];
|
||||||
|
|
||||||
|
NSString* file;
|
||||||
|
while (file = [en nextObject])
|
||||||
|
{
|
||||||
|
NSError *error = nil;
|
||||||
|
NSString *filepath = [NSString stringWithFormat:[cacheDirectory stringByAppendingString:@"/%@"],file];
|
||||||
|
NSDate *creationDate = [[fileManager attributesOfItemAtPath:filepath error:nil] fileCreationDate];
|
||||||
|
NSDate *d = [[NSDate date] dateByAddingTimeInterval:-14*24*60*60];
|
||||||
|
NSDateFormatter *df = [[NSDateFormatter alloc] init]; // = [NSDateFormatter initWithDateFormat:@"yyyy-MM-dd"];
|
||||||
|
[df setDateFormat:@"EEEE d"];
|
||||||
|
|
||||||
|
if ([creationDate compare:d] == NSOrderedAscending) {
|
||||||
|
[[NSFileManager defaultManager]
|
||||||
|
removeItemAtPath:[cacheDirectory stringByAppendingPathComponent:file]
|
||||||
|
error:&error];
|
||||||
|
deleted += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NSLog(@"Deleted %d old cached images", deleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)deleteAllCachedImages {
|
||||||
|
NSFileManager *fileManager = [[NSFileManager alloc] init];
|
||||||
|
NSError *error = nil;
|
||||||
|
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
|
||||||
|
NSString *cacheDirectory = [[paths objectAtIndex:0] stringByAppendingPathComponent:@"story_images"];
|
||||||
|
NSArray *directoryContents = [fileManager contentsOfDirectoryAtPath:cacheDirectory error:&error];
|
||||||
|
int removed = 0;
|
||||||
|
|
||||||
|
if (error == nil) {
|
||||||
|
for (NSString *path in directoryContents) {
|
||||||
|
NSString *fullPath = [cacheDirectory stringByAppendingPathComponent:path];
|
||||||
|
BOOL removeSuccess = [fileManager removeItemAtPath:fullPath error:&error];
|
||||||
|
removed++;
|
||||||
|
if (!removeSuccess) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NSLog(@"Deleted %d images.", removed);
|
||||||
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
#pragma mark -
|
#pragma mark -
|
||||||
|
|
|
@ -748,7 +748,7 @@ static const CGFloat kFolderTitleHeight = 28;
|
||||||
if (self.inPullToRefresh_) {
|
if (self.inPullToRefresh_) {
|
||||||
self.inPullToRefresh_ = NO;
|
self.inPullToRefresh_ = NO;
|
||||||
[self.appDelegate flushQueuedReadStories:YES withCallback:^{
|
[self.appDelegate flushQueuedReadStories:YES withCallback:^{
|
||||||
[self.appDelegate fetchUnreadHashes];
|
[self.appDelegate startOfflineQueue];
|
||||||
}];
|
}];
|
||||||
} else {
|
} else {
|
||||||
[self.appDelegate flushQueuedReadStories:YES withCallback:^{
|
[self.appDelegate flushQueuedReadStories:YES withCallback:^{
|
||||||
|
@ -1533,7 +1533,6 @@ heightForHeaderInSection:(NSInteger)section {
|
||||||
[request startAsynchronous];
|
[request startAsynchronous];
|
||||||
|
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
[self hideNotifier];
|
|
||||||
[self showCountingNotifier];
|
[self showCountingNotifier];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1614,7 +1613,7 @@ heightForHeaderInSection:(NSInteger)section {
|
||||||
[appDelegate.folderCountCache removeAllObjects];
|
[appDelegate.folderCountCache removeAllObjects];
|
||||||
[self.feedTitlesTable reloadData];
|
[self.feedTitlesTable reloadData];
|
||||||
[self refreshHeaderCounts];
|
[self refreshHeaderCounts];
|
||||||
[self.appDelegate fetchUnreadHashes];
|
[self.appDelegate startOfflineQueue];
|
||||||
}
|
}
|
||||||
|
|
||||||
// called when the date shown needs to be updated, optional
|
// called when the date shown needs to be updated, optional
|
||||||
|
|
23
clients/ios/Classes/offline/OfflineFetchImages.h
Normal file
23
clients/ios/Classes/offline/OfflineFetchImages.h
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
//
|
||||||
|
// OfflineFetchImages.h
|
||||||
|
// NewsBlur
|
||||||
|
//
|
||||||
|
// Created by Samuel Clay on 7/15/13.
|
||||||
|
// Copyright (c) 2013 NewsBlur. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
#import "NewsBlurAppDelegate.h"
|
||||||
|
#import "FMDatabaseQueue.h"
|
||||||
|
#import "ASINetworkQueue.h"
|
||||||
|
|
||||||
|
@interface OfflineFetchImages : NSOperation
|
||||||
|
|
||||||
|
@property (nonatomic) NewsBlurAppDelegate *appDelegate;
|
||||||
|
@property (readwrite) ASINetworkQueue *imageDownloadOperationQueue;
|
||||||
|
|
||||||
|
- (NSArray *)uncachedImageUrls;
|
||||||
|
- (void)storeCachedImage:(ASIHTTPRequest *)request;
|
||||||
|
- (void)cachedImageQueueFinished:(ASINetworkQueue *)queue;
|
||||||
|
|
||||||
|
@end
|
157
clients/ios/Classes/offline/OfflineFetchImages.m
Normal file
157
clients/ios/Classes/offline/OfflineFetchImages.m
Normal file
|
@ -0,0 +1,157 @@
|
||||||
|
//
|
||||||
|
// OfflineFetchImages.m
|
||||||
|
// NewsBlur
|
||||||
|
//
|
||||||
|
// Created by Samuel Clay on 7/15/13.
|
||||||
|
// Copyright (c) 2013 NewsBlur. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "OfflineFetchImages.h"
|
||||||
|
#import "NewsBlurAppDelegate.h"
|
||||||
|
#import "NewsBlurViewController.h"
|
||||||
|
#import "FMDatabase.h"
|
||||||
|
#import "FMDatabaseAdditions.h"
|
||||||
|
#import "Utilities.h"
|
||||||
|
|
||||||
|
@implementation OfflineFetchImages
|
||||||
|
@synthesize imageDownloadOperationQueue;
|
||||||
|
@synthesize appDelegate;
|
||||||
|
|
||||||
|
- (void)main {
|
||||||
|
[self fetchImages];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)fetchImages {
|
||||||
|
if (self.isCancelled) return;
|
||||||
|
|
||||||
|
NSLog(@"Fetching images...");
|
||||||
|
appDelegate = [NewsBlurAppDelegate sharedAppDelegate];
|
||||||
|
NSArray *urls = [self uncachedImageUrls];
|
||||||
|
|
||||||
|
imageDownloadOperationQueue = [[ASINetworkQueue alloc] init];
|
||||||
|
imageDownloadOperationQueue.maxConcurrentOperationCount = [[NSUserDefaults standardUserDefaults]
|
||||||
|
integerForKey:@"offline_image_concurrency"];
|
||||||
|
imageDownloadOperationQueue.delegate = self;
|
||||||
|
|
||||||
|
if (![[[NSUserDefaults standardUserDefaults]
|
||||||
|
objectForKey:@"offline_image_download"] boolValue] ||
|
||||||
|
[urls count] == 0) {
|
||||||
|
NSLog(@"Finished caching images. %d total", appDelegate.totalUncachedImagesCount);
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
[appDelegate.feedsViewController showDoneNotifier];
|
||||||
|
[appDelegate.feedsViewController hideNotifier];
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSMutableArray *downloadRequests = [NSMutableArray array];
|
||||||
|
for (NSArray *urlArray in urls) {
|
||||||
|
NSURL *url = [NSURL URLWithString:[urlArray objectAtIndex:0]];
|
||||||
|
NSString *storyHash = [urlArray objectAtIndex:1];
|
||||||
|
|
||||||
|
ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
|
||||||
|
[request setUserInfo:@{@"story_hash": storyHash}];
|
||||||
|
[request setDelegate:self];
|
||||||
|
[request setDidFinishSelector:@selector(storeCachedImage:)];
|
||||||
|
[request setDidFailSelector:@selector(storeCachedImage:)];
|
||||||
|
[request setTimeOutSeconds:5];
|
||||||
|
[downloadRequests addObject:request];
|
||||||
|
}
|
||||||
|
[imageDownloadOperationQueue addOperations:downloadRequests waitUntilFinished:YES];
|
||||||
|
|
||||||
|
[imageDownloadOperationQueue setQueueDidFinishSelector:@selector(cachedImageQueueFinished:)];
|
||||||
|
[imageDownloadOperationQueue setShouldCancelAllRequestsOnFailure:NO];
|
||||||
|
[imageDownloadOperationQueue go];
|
||||||
|
|
||||||
|
// dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
// [appDelegate.feedsViewController hideNotifier];
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSArray *)uncachedImageUrls {
|
||||||
|
NSMutableArray *urls = [NSMutableArray array];
|
||||||
|
|
||||||
|
[appDelegate.database inDatabase:^(FMDatabase *db) {
|
||||||
|
NSString *commonQuery = @"FROM cached_images c "
|
||||||
|
"INNER JOIN unread_hashes u ON (c.story_hash = u.story_hash) "
|
||||||
|
"WHERE c.image_cached is null ";
|
||||||
|
int count = [db intForQuery:[NSString stringWithFormat:@"SELECT COUNT(1) %@", commonQuery]];
|
||||||
|
if (appDelegate.totalUncachedImagesCount == 0) {
|
||||||
|
appDelegate.totalUncachedImagesCount = count;
|
||||||
|
appDelegate.remainingUncachedImagesCount = appDelegate.totalUncachedImagesCount;
|
||||||
|
} else {
|
||||||
|
appDelegate.remainingUncachedImagesCount = count;
|
||||||
|
}
|
||||||
|
|
||||||
|
int limit = 96;
|
||||||
|
NSString *order;
|
||||||
|
if ([[[NSUserDefaults standardUserDefaults] objectForKey:@"default_order"] isEqualToString:@"oldest"]) {
|
||||||
|
order = @"ASC";
|
||||||
|
} else {
|
||||||
|
order = @"DESC";
|
||||||
|
}
|
||||||
|
NSString *sql = [NSString stringWithFormat:@"SELECT c.image_url, c.story_hash, u.story_timestamp %@ ORDER BY u.story_timestamp %@ LIMIT %d", commonQuery, order, limit];
|
||||||
|
FMResultSet *cursor = [db executeQuery:sql];
|
||||||
|
|
||||||
|
while ([cursor next]) {
|
||||||
|
[urls addObject:@[[cursor objectForColumnName:@"image_url"],
|
||||||
|
[cursor objectForColumnName:@"story_hash"],
|
||||||
|
[cursor objectForColumnName:@"story_timestamp"]]];
|
||||||
|
}
|
||||||
|
int start = (int)[[NSDate date] timeIntervalSince1970];
|
||||||
|
int end = [[[urls lastObject] objectAtIndex:2] intValue];
|
||||||
|
int seconds = start - (end ? end : start);
|
||||||
|
__block int hours = (int)round(seconds / 60.f / 60.f);
|
||||||
|
|
||||||
|
__block float progress = 0.f;
|
||||||
|
if (appDelegate.totalUncachedImagesCount) {
|
||||||
|
progress = 1.f - ((float)appDelegate.remainingUncachedImagesCount /
|
||||||
|
(float)appDelegate.totalUncachedImagesCount);
|
||||||
|
}
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
[appDelegate.feedsViewController showCachingNotifier:progress hoursBack:hours];
|
||||||
|
});
|
||||||
|
}];
|
||||||
|
|
||||||
|
return urls;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)storeCachedImage:(ASIHTTPRequest *)request {
|
||||||
|
if (self.isCancelled) return;
|
||||||
|
|
||||||
|
NSString *storyHash = [[request userInfo] objectForKey:@"story_hash"];
|
||||||
|
|
||||||
|
if ([request responseStatusCode] == 200) {
|
||||||
|
NSData *responseData = [request responseData];
|
||||||
|
NSString *md5Url = [Utilities md5:[[request originalURL] absoluteString]];
|
||||||
|
NSLog(@"Storing image: %@ (%d bytes - %d in queue)", storyHash, [responseData length], [imageDownloadOperationQueue requestsCount]);
|
||||||
|
if ([responseData length] <= 43) {
|
||||||
|
NSLog(@" ---> Image url: %@", [request url]);
|
||||||
|
}
|
||||||
|
|
||||||
|
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||||||
|
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
|
||||||
|
NSString *cacheDirectory = [[paths objectAtIndex:0] stringByAppendingPathComponent:@"story_images"];
|
||||||
|
NSString *fullPath = [cacheDirectory stringByAppendingPathComponent:[NSString stringWithFormat:@"%@", md5Url]];
|
||||||
|
|
||||||
|
[fileManager createFileAtPath:fullPath contents:responseData attributes:nil];
|
||||||
|
} else {
|
||||||
|
NSLog(@"Failed to fetch: %@ / %@", [[request originalURL] absoluteString], storyHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
[appDelegate.database inDatabase:^(FMDatabase *db) {
|
||||||
|
[db executeUpdate:@"UPDATE cached_images SET "
|
||||||
|
"image_cached = 1 WHERE story_hash = ?",
|
||||||
|
storyHash];
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)cachedImageQueueFinished:(ASINetworkQueue *)queue {
|
||||||
|
NSLog(@"Queue finished: %d total (%d remaining)", appDelegate.totalUncachedImagesCount, appDelegate.remainingUncachedImagesCount);
|
||||||
|
[self fetchImages];
|
||||||
|
// dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
// [self.feedsViewController hideNotifier];
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
19
clients/ios/Classes/offline/OfflineFetchStories.h
Normal file
19
clients/ios/Classes/offline/OfflineFetchStories.h
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
//
|
||||||
|
// OfflineFetchStories.h
|
||||||
|
// NewsBlur
|
||||||
|
//
|
||||||
|
// Created by Samuel Clay on 7/15/13.
|
||||||
|
// Copyright (c) 2013 NewsBlur. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
#import "NewsBlurAppDelegate.h"
|
||||||
|
|
||||||
|
@interface OfflineFetchStories : NSOperation
|
||||||
|
|
||||||
|
@property (nonatomic) NewsBlurAppDelegate *appDelegate;
|
||||||
|
|
||||||
|
- (NSArray *)unfetchedStoryHashes;
|
||||||
|
- (void)storeAllUnreadStories:(NSDictionary *)results;
|
||||||
|
|
||||||
|
@end
|
159
clients/ios/Classes/offline/OfflineFetchStories.m
Normal file
159
clients/ios/Classes/offline/OfflineFetchStories.m
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
//
|
||||||
|
// OfflineFetchStories.m
|
||||||
|
// NewsBlur
|
||||||
|
//
|
||||||
|
// Created by Samuel Clay on 7/15/13.
|
||||||
|
// Copyright (c) 2013 NewsBlur. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "OfflineFetchStories.h"
|
||||||
|
#import "NewsBlurAppDelegate.h"
|
||||||
|
#import "NewsBlurViewController.h"
|
||||||
|
#import "FMDatabase.h"
|
||||||
|
#import "FMDatabaseAdditions.h"
|
||||||
|
#import "AFJSONRequestOperation.h"
|
||||||
|
#import "JSON.h"
|
||||||
|
|
||||||
|
@implementation OfflineFetchStories
|
||||||
|
|
||||||
|
@synthesize appDelegate;
|
||||||
|
|
||||||
|
- (void)main {
|
||||||
|
[self fetchStories];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)fetchStories {
|
||||||
|
if (self.isCancelled) return;
|
||||||
|
|
||||||
|
NSLog(@"Fetching Stories...");
|
||||||
|
appDelegate = [NewsBlurAppDelegate sharedAppDelegate];
|
||||||
|
NSArray *hashes = [self unfetchedStoryHashes];
|
||||||
|
|
||||||
|
if (![[[NSUserDefaults standardUserDefaults]
|
||||||
|
objectForKey:@"offline_allowed"] boolValue]) {
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
[appDelegate.feedsViewController hideNotifier];
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
} else if ([hashes count] == 0) {
|
||||||
|
NSLog(@"Finished downloading unread stories. %d total", appDelegate.totalUnfetchedStoryCount);
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
if (![[[NSUserDefaults standardUserDefaults]
|
||||||
|
objectForKey:@"offline_image_download"] boolValue]) {
|
||||||
|
[appDelegate.feedsViewController showDoneNotifier];
|
||||||
|
[appDelegate.feedsViewController hideNotifier];
|
||||||
|
} else {
|
||||||
|
[appDelegate.feedsViewController showCachingNotifier:0 hoursBack:1];
|
||||||
|
[appDelegate startOfflineFetchImages];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"%@/reader/river_stories?page=0&h=%@",
|
||||||
|
NEWSBLUR_URL, [hashes componentsJoinedByString:@"&h="]]];
|
||||||
|
AFJSONRequestOperation *request = [AFJSONRequestOperation
|
||||||
|
JSONRequestOperationWithRequest:[NSURLRequest requestWithURL:url]
|
||||||
|
success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
|
||||||
|
[self storeAllUnreadStories:JSON];
|
||||||
|
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) {
|
||||||
|
NSLog(@"Failed fetch all unreads.");
|
||||||
|
}];
|
||||||
|
request.successCallbackQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,
|
||||||
|
(unsigned long)NULL);
|
||||||
|
[request start];
|
||||||
|
[request waitUntilFinished];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSArray *)unfetchedStoryHashes {
|
||||||
|
NSMutableArray *hashes = [NSMutableArray array];
|
||||||
|
|
||||||
|
[appDelegate.database inDatabase:^(FMDatabase *db) {
|
||||||
|
NSString *commonQuery = @"FROM unread_hashes u "
|
||||||
|
"LEFT OUTER JOIN stories s ON (s.story_hash = u.story_hash) "
|
||||||
|
"WHERE s.story_hash IS NULL";
|
||||||
|
NSLog(@"Checking unfetched hashes...");
|
||||||
|
int count = [db intForQuery:[NSString stringWithFormat:@"SELECT COUNT(1) %@", commonQuery]];
|
||||||
|
if (appDelegate.totalUnfetchedStoryCount == 0) {
|
||||||
|
appDelegate.totalUnfetchedStoryCount = count;
|
||||||
|
appDelegate.remainingUnfetchedStoryCount = appDelegate.totalUnfetchedStoryCount;
|
||||||
|
} else {
|
||||||
|
appDelegate.remainingUnfetchedStoryCount = count;
|
||||||
|
}
|
||||||
|
|
||||||
|
int limit = 100;
|
||||||
|
NSString *order;
|
||||||
|
if ([[[NSUserDefaults standardUserDefaults] objectForKey:@"default_order"] isEqualToString:@"oldest"]) {
|
||||||
|
order = @"ASC";
|
||||||
|
} else {
|
||||||
|
order = @"DESC";
|
||||||
|
}
|
||||||
|
FMResultSet *cursor = [db executeQuery:[NSString stringWithFormat:@"SELECT u.story_hash %@ ORDER BY u.story_timestamp %@ LIMIT %d", commonQuery, order, limit]];
|
||||||
|
|
||||||
|
while ([cursor next]) {
|
||||||
|
[hashes addObject:[cursor objectForColumnName:@"story_hash"]];
|
||||||
|
}
|
||||||
|
int start = (int)[[NSDate date] timeIntervalSince1970];
|
||||||
|
int end = appDelegate.latestFetchedStoryDate;
|
||||||
|
int seconds = start - (end ? end : start);
|
||||||
|
__block int hours = (int)round(seconds / 60.f / 60.f);
|
||||||
|
|
||||||
|
__block float progress = 0.f;
|
||||||
|
if (appDelegate.totalUnfetchedStoryCount) {
|
||||||
|
progress = 1.f - ((float)appDelegate.remainingUnfetchedStoryCount /
|
||||||
|
(float)appDelegate.totalUnfetchedStoryCount);
|
||||||
|
}
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
[appDelegate.feedsViewController showSyncingNotifier:progress hoursBack:hours];
|
||||||
|
});
|
||||||
|
}];
|
||||||
|
|
||||||
|
return hashes;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)storeAllUnreadStories:(NSDictionary *)results {
|
||||||
|
if (self.isCancelled) return;
|
||||||
|
|
||||||
|
__block BOOL anySuccess = NO;
|
||||||
|
|
||||||
|
[appDelegate.database inTransaction:^(FMDatabase *db, BOOL *rollback) {
|
||||||
|
for (NSDictionary *story in [results objectForKey:@"stories"]) {
|
||||||
|
BOOL inserted = [db executeUpdate:@"INSERT into stories "
|
||||||
|
"(story_feed_id, story_hash, story_timestamp, story_json) VALUES "
|
||||||
|
"(?, ?, ?, ?)",
|
||||||
|
[story objectForKey:@"story_feed_id"],
|
||||||
|
[story objectForKey:@"story_hash"],
|
||||||
|
[story objectForKey:@"story_timestamp"],
|
||||||
|
[story JSONRepresentation]
|
||||||
|
];
|
||||||
|
if ([[story objectForKey:@"image_urls"] class] != [NSNull class] &&
|
||||||
|
[[story objectForKey:@"image_urls"] count]) {
|
||||||
|
for (NSString *imageUrl in [story objectForKey:@"image_urls"]) {
|
||||||
|
[db executeUpdate:@"INSERT INTO cached_images "
|
||||||
|
"(story_feed_id, story_hash, image_url) VALUES "
|
||||||
|
"(?, ?, ?)",
|
||||||
|
[story objectForKey:@"story_feed_id"],
|
||||||
|
[story objectForKey:@"story_hash"],
|
||||||
|
imageUrl
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!anySuccess && inserted) anySuccess = YES;
|
||||||
|
}
|
||||||
|
if (anySuccess) {
|
||||||
|
appDelegate.latestFetchedStoryDate = [[[[results objectForKey:@"stories"] lastObject]
|
||||||
|
objectForKey:@"story_timestamp"] intValue];
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
|
||||||
|
if (anySuccess) {
|
||||||
|
[self fetchStories];
|
||||||
|
} else {
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
[appDelegate.feedsViewController hideNotifier];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@end
|
20
clients/ios/Classes/offline/OfflineSyncUnreads.h
Normal file
20
clients/ios/Classes/offline/OfflineSyncUnreads.h
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
//
|
||||||
|
// OfflineSyncUnreads.h
|
||||||
|
// NewsBlur
|
||||||
|
//
|
||||||
|
// Created by Samuel Clay on 7/15/13.
|
||||||
|
// Copyright (c) 2013 NewsBlur. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
#import "NewsBlurAppDelegate.h"
|
||||||
|
#import "FMDatabaseQueue.h"
|
||||||
|
#import "ASINetworkQueue.h"
|
||||||
|
|
||||||
|
@interface OfflineSyncUnreads : NSOperation
|
||||||
|
|
||||||
|
@property (nonatomic) NewsBlurAppDelegate *appDelegate;
|
||||||
|
|
||||||
|
- (void)storeUnreadHashes:(NSDictionary *)results;
|
||||||
|
|
||||||
|
@end
|
97
clients/ios/Classes/offline/OfflineSyncUnreads.m
Normal file
97
clients/ios/Classes/offline/OfflineSyncUnreads.m
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
//
|
||||||
|
// OfflineSyncUnreads.m
|
||||||
|
// NewsBlur
|
||||||
|
//
|
||||||
|
// Created by Samuel Clay on 7/15/13.
|
||||||
|
// Copyright (c) 2013 NewsBlur. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "OfflineSyncUnreads.h"
|
||||||
|
#import "NewsBlurAppDelegate.h"
|
||||||
|
#import "NewsBlurViewController.h"
|
||||||
|
#import "FMResultSet.h"
|
||||||
|
#import "FMDatabase.h"
|
||||||
|
#import "AFJSONRequestOperation.h"
|
||||||
|
|
||||||
|
@implementation OfflineSyncUnreads
|
||||||
|
|
||||||
|
@synthesize appDelegate;
|
||||||
|
|
||||||
|
- (void)main {
|
||||||
|
appDelegate = [NewsBlurAppDelegate sharedAppDelegate];
|
||||||
|
|
||||||
|
NSLog(@"Syncing Unreads...");
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
[appDelegate.feedsViewController showSyncingNotifier];
|
||||||
|
});
|
||||||
|
|
||||||
|
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"%@/reader/unread_story_hashes?include_timestamps=true",
|
||||||
|
NEWSBLUR_URL]];
|
||||||
|
AFJSONRequestOperation *request = [AFJSONRequestOperation
|
||||||
|
JSONRequestOperationWithRequest:[NSURLRequest requestWithURL:url]
|
||||||
|
success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
|
||||||
|
[self storeUnreadHashes:JSON];
|
||||||
|
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) {
|
||||||
|
NSLog(@"Failed fetch all story hashes.");
|
||||||
|
}];
|
||||||
|
request.successCallbackQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,
|
||||||
|
(unsigned long)NULL);
|
||||||
|
[request start];
|
||||||
|
[request waitUntilFinished];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)storeUnreadHashes:(NSDictionary *)results {
|
||||||
|
if (self.isCancelled) return;
|
||||||
|
|
||||||
|
[appDelegate.database inTransaction:^(FMDatabase *db, BOOL *rollback) {
|
||||||
|
NSLog(@"Storing unread story hashes...");
|
||||||
|
[db executeUpdate:@"DROP TABLE unread_hashes"];
|
||||||
|
[appDelegate setupDatabase:db];
|
||||||
|
NSDictionary *hashes = [results objectForKey:@"unread_feed_story_hashes"];
|
||||||
|
for (NSString *feed in [hashes allKeys]) {
|
||||||
|
NSArray *story_hashes = [hashes objectForKey:feed];
|
||||||
|
for (NSArray *story_hash_tuple in story_hashes) {
|
||||||
|
[db executeUpdate:@"INSERT into unread_hashes"
|
||||||
|
"(story_feed_id, story_hash, story_timestamp) VALUES "
|
||||||
|
"(?, ?, ?)",
|
||||||
|
feed,
|
||||||
|
[story_hash_tuple objectAtIndex:0],
|
||||||
|
[story_hash_tuple objectAtIndex:1]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}];
|
||||||
|
[appDelegate.database inTransaction:^(FMDatabase *db, BOOL *rollback) {
|
||||||
|
// Once all unread hashes are in, only keep under preference for offline limit
|
||||||
|
NSInteger offlineLimit = [[NSUserDefaults standardUserDefaults] integerForKey:@"offline_store_limit"];
|
||||||
|
NSString *order;
|
||||||
|
NSString *orderComp;
|
||||||
|
if ([[[NSUserDefaults standardUserDefaults] objectForKey:@"default_order"] isEqualToString:@"oldest"]) {
|
||||||
|
order = @"ASC";
|
||||||
|
orderComp = @">";
|
||||||
|
} else {
|
||||||
|
order = @"DESC";
|
||||||
|
orderComp = @"<";
|
||||||
|
}
|
||||||
|
FMResultSet *cursor = [db executeQuery:[NSString stringWithFormat:@"SELECT story_timestamp FROM unread_hashes ORDER BY story_timestamp %@ LIMIT 1 OFFSET %d", order, offlineLimit]];
|
||||||
|
int offlineLimitTimestamp = 0;
|
||||||
|
while ([cursor next]) {
|
||||||
|
offlineLimitTimestamp = [cursor intForColumn:@"story_timestamp"];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
NSLog(@"Deleting stories over limit: %d - %d", offlineLimit, offlineLimitTimestamp);
|
||||||
|
[db executeUpdate:[NSString stringWithFormat:@"DELETE FROM unread_hashes WHERE story_timestamp %@ %d", orderComp, offlineLimitTimestamp]];
|
||||||
|
}];
|
||||||
|
|
||||||
|
appDelegate.totalUnfetchedStoryCount = 0;
|
||||||
|
appDelegate.remainingUnfetchedStoryCount = 0;
|
||||||
|
appDelegate.latestFetchedStoryDate = 0;
|
||||||
|
appDelegate.totalUncachedImagesCount = 0;
|
||||||
|
appDelegate.remainingUncachedImagesCount = 0;
|
||||||
|
|
||||||
|
[appDelegate startOfflineFetchStories];
|
||||||
|
NSLog(@"Done syncing Unreads...");
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
Loading…
Add table
Reference in a new issue