Client/ClientAppDelegate.m

/*
     File: ClientAppDelegate.m
 Abstract: Core client application logic.
  Version: 2.2
 
 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.
 
 Copyright (C) 2013 Apple Inc. All Rights Reserved.
 
 */
 
#import "ClientAppDelegate.h"
 
#import "FileReceiveOperation.h"
 
@interface ClientAppDelegate () <NSNetServiceBrowserDelegate, NSStreamDelegate>
 
// Outlets
 
@property (nonatomic, strong, readwrite) IBOutlet NSArrayController *   servicesArray;
 
// Actions
 
- (IBAction)tableRowClickedAction:(id)sender;
 
enum {
    kDebugMenuTag = 0x64626720          // == 'dbg ' == 1684170528
};
 
enum {
    kDebugOptionMaskStallReceive       = 0x01,
    kDebugOptionMaskReceiveBadChecksum = 0x02
};
 
- (IBAction)toggleDebugOptionAction:(id)sender;
 
// The user interface uses Cocoa bindings to set itself up based on the following 
// KVC/KVO compatible properties.
 
@property (nonatomic, strong, readonly ) NSMutableSet *         services;
@property (nonatomic, strong, readonly ) NSArray *              sortDescriptors;
@property (nonatomic, strong, readwrite) NSImage *              lastReceivedImage;
@property (nonatomic, copy,   readwrite) NSString *             longStatus;
@property (nonatomic, assign, readonly, getter=isReceiving) BOOL receiving;
 
// browser internal properties
 
@property (nonatomic, strong, readwrite) NSNetServiceBrowser *  browser;
@property (nonatomic, strong, readonly ) NSMutableSet *         pendingServicesToAdd;
@property (nonatomic, strong, readonly ) NSMutableSet *         pendingServicesToRemove;
 
// downloader internal properties
 
@property (nonatomic, strong, readonly ) NSOperationQueue *     queue;
@property (nonatomic, assign, readwrite) NSUInteger             runningOperations;
@property (nonatomic, assign, readonly ) BOOL                   isReceiving;
 
// general internal properties
 
@property (nonatomic, assign, readwrite) NSUInteger             debugOptions;
 
@end
 
@implementation ClientAppDelegate
 
- (id)init
{
    self = [super init];
    if (self != nil) {
        self->_services = [[NSMutableSet alloc] init];
 
        self->_longStatus = @"Click on a service to download its image.";
 
        self->_pendingServicesToAdd = [[NSMutableSet alloc] init];
        self->_pendingServicesToRemove = [[NSMutableSet alloc] init];
 
        self->_sortDescriptors = @[
            [[NSSortDescriptor alloc] initWithKey:@"name"   ascending:YES selector:@selector(localizedStandardCompare:)], 
            [[NSSortDescriptor alloc] initWithKey:@"domain" ascending:YES selector:@selector(localizedStandardCompare:)]
        ];
 
        self->_queue = [[NSOperationQueue alloc] init];
    }
    return self;
}
 
- (void)dealloc
    // The application delegate lives for the lifetime of the application, so we don't bother 
    // implementing -dealloc.
{
    assert(NO);
}
 
#pragma mark * Application delegate callbacks
 
// This object is the delegate of the NSApplication instance so we can get notifications about 
// various state changes.
 
- (void)applicationDidFinishLaunching:(NSNotification *)notification
    // An application delegate callback called when the application has just started up.
{
    #pragma unused(notification)
 
    // Remove the debug menu if we're not the debug variant.
    
    #if defined(NDEBUG)
        {
            NSMenuItem *    debugMenuItem;
 
            debugMenuItem = nil;
            for (NSMenuItem * menuItem in [[NSApp mainMenu] itemArray]) {
                assert([menuItem isKindOfClass:[NSMenuItem class]]);
                if ([menuItem tag] == kDebugMenuTag) {
                    debugMenuItem = menuItem;
                    break;
                }
            }
            [[NSApp mainMenu] removeItem:debugMenuItem];
        }
    #endif
 
    // Start the Bonjour browser.
 
    [self startBrowsing];
}
 
- (void)applicationWillTerminate:(NSNotification *)notification
    // An application delegate callback called when the application is about to quit.
    // At this point we stop any downloads.  We leave the browser running because 
    // the system will clean it up when our process exits.
{
    #pragma unused(notification)
    if (self.isReceiving) {
        [self.queue cancelAllOperations];
    }
}
 
#pragma mark * Bound properties
 
// The user interface uses Cocoa bindings to set itself up based on these
// KVC/KVO compatible properties:
//
// o isReceiving
// o lastReceivedImage
// o services
// o sortDescriptors
// o longStatus
 
+ (NSSet *)keyPathsForValuesAffectingIsReceiving
{
    return [NSSet setWithObject:@"runningOperations"];
}
 
- (BOOL)isReceiving
{
    return (self.runningOperations != 0);
}
 
#pragma mark * Actions
 
- (IBAction)tableRowClickedAction:(id)sender
    // Called when user clicks a row in the services table.  If we're not already receiving, 
    // we kick off a receive.
{
    #pragma unused(sender)
    // We test for a positive clickedRow to eliminate clicks in the column headers.
    if ( ([sender clickedRow] >= 0) && [[self.servicesArray selectedObjects] count] != 0) {
        NSNetService *  service;
 
        service = [[self.servicesArray selectedObjects] objectAtIndex:0];
        assert([service isKindOfClass:[NSNetService class]]);
 
        [self startReceiveFromService:service];
    }
}
 
- (IBAction)toggleDebugOptionAction:(id)sender
    // Called when the user selects an item from the Debug menu.  We use the 
    // menu item's tag to determine which debug option to toggle.
{
    NSMenuItem *    menuItem;
    
    menuItem = (NSMenuItem *) sender;
    assert([menuItem isKindOfClass:[NSMenuItem class]]);
    assert([menuItem tag] != 0);
    self.debugOptions ^= (NSUInteger) [menuItem tag];
    [menuItem setState: ! [menuItem state]];
}
 
#pragma mark * Browsing
 
- (void)startBrowsing
    // Starts a browse operation for our service type.
{
    assert(self.browser == nil);
    self.browser = [[NSNetServiceBrowser alloc] init];
    [self.browser setDelegate:self];
    // Passing in "" for the domain causes us to browse in the default browse domain
    [self.browser searchForServicesOfType:@"_wwdcpic2._tcp." inDomain:@""];
}
 
- (void)stopBrowsingWithStatus:(NSString *)status
    // Stops the browser after some sort of fatal error, displaying 
    // the status message to the user.
{
    assert(status != nil);
    
    [self.browser setDelegate:nil];
    [self.browser stop];
    self.browser = nil;
    
    [self.pendingServicesToAdd removeAllObjects];
    [self.pendingServicesToRemove removeAllObjects];
 
    [self willChangeValueForKey:@"services"];
    [self.services removeAllObjects];
    [self  didChangeValueForKey:@"services"];
    
    self.longStatus = status;
}
 
- (void)netServiceBrowser:(NSNetServiceBrowser *)aNetServiceBrowser didFindService:(NSNetService *)aNetService moreComing:(BOOL)moreComing
    // An NSNetService delegate callback that's called when we discover a service. 
    // We add this service to our set of pending services to add and, if there are 
    // no more services coming, we add that set to our services set, triggering the 
    // necessary KVO notification.
{
    assert(aNetServiceBrowser == self.browser);
    #pragma unused(aNetServiceBrowser)
    
    [self.pendingServicesToAdd addObject:aNetService];
    
    if ( ! moreComing ) {
        NSSet * setToAdd;
 
        setToAdd = [self.pendingServicesToAdd copy];
        assert(setToAdd != nil);
        [self.pendingServicesToAdd removeAllObjects];
        
        [self willChangeValueForKey:@"services" withSetMutation:NSKeyValueUnionSetMutation usingObjects:setToAdd];
        [self.services unionSet:setToAdd];
        [self  didChangeValueForKey:@"services" withSetMutation:NSKeyValueUnionSetMutation usingObjects:setToAdd];
    }
}
 
- (void)netServiceBrowser:(NSNetServiceBrowser *)aNetServiceBrowser didRemoveService:(NSNetService *)aNetService moreComing:(BOOL)moreComing
    // An NSNetService delegate callback that's called when a service goes away. 
    // We add this service to our set of pending services to remove and, if there are 
    // no more services coming (well, going :-), we remove that set to our services set, 
    // triggering the necessary KVO notification.
{
    assert(aNetServiceBrowser == self.browser);
    #pragma unused(aNetServiceBrowser)
 
    [self.pendingServicesToRemove addObject:aNetService];
    
    if ( ! moreComing ) {
        NSSet * setToRemove;
 
        setToRemove = [self.pendingServicesToRemove copy];
        assert(setToRemove != nil);
        [self.pendingServicesToRemove removeAllObjects];
 
        [self willChangeValueForKey:@"services" withSetMutation:NSKeyValueMinusSetMutation usingObjects:setToRemove];
        [self.services minusSet:setToRemove];
        [self  didChangeValueForKey:@"services" withSetMutation:NSKeyValueMinusSetMutation usingObjects:setToRemove];
    }
}
 
- (void)netServiceBrowserDidStopSearch:(NSNetServiceBrowser *)aNetServiceBrowser
    // An NSNetService delegate callback that's called when the service spontaneously 
    // stops.  This rarely happens on OS X but, regardless, we respond by shutting 
    // down our browser.
{
    assert(aNetServiceBrowser == self.browser);
    #pragma unused(aNetServiceBrowser)
    [self stopBrowsingWithStatus:@"Service browsing stopped."];
}
 
- (void)netServiceBrowser:(NSNetServiceBrowser *)aNetServiceBrowser didNotSearch:(NSDictionary *)errorDict
    // An NSNetService delegate callback that's called when the browser fails 
    // completely.  We respond by shutting it down.
{
    assert(aNetServiceBrowser == self.browser);
    #pragma unused(aNetServiceBrowser)
    assert(errorDict != nil);
    #pragma unused(errorDict)
    [self stopBrowsingWithStatus:@"Service browsing failed."];
}
 
#pragma mark Core receive
 
- (void)startReceiveFromService:(NSNetService *)service
    // Starts a receive operation from the specified service.
{
    NSInputStream *         stream;
    FileReceiveOperation *  op;
    
    assert(service != nil);
    
    // Cancel any previous download operations.
    
    [self.queue cancelAllOperations];
    
    // Nix the current image so that if the download fails or stalls we're left with 
    // a blank image rather than the previous image.
    
    self.lastReceivedImage = nil;
    
    // Create a stream from the service, and create a FileReceiveOperation with that stream.
    
    [service getInputStream:&stream outputStream:nil];
    assert(stream != nil);
 
    op = [[FileReceiveOperation alloc] initWithInputStream:stream];
    assert(op != nil);
 
    // Configure the operation.
    
    #if ! defined(NDEBUG)
        if (self.debugOptions & kDebugOptionMaskStallReceive) {
            op.debugStallReceive = YES;
        }
        if (self.debugOptions & kDebugOptionMaskReceiveBadChecksum) {
            op.debugReceiveBadChecksum = YES;
        }
    #endif
 
    // Watch for the operation finishing.  In a real application I'd probably use something more 
    // sophisticated (like the QWatchedOperationQueue class from the LinkedImageFetcher sample code), 
    // but in this small sample I just use KVO directly.
    
    [op addObserver:self forKeyPath:@"isFinished" options:0 context:&self->_queue];
 
    // Enqueue the operation and then clean up.
 
    [self.queue addOperation:op];
    
    self.longStatus = [NSString stringWithFormat:@"Downloading image from “%@”.", [service name]];
    self.runningOperations += 1;
}
 
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if (context == &self->_queue) {
        assert([keyPath isEqual:@"isFinished"]);
        assert([object isKindOfClass:[FileReceiveOperation class]]);
        
        // This notification is delivered when a FileReceiveOperation's "isFinished" property 
        // changes.  We respond by calling the -didFinishOperation: operation on the main 
        // thread to clean up that operation.
        
        assert( [(FileReceiveOperation *) object isFinished] );
 
        // IMPORTANT
        // ---------
        // KVO notifications arrive on the thread that sets the property.  In this case that's 
        // always going to be the main thread (because FileReceiveOperation is a concurrent operation 
        // that runs off the main thread run loop), but I take no chances and force us to the 
        // main thread.  There's no worries about race conditions here (one of the things that 
        // QWatchedOperationQueue solves nicely) because AppDelegate lives for the lifetime of 
        // the application.
        
        [self performSelectorOnMainThread:@selector(didFinishOperation:) withObject:object waitUntilDone:NO];
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}
 
- (void)didFinishOperation:(FileReceiveOperation *)op
    // Called when a FileSendOperation finishes.  It gets the downloaded file, creates 
    // an image from it, and displays that image.
{
    NSString *  status;
 
    assert([op isKindOfClass:[FileReceiveOperation class]]);
    
    [op removeObserver:self forKeyPath:@"isFinished"];
    
    status = nil;
    if (op.error == nil) {
        NSData *    imageData;
        
        imageData = [NSData dataWithContentsOfMappedFile:op.finalFilePath];
        if (imageData == nil) {
            status = @"Image load failed.";
        } else {
            NSImage *   image;
 
            image = [[NSImage alloc] initWithData:imageData];
            if (image == nil) {
                status = @"Downloaded image was unusable.";
            } else {
                self.lastReceivedImage = image;
            }
        }
        
        // Delete the file after we've mapped it so that, once we get rid of this image 
        // and hence unmap the file, the system will reclaim the disk space automagically.
        
        (void) [[NSFileManager defaultManager] removeItemAtPath:op.finalFilePath error:NULL];
    } else if ([[op.error domain] isEqual:NSCocoaErrorDomain] && ([op.error code] == NSUserCancelledError)) {
        status = @"Download cancelled.";
    } else {
        status = @"Download failed.";
    }
    
    // Only set the status if we're the last running operation.  This prevents cancelled 
    // operations from overriding the initial status of the last running operation.
    
    if (self.runningOperations == 1) {
        self.longStatus = status;
    }
    
    assert(self.runningOperations != 0);
    self.runningOperations -= 1;
}
 
@end