Model/PhotoGallery.m
/* |
File: PhotoGallery.m |
Contains: A model object that represents a gallery of photos on the network. |
Written by: DTS |
Copyright: Copyright (c) 2010 Apple Inc. All Rights Reserved. |
Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple Inc. |
("Apple") in consideration of your agreement to the following |
terms, and your use, installation, modification or |
redistribution of this Apple software constitutes acceptance of |
these terms. If you do not agree with these terms, please do |
not use, install, modify or redistribute this Apple software. |
In consideration of your agreement to abide by the following |
terms, and subject to these terms, Apple grants you a personal, |
non-exclusive license, under Apple's copyrights in this |
original Apple software (the "Apple Software"), to use, |
reproduce, modify and redistribute the Apple Software, with or |
without modifications, in source and/or binary forms; provided |
that if you redistribute the Apple Software in its entirety and |
without modifications, you must retain this notice and the |
following text and disclaimers in all such redistributions of |
the Apple Software. Neither the name, trademarks, service marks |
or logos of Apple Inc. may be used to endorse or promote |
products derived from the Apple Software without specific prior |
written permission from Apple. Except as expressly stated in |
this notice, no other rights or licenses, express or implied, |
are granted by Apple herein, including but not limited to any |
patent rights that may be infringed by your derivative works or |
by other works in which the Apple Software may be incorporated. |
The Apple Software is provided by Apple on an "AS IS" basis. |
APPLE MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING |
WITHOUT LIMITATION THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, |
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, REGARDING |
THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN |
COMBINATION WITH YOUR PRODUCTS. |
IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, |
INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED |
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY |
OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION |
OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY |
OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR |
OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF |
SUCH DAMAGE. |
*/ |
#import "PhotoGallery.h" |
#import "Photo.h" |
#import "PhotoGalleryContext.h" |
#import "NetworkManager.h" |
#import "RecursiveDeleteOperation.h" |
#import "RetryingHTTPOperation.h" |
#import "GalleryParserOperation.h" |
#import "Logging.h" |
@interface PhotoGallery () |
// read/write variants of public properties |
@property (nonatomic, retain, readwrite) NSEntityDescription * photoEntity; |
// private properties |
@property (nonatomic, assign, readonly ) NSUInteger sequenceNumber; |
@property (nonatomic, retain, readwrite) PhotoGalleryContext * galleryContext; |
@property (nonatomic, copy, readonly ) NSString * galleryCachePath; |
@property (nonatomic, retain, readwrite) NSTimer * saveTimer; |
@property (nonatomic, assign, readwrite) PhotoGallerySyncState syncState; |
@property (nonatomic, retain, readwrite) RetryingHTTPOperation * getOperation; |
@property (nonatomic, retain, readwrite) GalleryParserOperation * parserOperation; |
@property (nonatomic, copy, readwrite) NSDate * lastSyncDate; |
@property (nonatomic, copy, readwrite) NSError * lastSyncError; |
// forward declarations |
- (void)startParserOperationWithData:(NSData *)data; |
- (void)commitParserResults:(NSArray *)latestResults; |
@end |
@implementation PhotoGallery |
// These strings define the format of our gallery cache. First up, kGalleryNameTemplate |
// and kGalleryExtension specify the name of the gallery cache directory itself. |
static NSString * kGalleryNameTemplate = @"Gallery%.9f.%@"; |
static NSString * kGalleryExtension = @"gallery"; |
// Then, within each gallery cache directory, there are the following items: |
// |
// o kInfoFileName is the name of a plist file within the gallery cache. If this is missing, |
// the gallery cache has been abandoned (and can be removed at the next startup time). |
// |
// o kDatabaseFileName is the name of the Core Data file that holds the Photo and Thumbnail |
// model objects. |
// |
// o kPhotosDirectoryName is the name of the directory containing the actual photo files. |
// Note that this is shared with PhotoGalleryContext, which is why it's not "static". |
static NSString * kInfoFileName = @"GalleryInfo.plist"; |
static NSString * kDatabaseFileName = @"Gallery.db"; |
NSString * kPhotosDirectoryName = @"Photos"; |
// The gallery info file (kInfoFileName) contains a dictionary with just one property |
// currently defined, kInfoFileName, which is the URL string of the gallery's XML data. |
static NSString * kGalleryInfoKeyGalleryURLString = @"gallerURLString"; |
+ (NSString *)cachesDirectoryPath |
// Returns the path to the caches directory. This is a class method because it's |
// used by +applicationStartup. |
{ |
NSString * result; |
NSArray * paths; |
result = nil; |
paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); |
if ( (paths != nil) && ([paths count] != 0) ) { |
assert([[paths objectAtIndex:0] isKindOfClass:[NSString class]]); |
result = [paths objectAtIndex:0]; |
} |
return result; |
} |
+ (void)abandonGalleryCacheAtPath:(NSString *)galleryCachePath |
{ |
(void) [[NSFileManager defaultManager] removeItemAtPath:[galleryCachePath stringByAppendingPathComponent:kInfoFileName] error:NULL]; |
} |
+ (void)applicationStartup |
// See comment in header. |
{ |
NSUserDefaults * userDefaults; |
NSFileManager * fileManager; |
BOOL clearAllCaches; |
NSString * cachesDirectoryPath; |
NSArray * potentialGalleryCacheNames; |
NSMutableArray * deletableGalleryCachePaths; |
NSMutableArray * liveGalleryCachePathsAndDates; |
fileManager = [NSFileManager defaultManager]; |
assert(fileManager != nil); |
userDefaults = [NSUserDefaults standardUserDefaults]; |
assert(userDefaults != nil); |
cachesDirectoryPath = [self cachesDirectoryPath]; |
assert(cachesDirectoryPath != nil); |
// See if we've been asked to nuke all gallery caches. |
clearAllCaches = [userDefaults boolForKey:@"galleryClearCache"]; |
if (clearAllCaches) { |
[[QLog log] logWithFormat:@"gallery clear cache"]; |
[userDefaults removeObjectForKey:@"galleryClearCache"]; |
[userDefaults synchronize]; |
} |
// Walk the list of gallery caches looking for abandoned ones (or, if we're |
// clearing all caches, do them all). Add the targeted gallery caches |
// to our list of things to delete. Also, for any galleries that remain, |
// put the path and the mod date in a list so that we can then find the |
// oldest galleries and delete them. |
deletableGalleryCachePaths = [NSMutableArray array]; |
assert(deletableGalleryCachePaths != nil); |
potentialGalleryCacheNames = [fileManager contentsOfDirectoryAtPath:cachesDirectoryPath error:NULL]; |
assert(potentialGalleryCacheNames != nil); |
liveGalleryCachePathsAndDates = [NSMutableArray array]; |
assert(liveGalleryCachePathsAndDates != nil); |
for (NSString * galleryCacheName in potentialGalleryCacheNames) { |
if ([galleryCacheName hasSuffix:kGalleryExtension]) { |
NSString * galleryCachePath; |
NSString * galleryInfoFilePath; |
NSString * galleryDatabaseFilePath; |
galleryCachePath = [cachesDirectoryPath stringByAppendingPathComponent:galleryCacheName]; |
assert(galleryCachePath != nil); |
galleryInfoFilePath = [galleryCachePath stringByAppendingPathComponent:kInfoFileName]; |
assert(galleryInfoFilePath != nil); |
galleryDatabaseFilePath = [galleryCachePath stringByAppendingPathComponent:kDatabaseFileName]; |
assert(galleryDatabaseFilePath != nil); |
if (clearAllCaches) { |
[[QLog log] logWithFormat:@"gallery clear '%@'", galleryCacheName]; |
(void) [fileManager removeItemAtPath:galleryInfoFilePath error:NULL]; |
[deletableGalleryCachePaths addObject:galleryCachePath]; |
} else if ( ! [fileManager fileExistsAtPath:galleryInfoFilePath]) { |
[[QLog log] logWithFormat:@"gallery delete abandoned '%@'", galleryCacheName]; |
[deletableGalleryCachePaths addObject:galleryCachePath]; |
} else { |
NSDate * modDate; |
// This gallery cache isn't abandoned. Get the modification date of its database. If |
// that fails, the gallery cache is toast, so just add it to the to-delete list. |
// If that succeeds, add a dictionary containing the gallery cache path and the |
// mod date to the list of live gallery caches. |
modDate = [[fileManager attributesOfItemAtPath:galleryDatabaseFilePath error:NULL] objectForKey:NSFileModificationDate]; |
if (modDate == nil) { |
[[QLog log] logWithFormat:@"gallery delete invalid '%@'", galleryCacheName]; |
[deletableGalleryCachePaths addObject:galleryCachePath]; |
} else { |
assert([modDate isKindOfClass:[NSDate class]]); |
[liveGalleryCachePathsAndDates addObject:[NSDictionary dictionaryWithObjectsAndKeys: |
galleryCachePath, @"path", |
modDate, @"modDate", |
nil |
]]; |
} |
} |
} |
} |
// See if we've exceeded our gallery cache limit, in which case we keep abandoning the oldest |
// gallery cache until we're under that limit. |
[liveGalleryCachePathsAndDates sortUsingDescriptors:[NSArray arrayWithObject:[[[NSSortDescriptor alloc] initWithKey:@"modDate" ascending:YES] autorelease]]]; |
while ( [liveGalleryCachePathsAndDates count] > 3 ) { |
NSString * path; |
path = [[liveGalleryCachePathsAndDates objectAtIndex:0] objectForKey:@"path"]; |
assert([path isKindOfClass:[NSString class]]); |
[[QLog log] logWithFormat:@"gallery abandon and delete '%@'", [path lastPathComponent]]; |
[self abandonGalleryCacheAtPath:path]; |
[deletableGalleryCachePaths addObject:path]; |
[liveGalleryCachePathsAndDates removeObjectAtIndex:0]; |
} |
// Start an operation to delete the targeted gallery caches. This happens on a |
// thread so that it doesn't prevent the app starting up. The app will |
// ignore these gallery caches anyway, because we removed their gallery info files. |
// Also, we don't monitor this operation for successful completion. It |
// just does its stuff and then goes away. That means that we effectively |
// leak the operation queue. Not a big deal. It also means that, if the |
// app quits before the operation is done, it just gets killed. That's |
// OK too; the delete will pick up where it left off when the app is next |
// relaunched. |
if ( [deletableGalleryCachePaths count] != 0 ) { |
static NSOperationQueue * sGalleryDeleteQueue; |
RecursiveDeleteOperation * op; |
sGalleryDeleteQueue = [[NSOperationQueue alloc] init]; |
assert(sGalleryDeleteQueue != nil); |
op = [[[RecursiveDeleteOperation alloc] initWithPaths:deletableGalleryCachePaths] autorelease]; |
assert(op != nil); |
if ( [op respondsToSelector:@selector(setThreadPriority:)] ) { |
[op setThreadPriority:0.1]; |
} |
[sGalleryDeleteQueue addOperation:op]; |
} |
} |
- (id)initWithGalleryURLString:(NSString *)galleryURLString |
{ |
assert(galleryURLString != nil); |
// The initialisation method is very simple. All of the heavy lifting is done |
// in -start. |
self = [super init]; |
if (self != nil) { |
static NSUInteger sNextGallerySequenceNumber; |
self->_galleryURLString = [galleryURLString copy]; |
self->_sequenceNumber = sNextGallerySequenceNumber; |
sNextGallerySequenceNumber += 1; |
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didBecomeActive:) name:UIApplicationDidBecomeActiveNotification object:nil]; |
[[QLog log] logWithFormat:@"gallery %zu is %@", (size_t) self->_sequenceNumber, galleryURLString]; |
} |
return self; |
} |
- (void)dealloc |
{ |
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidBecomeActiveNotification object:nil]; |
[self->_galleryURLString release]; |
// We should have been stopped before being released, so these properties |
// should be nil by the time -dealloc is called. |
assert(self->_galleryContext == nil); |
assert(self->_photoEntity == nil); |
assert(self->_saveTimer == nil); |
[self->_lastSyncDate release]; |
[self->_lastSyncError release]; |
[self->_standardDateFormatter release]; |
// We should have been stopped before being released, so these properties |
// should be nil by the time -dealloc is called. |
assert(self->_getOperation == nil); |
assert(self->_parserOperation == nil); |
[super dealloc]; |
} |
@synthesize galleryURLString = _galleryURLString; |
@synthesize sequenceNumber = _sequenceNumber; |
- (void)didBecomeActive:(NSNotification *)note |
{ |
#pragma unused(note) |
// Having the ability to sync on activate makes it easy to test various cases where |
// you want to force a sync in a weird context (like when the PhotoDetailViewController |
// is up). |
if ( [[NSUserDefaults standardUserDefaults] boolForKey:@"gallerySyncOnActivate"] ) { |
if (self.galleryContext != nil) { |
[self startSync]; |
} |
} |
} |
#pragma mark * Core Data wrangling |
@synthesize galleryContext = _galleryContext; |
+ (NSSet *)keyPathsForValuesAffectingManagedObjectContext |
{ |
return [NSSet setWithObject:@"galleryContext"]; |
} |
- (NSManagedObjectContext *)managedObjectContext |
{ |
return self.galleryContext; |
} |
- (NSEntityDescription *)photoEntity |
{ |
if (self->_photoEntity == nil) { |
assert(self.galleryContext != nil); |
self->_photoEntity = [[NSEntityDescription entityForName:@"Photo" inManagedObjectContext:self.galleryContext] retain]; |
assert(self->_photoEntity != nil); |
} |
return self->_photoEntity; |
} |
@synthesize photoEntity = _photoEntity; |
- (NSFetchRequest *)photosFetchRequest |
// Returns a fetch request that gets all of the photos in the database. |
{ |
NSFetchRequest * fetchRequest; |
fetchRequest = [[[NSFetchRequest alloc] init] autorelease]; |
assert(fetchRequest != nil); |
[fetchRequest setEntity:self.photoEntity]; |
[fetchRequest setFetchBatchSize:20]; |
return fetchRequest; |
} |
- (NSString *)galleryCachePathForOurGallery |
// Try to find the gallery cache for our gallery URL string. |
{ |
NSString * result; |
NSFileManager * fileManager; |
NSString * cachesDirectoryPath; |
NSArray * potentialGalleries; |
NSString * galleryName; |
assert(self.galleryURLString != nil); |
fileManager = [NSFileManager defaultManager]; |
assert(fileManager != nil); |
cachesDirectoryPath = [[self class] cachesDirectoryPath]; |
assert(cachesDirectoryPath != nil); |
// First look through the caches directory for a gallery cache whose info file |
// matches the gallery URL string we're looking for. |
potentialGalleries = [fileManager contentsOfDirectoryAtPath:cachesDirectoryPath error:NULL]; |
assert(potentialGalleries != nil); |
result = nil; |
for (galleryName in potentialGalleries) { |
if ([galleryName hasSuffix:kGalleryExtension]) { |
NSDictionary * galleryInfo; |
NSString * galleryInfoURLString; |
galleryInfo = [NSDictionary dictionaryWithContentsOfFile:[[cachesDirectoryPath stringByAppendingPathComponent:galleryName] stringByAppendingPathComponent:kInfoFileName]]; |
if (galleryInfo != nil) { |
galleryInfoURLString = [galleryInfo objectForKey:kGalleryInfoKeyGalleryURLString]; |
if ( [self.galleryURLString isEqual:galleryInfoURLString] ) { |
result = [cachesDirectoryPath stringByAppendingPathComponent:galleryName]; |
break; |
} |
} |
} |
} |
// If we find nothing, create a new gallery cache and record it as belonging to the specified |
// gallery URL string. |
if (result == nil) { |
BOOL success; |
galleryName = [NSString stringWithFormat:kGalleryNameTemplate, [NSDate timeIntervalSinceReferenceDate], kGalleryExtension]; |
assert(galleryName != nil); |
result = [cachesDirectoryPath stringByAppendingPathComponent:galleryName]; |
success = [fileManager createDirectoryAtPath:result withIntermediateDirectories:NO attributes:NULL error:NULL]; |
if (success) { |
NSDictionary * galleryInfo; |
galleryInfo = [NSDictionary dictionaryWithObjectsAndKeys:self.galleryURLString, kGalleryInfoKeyGalleryURLString, nil]; |
assert(galleryInfo != nil); |
success = [galleryInfo writeToFile:[result stringByAppendingPathComponent:kInfoFileName] atomically:YES]; |
} |
if ( ! success ) { |
result = nil; |
} |
[[QLog log] logWithFormat:@"gallery %zu created new '%@'", (size_t) self.sequenceNumber, galleryName]; |
} else { |
assert(galleryName != nil); |
[[QLog log] logWithFormat:@"gallery %zu found existing '%@'", (size_t) self.sequenceNumber, galleryName]; |
} |
return result; |
} |
- (void)abandonGalleryCacheAtPath:(NSString *)galleryCachePath |
// Abandons the specified gallery cache directory. We do this simply by removing the gallery |
// info file. The directory will be deleted when the application is next launched. |
{ |
assert(galleryCachePath != nil); |
[[QLog log] logWithFormat:@"gallery %zu abandon '%@'", (size_t) self.sequenceNumber, [galleryCachePath lastPathComponent]]; |
[[self class] abandonGalleryCacheAtPath:galleryCachePath]; |
} |
- (NSString *)galleryCachePath |
{ |
assert(self.galleryContext != nil); |
return self.galleryContext.galleryCachePath; |
} |
- (BOOL)setupGalleryContext |
// Attempt to start up the gallery cache for our gallery URL string, either by finding an existing |
// cache or by creating one from scratch. On success, self.galleryCachePath will point to that |
// gallery cache and self.galleryContext will be the managed object context for the database |
// within the gallery cache. |
{ |
BOOL success; |
NSError * error; |
NSFileManager * fileManager; |
NSString * galleryCachePath; |
NSString * photosDirectoryPath; |
BOOL isDir; |
NSURL * databaseURL; |
NSManagedObjectModel * model; |
NSPersistentStoreCoordinator * psc; |
assert(self.galleryURLString != nil); |
[[QLog log] logWithFormat:@"gallery %zu starting", (size_t) self.sequenceNumber]; |
error = nil; |
fileManager = [NSFileManager defaultManager]; |
assert(fileManager != nil); |
// Find the gallery cache directory for this gallery. |
galleryCachePath = [self galleryCachePathForOurGallery]; |
success = (galleryCachePath != nil); |
// Create the "Photos" directory if it doesn't already exist. |
if (success) { |
photosDirectoryPath = [galleryCachePath stringByAppendingPathComponent:kPhotosDirectoryName]; |
assert(photosDirectoryPath != nil); |
success = [fileManager fileExistsAtPath:photosDirectoryPath isDirectory:&isDir] && isDir; |
if ( ! success ) { |
success = [fileManager createDirectoryAtPath:photosDirectoryPath withIntermediateDirectories:NO attributes:NULL error:NULL]; |
} |
} |
// Start up Core Data in the gallery directory. |
if (success) { |
NSString * modelPath; |
modelPath = [[NSBundle bundleForClass:[self class]] pathForResource:@"Photos" ofType:@"mom"]; |
assert(modelPath != nil); |
model = [[[NSManagedObjectModel alloc] initWithContentsOfURL:[NSURL fileURLWithPath:modelPath]] autorelease]; |
success = (model != nil); |
} |
if (success) { |
databaseURL = [NSURL fileURLWithPath:[galleryCachePath stringByAppendingPathComponent:kDatabaseFileName]]; |
assert(databaseURL != nil); |
psc = [[[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model] autorelease]; |
success = (psc != nil); |
} |
if (success) { |
success = [psc addPersistentStoreWithType:NSSQLiteStoreType |
configuration:nil |
URL:databaseURL |
options:nil |
error:&error |
] != nil; |
if (success) { |
error = nil; |
} |
} |
if (success) { |
PhotoGalleryContext * context; |
// Everything has gone well, so we create a managed object context from our persistent |
// store. Note that we use a subclass of NSManagedObjectContext, PhotoGalleryContext, which |
// carries along some state that the managed objects (specifically the Photo objects) need |
// access to. |
context = [[[PhotoGalleryContext alloc] initWithGalleryURLString:self.galleryURLString galleryCachePath:galleryCachePath] autorelease]; |
assert(context != nil); |
[context setPersistentStoreCoordinator:psc]; |
// In older versions of the code various folks observed our photoGalleryContext property |
// and did clever things when it changed. So it was important to not set that property |
// until everything as fully up and running. That no longer happens, but I've kept the |
// configure-before-set code because it seems like the right thing to do. |
self.galleryContext = context; |
// Subscribe to the context changed notification so that we can auto-save. |
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(contextChanged:) name:NSManagedObjectContextObjectsDidChangeNotification object:self.managedObjectContext]; |
[[QLog log] logWithFormat:@"gallery %zu started '%@'", (size_t) self.sequenceNumber, [self.galleryCachePath lastPathComponent]]; |
} else { |
// Bad things happened. Log the error and return NO. |
if (error == nil) { |
[[QLog log] logWithFormat:@"gallery %zu start error", (size_t) self.sequenceNumber]; |
} else { |
[[QLog log] logWithFormat:@"gallery %zu start error %@", (size_t) self.sequenceNumber, error]; |
} |
// Also, if we found or created a gallery cache but failed to start up in it, abandon it in |
// the hope that our next attempt will work better. |
if (galleryCachePath != nil) { |
[self abandonGalleryCacheAtPath:galleryCachePath]; |
} |
} |
return success; |
} |
- (void)start |
// See comment in header. |
{ |
BOOL success; |
assert(self.galleryURLString != nil); |
// Try to start up. If this fails, it abandons the gallery cache, so a retry |
// on our part is warranted. |
success = [self setupGalleryContext]; |
if ( ! success ) { |
success = [self setupGalleryContext]; |
} |
// If all went well, start the syncing processing. If not, the application is dead |
// and we crash. |
if (success) { |
[self startSync]; |
} else { |
abort(); |
} |
} |
@synthesize saveTimer = _saveTimer; |
- (void)save |
// See comment in header. |
{ |
NSError * error; |
error = nil; |
// Disable the auto-save timer. |
[self.saveTimer invalidate]; |
self.saveTimer = nil; |
// Save. |
if ( (self.galleryContext != nil) && [self.galleryContext hasChanges] ) { |
BOOL success; |
success = [self.galleryContext save:&error]; |
if (success) { |
error = nil; |
} |
} |
// Log the results. |
if (error == nil) { |
[[QLog log] logWithFormat:@"gallery %zu saved", (size_t) self.sequenceNumber]; |
} else { |
[[QLog log] logWithFormat:@"gallery %zu save error %@", (size_t) self.sequenceNumber, error]; |
} |
} |
- (void)contextChanged:(NSNotification *)note |
// Called when the managed object context changes (courtesy of the |
// NSManagedObjectContextObjectsDidChangeNotification notification). We start an |
// auto-save timer to fire in 5 seconds. This means that rapid-fire changes don't |
// cause a flood of saves. |
{ |
#pragma unused(note) |
if (self.saveTimer != nil) { |
[self.saveTimer invalidate]; |
} |
self.saveTimer = [NSTimer scheduledTimerWithTimeInterval:5.0 target:self selector:@selector(save) userInfo:nil repeats:NO]; |
} |
- (void)stop |
// See comment in header. |
// |
// Shuts down our access to the gallery cache. We do this in two situations: |
// |
// o When the user switches gallery. |
// o When the application terminates. |
{ |
[self stopSync]; |
// Shut down the managed object context. |
if (self.galleryContext != nil) { |
// Shut down the auto save mechanism and then force a save. |
[[NSNotificationCenter defaultCenter] removeObserver:self name:NSManagedObjectContextObjectsDidChangeNotification object:self.galleryContext]; |
[self save]; |
self.photoEntity = nil; |
self.galleryContext = nil; |
} |
[[QLog log] logWithFormat:@"gallery %zu stopped", (size_t) self.sequenceNumber]; |
} |
#pragma mark * Synchronisation |
@synthesize getOperation = _getOperation; |
@synthesize parserOperation = _parserOperation; |
@synthesize lastSyncDate = _lastSyncDate; |
+ (NSSet *)keyPathsForValuesAffectingSyncStatus |
{ |
return [NSSet setWithObjects:@"syncState", @"lastSyncError", @"standardDateFormatter", @"lastSyncDate", @"getOperation.retryStateClient", nil]; |
} |
- (NSString *)syncStatus |
// See comment in header. |
{ |
NSString * result; |
if (self.lastSyncError == nil) { |
switch (self.syncState) { |
case kPhotoGallerySyncStateStopped: { |
if (self.lastSyncDate == nil) { |
result = @"Not updated"; |
} else { |
result = [NSString stringWithFormat:@"Updated: %@", [self.standardDateFormatter stringFromDate:self.lastSyncDate]]; |
} |
} break; |
default: { |
if ( (self.getOperation != nil) && (self.getOperation.retryStateClient == kRetryingHTTPOperationStateWaitingToRetry) ) { |
result = @"Waiting for network"; |
} else { |
result = @"Updating…"; |
} |
} break; |
} |
} else { |
if ([[self.lastSyncError domain] isEqual:NSCocoaErrorDomain] && [self.lastSyncError code] == NSUserCancelledError) { |
result = @"Update cancelled"; |
} else { |
// At this point self.lastSyncError contains the actual error. |
// However, we ignore that and return a very generic error status. |
// Users don't understand "Connection reset by peer" anyway (-: |
result = @"Update failed"; |
} |
} |
return result; |
} |
- (NSDateFormatter *)standardDateFormatter |
// See comment in header. |
{ |
if (self->_standardDateFormatter == nil) { |
self->_standardDateFormatter = [[NSDateFormatter alloc] init]; |
assert(self->_standardDateFormatter != nil); |
[self->_standardDateFormatter setDateStyle:NSDateFormatterMediumStyle]; |
[self->_standardDateFormatter setTimeStyle:NSDateFormatterMediumStyle]; |
// Watch for changes in the locale and time zone so that we can update |
// our date formatter accordingly. |
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateStandardDateFormatter:) name:NSCurrentLocaleDidChangeNotification object:nil]; |
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateStandardDateFormatter:) name:NSSystemTimeZoneDidChangeNotification object:nil]; |
} |
return self->_standardDateFormatter; |
} |
- (void)updateStandardDateFormatter:(NSNotification *)note |
// Called when either the current locale or the current time zone changes. |
// We respond by applying the latest values to our date formatter. |
{ |
#pragma unused(note) |
NSDateFormatter * df; |
df = self.standardDateFormatter; |
[self willChangeValueForKey:@"standardDateFormatter"]; |
[df setLocale:[NSLocale currentLocale]]; |
[df setTimeZone:[NSTimeZone localTimeZone]]; |
[self didChangeValueForKey:@"standardDateFormatter"]; |
} |
@synthesize lastSyncError = _lastSyncError; |
+ (BOOL)automaticallyNotifiesObserversOfLastSyncError |
{ |
return NO; |
} |
- (void)setLastSyncError:(NSError *)newValue |
// We override this setter purely so that we can log the error. |
{ |
assert([NSThread isMainThread]); |
if (newValue != nil) { |
[[QLog log] logWithFormat:@"gallery %zu sync error %@", (size_t) self.sequenceNumber, newValue]; |
} |
if (newValue != self->_lastSyncError) { |
[self willChangeValueForKey:@"lastSyncError"]; |
[self->_lastSyncError release]; |
self->_lastSyncError = [newValue copy]; |
[self didChangeValueForKey:@"lastSyncError"]; |
} |
} |
- (void)startGetOperation |
// Starts the HTTP operation to GET the photo gallery's XML. |
{ |
NSMutableURLRequest * request; |
assert(self.syncState == kPhotoGallerySyncStateStopped); |
[[QLog log] logOption:kLogOptionSyncDetails withFormat:@"gallery %zu sync get start", (size_t) self.sequenceNumber]; |
request = [self.galleryContext requestToGetGalleryRelativeString:nil]; |
assert(request != nil); |
assert(self.getOperation == nil); |
self.getOperation = [[[RetryingHTTPOperation alloc] initWithRequest:request] autorelease]; |
assert(self.getOperation != nil); |
[self.getOperation setQueuePriority:NSOperationQueuePriorityNormal]; |
self.getOperation.acceptableContentTypes = [NSSet setWithObjects:@"application/xml", @"text/xml", nil]; |
[[NetworkManager sharedManager] addNetworkManagementOperation:self.getOperation finishedTarget:self action:@selector(getOperationDone:)]; |
self.syncState = kPhotoGallerySyncStateGetting; |
} |
- (void)getOperationDone:(RetryingHTTPOperation *)operation |
// Called when the HTTP operation to GET the photo gallery's XML completes. |
// If all is well we start an operation to parse the XML. |
{ |
NSError * error; |
assert([operation isKindOfClass:[RetryingHTTPOperation class]]); |
assert(operation == self.getOperation); |
assert(self.syncState == kPhotoGallerySyncStateGetting); |
[[QLog log] logOption:kLogOptionSyncDetails withFormat:@"gallery %zu sync listing done", (size_t) self.sequenceNumber]; |
error = operation.error; |
if (error != nil) { |
self.lastSyncError = error; |
self.syncState = kPhotoGallerySyncStateStopped; |
} else { |
if ([QLog log].isEnabled) { |
[[QLog log] logOption:kLogOptionNetworkData withFormat:@"receive %@", self.getOperation.responseContent]; |
} |
[self startParserOperationWithData:self.getOperation.responseContent]; |
} |
self.getOperation = nil; |
} |
- (void)startParserOperationWithData:(NSData *)data |
// Starts the operation to parse the gallery's XML. |
{ |
assert(self.syncState == kPhotoGallerySyncStateGetting); |
[[QLog log] logOption:kLogOptionSyncDetails withFormat:@"gallery %zu sync parse start", (size_t) self.sequenceNumber]; |
assert(self.parserOperation == nil); |
self.parserOperation = [[[GalleryParserOperation alloc] initWithData:data] autorelease]; |
assert(self.parserOperation != nil); |
[self.parserOperation setQueuePriority:NSOperationQueuePriorityNormal]; |
[[NetworkManager sharedManager] addCPUOperation:self.parserOperation finishedTarget:self action:@selector(parserOperationDone:)]; |
self.syncState = kPhotoGallerySyncStateParsing; |
} |
- (void)parserOperationDone:(GalleryParserOperation *)operation |
// Called when the operation to parse the gallery's XML completes. |
// If all went well we commit the results to our database. |
{ |
assert([NSThread isMainThread]); |
assert([operation isKindOfClass:[GalleryParserOperation class]]); |
assert(operation == self.parserOperation); |
assert(self.syncState == kPhotoGallerySyncStateParsing); |
[[QLog log] logOption:kLogOptionSyncDetails withFormat:@"gallery %zu sync parse done", (size_t) self.sequenceNumber]; |
if (operation.error != nil) { |
self.lastSyncError = operation.error; |
self.syncState = kPhotoGallerySyncStateStopped; |
} else { |
[self commitParserResults:operation.results]; |
assert(self.lastSyncError == nil); |
self.lastSyncDate = [NSDate date]; |
self.syncState = kPhotoGallerySyncStateStopped; |
[[QLog log] logWithFormat:@"gallery %zu sync success", (size_t) self.sequenceNumber]; |
} |
self.parserOperation = nil; |
} |
#if ! defined(NDEBUG) |
- (void)checkDatabase |
// In debug mode we call this routine after committing our changes to the database |
// to verify that the database looks reasonable. |
{ |
NSFetchRequest * photosFetchRequest; |
NSFetchRequest * thumbnailsFetchRequest; |
NSArray * allPhotos; |
NSMutableSet * remainingThumbnails; |
Photo * photo; |
Thumbnail * thumbnail; |
assert(self.galleryContext != nil); |
// Get all of the photos and all of the thumbnails. |
photosFetchRequest = [self photosFetchRequest]; |
assert(photosFetchRequest != nil); |
allPhotos = [self.galleryContext executeFetchRequest:photosFetchRequest error:NULL]; |
assert(allPhotos != nil); |
thumbnailsFetchRequest = [[[NSFetchRequest alloc] init] autorelease]; |
assert(thumbnailsFetchRequest != nil); |
[thumbnailsFetchRequest setEntity:[NSEntityDescription entityForName:@"Thumbnail" inManagedObjectContext:self.galleryContext]]; |
[thumbnailsFetchRequest setFetchBatchSize:20]; |
remainingThumbnails = [NSMutableSet setWithArray:[self.galleryContext executeFetchRequest:thumbnailsFetchRequest error:NULL]]; |
assert(remainingThumbnails != nil); |
// Check that ever photo has a thumbnail (and also remove that thumbnail |
// from the remainingThumbnails set). |
for (photo in allPhotos) { |
assert([photo isKindOfClass:[Photo class]]); |
thumbnail = photo.thumbnail; |
if (thumbnail != nil) { |
if ([remainingThumbnails containsObject:thumbnail]) { |
[remainingThumbnails removeObject:thumbnail]; |
} else { |
NSLog(@"*** photo %@ has no thumbnail", photo.photoID); |
} |
} |
} |
// Check that there are no orphaned thumbnails (thumbnails that aren't attached to |
// a photo). |
for (thumbnail in remainingThumbnails) { |
NSLog(@"*** thumbnail %@ orphaned", thumbnail); |
} |
} |
#endif |
- (void)commitParserResults:(NSArray *)parserResults |
// Commits the results of parsing our the gallery's XML to the Core Data database. |
{ |
NSError * error; |
NSDate * syncDate; |
NSArray * knownPhotos; // of Photo |
syncDate = [NSDate date]; |
assert(syncDate != nil); |
// Start by getting all of the photos that we currently have in the database. |
knownPhotos = [self.galleryContext executeFetchRequest:[self photosFetchRequest] error:&error]; |
assert(knownPhotos != nil); |
if (knownPhotos != nil) { |
NSMutableSet * photosToRemove; |
NSMutableDictionary * photoIDToKnownPhotos; |
NSMutableSet * parserIDs; |
Photo * knownPhoto; |
// For each photo found in the XML, get the corresponding Photo object |
// (based on the photoID). If there is one, update it based on the new |
// properties from the XML (this may cause the photo to get new thumbnail |
// and photo images, and trigger significant UI updates). If there isn't an |
// existing photo, create one based on the properties from the XML. |
// Create photosToRemove, which starts out as a set of all the photos we know |
// about. As we refresh each existing photo, we remove it from this set. Any |
// photos left over are no longer present in the XML, and we remove them. |
photosToRemove = [NSMutableSet setWithArray:knownPhotos]; |
assert(photosToRemove != nil); |
// Create photoIDToKnownPhotos, which is a map from photoID to photo. We use this |
// to quickly determine if a photo with a specific photoID currently exists. |
photoIDToKnownPhotos = [NSMutableDictionary dictionary]; |
assert(photoIDToKnownPhotos != nil); |
for (knownPhoto in knownPhotos) { |
assert([knownPhoto isKindOfClass:[Photo class]]); |
[photoIDToKnownPhotos setObject:knownPhoto forKey:knownPhoto.photoID]; |
} |
// Finally, create parserIDs, which is set of all the photoIDs that have come in |
// from the XML. We use this to detect duplicate photoIDs in the incoming XML. |
// It would be bad to have two photos with the same ID. |
parserIDs = [NSMutableSet set]; |
assert(parserIDs != nil); |
// Iterate through the incoming XML results, processing each one in turn. |
for (NSDictionary * parserResult in parserResults) { |
NSString * photoID; |
photoID = [parserResult objectForKey:kGalleryParserResultPhotoID]; |
assert([photoID isKindOfClass:[NSString class]]); |
// Check for duplicates. |
if ([parserIDs containsObject:photoID]) { |
[[QLog log] logOption:kLogOptionSyncDetails withFormat:@"gallery %zu sync duplicate photo %@", (size_t) self.sequenceNumber, photoID]; |
} else { |
NSDictionary * properties; |
[parserIDs addObject:photoID]; |
// Build a properties dictionary, used by both the create and update code paths. |
properties = [NSDictionary dictionaryWithObjectsAndKeys: |
photoID, @"photoID", |
[parserResult objectForKey:kGalleryParserResultName], @"displayName", |
[parserResult objectForKey:kGalleryParserResultDate], @"date", |
[parserResult objectForKey:kGalleryParserResultPhotoPath], @"remotePhotoPath", |
[parserResult objectForKey:kGalleryParserResultThumbnailPath], @"remoteThumbnailPath", |
nil |
]; |
assert(properties != nil); |
// See whether we know about this specific photoID. |
knownPhoto = [photoIDToKnownPhotos objectForKey:photoID]; |
if (knownPhoto != nil) { |
// Yes. Give the photo a chance to update itself from the incoming properties. |
[[QLog log] logOption:kLogOptionSyncDetails withFormat:@"gallery %zu sync refresh %@", (size_t) self.sequenceNumber, photoID]; |
[photosToRemove removeObject:knownPhoto]; |
[knownPhoto updateWithProperties:properties]; |
} else { |
// No. Create a new photo with the specified properties. |
[[QLog log] logOption:kLogOptionSyncDetails withFormat:@"gallery %zu sync create %@", (size_t) self.sequenceNumber, photoID]; |
knownPhoto = [Photo insertNewPhotoWithProperties:properties inManagedObjectContext:self.galleryContext]; |
assert(knownPhoto != nil); |
assert(knownPhoto.photoID != nil); |
assert(knownPhoto.localPhotoPath == nil); |
assert(knownPhoto.thumbnail == nil); |
[photoIDToKnownPhotos setObject:knownPhoto forKey:knownPhoto.photoID]; |
} |
} |
} |
// Remove any photos that are no longer present in the XML. |
for (knownPhoto in photosToRemove) { |
[[QLog log] logOption:kLogOptionSyncDetails withFormat:@"gallery %zu sync delete %@", (size_t) self.sequenceNumber, knownPhoto.photoID]; |
[self.galleryContext deleteObject:knownPhoto]; |
} |
} |
#if ! defined(NDEBUG) |
[self checkDatabase]; |
#endif |
} |
+ (NSSet *)keyPathsForValuesAffectingSyncing |
{ |
return [NSSet setWithObject:@"syncState"]; |
} |
- (BOOL)isSyncing |
// See comment in header. |
{ |
return (self->_syncState > kPhotoGallerySyncStateStopped); |
} |
@synthesize syncState = _syncState; |
+ (BOOL)automaticallyNotifiesObserversOfSyncState |
{ |
return NO; |
} |
- (void)setSyncState:(PhotoGallerySyncState)newValue |
{ |
if (newValue != self->_syncState) { |
BOOL isSyncingChanged; |
isSyncingChanged = (self->_syncState > kPhotoGallerySyncStateStopped) != (newValue > kPhotoGallerySyncStateStopped); |
[self willChangeValueForKey:@"syncState"]; |
if (isSyncingChanged) { |
[self willChangeValueForKey:@"syncing"]; |
} |
self->_syncState = newValue; |
if (isSyncingChanged) { |
[self didChangeValueForKey:@"syncing"]; |
} |
[self didChangeValueForKey:@"syncState"]; |
} |
} |
- (void)startSync |
// See comment in header. |
{ |
if ( ! self.isSyncing ) { |
if (self.syncState == kPhotoGallerySyncStateStopped) { |
[[QLog log] logWithFormat:@"gallery %zu sync start", (size_t) self.sequenceNumber]; |
assert(self.getOperation == nil); |
self.lastSyncError = nil; |
[self startGetOperation]; |
} |
} |
} |
- (void)stopSync |
// See comment in header. |
{ |
if (self.isSyncing) { |
if (self.getOperation != nil) { |
[[NetworkManager sharedManager] cancelOperation:self.getOperation]; |
self.getOperation = nil; |
} |
if (self.parserOperation) { |
[[NetworkManager sharedManager] cancelOperation:self.parserOperation]; |
self.parserOperation = nil; |
} |
self.lastSyncError = [NSError errorWithDomain:NSCocoaErrorDomain code:NSUserCancelledError userInfo:nil]; |
self.syncState = kPhotoGallerySyncStateStopped; |
[[QLog log] logWithFormat:@"gallery %zu sync stopped", (size_t) self.sequenceNumber]; |
} |
} |
@end |
Copyright © 2010 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2010-10-22