Support multiple accounts. Bug fixes. Improve concurrency.
[pithos-macos] / pithos-macos / pithos_macosAppDelegate.m
index 42d731e..e52fe17 100644 (file)
@@ -2,7 +2,7 @@
 //  pithos_macosAppDelegate.m
 //  pithos-macos
 //
-// Copyright 2011 GRNET S.A. All rights reserved.
+// Copyright 2011-2012 GRNET S.A. All rights reserved.
 //
 // Redistribution and use in source and binary forms, with or
 // without modification, are permitted provided that the following
 // or implied, of GRNET S.A.
 
 #import "pithos_macosAppDelegate.h"
+#import "PithosAccount.h"
 #import "PithosBrowserController.h"
+#import "PithosPreferencesController.h"
+#import "PithosSyncDaemon.h"
+#import "ASIPithosRequest.h"
+#import "ASIPithos.h"
+#import "ASIDownloadCache.h"
+#import "LastCompletedSyncTransformer.h"
 
 @implementation pithos_macosAppDelegate
-@synthesize pithosBrowserController;
+@synthesize pithosBrowserController, alwaysNo, aboutVersion, activated, currentPithosAccount, pithosAccounts, pithosAccountsDictionary, syncPithosAccount;
 
 - (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
     [[NSAppleEventManager sharedAppleEventManager] setEventHandler:self 
                                                        andSelector:@selector(handleAppleEvent:withReplyEvent:) 
                                                      forEventClass:kInternetEventClass 
                                                         andEventID:kAEGetURL];
-    [pithosBrowserController showWindow:self];
+    
+    userDefaults = [[NSUserDefaults standardUserDefaults] retain];
+
+    NSString *stringURL = [userDefaults stringForKey:@"aboutURL"];
+    NSURL *testURL = (stringURL) ? [NSURL URLWithString:stringURL] : nil;
+    if (!testURL || !testURL.scheme || !testURL.host) {
+        [userDefaults setObject:@"https://pithos.dev.grnet.gr/docs/pithos" forKey:@"aboutURL"];
+        [userDefaults synchronize];
+    }
+    
+    syncTimeInterval = [userDefaults doubleForKey:@"syncTimeInterval"];
+    if (syncTimeInterval <= 0.0) {
+        syncTimeInterval = 180.0;
+        [userDefaults setDouble:syncTimeInterval forKey:@"syncTimeInterval"];
+        [userDefaults synchronize];
+    }
+    
+    NSData *tmpData = [userDefaults objectForKey:@"pithosAccounts"];
+    NSArray *tmpArray;
+    if (tmpData && (tmpArray = [NSKeyedUnarchiver unarchiveObjectWithData:tmpData]))
+        self.pithosAccounts = [NSMutableArray arrayWithArray:tmpArray];
+    else
+        self.pithosAccounts = [NSMutableArray array];
+    
+    if (![pithosAccounts count]) {
+        [pithosAccounts addObject:[PithosAccount pithosAccount]];
+        self.pithosAccounts = self.pithosAccounts;
+    } else {
+        self.activated = YES;
+    }
+    
+    pithosAccountsDictionary = [[NSMutableDictionary alloc] initWithCapacity:[pithosAccounts count]];
+    for (PithosAccount *pithosAccount in pithosAccounts) {
+        [pithosAccountsDictionary setObject:pithosAccount forKey:pithosAccount.name];
+        if (!currentPithosAccount && pithosAccount.active)
+            currentPithosAccount = [pithosAccount retain];
+    }
+    if (!currentPithosAccount)
+        self.currentPithosAccount = [pithosAccounts objectAtIndex:0];
+    
+    if (currentPithosAccount.active) {
+        [self savePithosAccounts:self];
+        [self showPithosBrowser:self];
+        pithosBrowserController.pithos = currentPithosAccount.pithos;
+    } else {
+        // XXX maybe call specifically to go to new account tab
+        [self showPithosPreferences:self];
+    }
+
+    syncTimer = [[NSTimer scheduledTimerWithTimeInterval:syncTimeInterval 
+                                                  target:self 
+                                                selector:@selector(sync) 
+                                                userInfo:nil 
+                                                 repeats:YES] retain];
+    [syncTimer fire];
+}
+
+// Based on: http://cocoatutorial.grapewave.com/2010/01/creating-a-status-bar-application/
+// and: http://www.cocoadev.com/index.pl?ThumbnailImages
+- (void)awakeFromNib {
+    NSImage *sourceImage = [NSImage imageNamed:@"pithos-large.png"];
+    
+    NSImage *smallImage = [[[NSImage alloc] initWithSize:NSMakeSize(18, 18)] autorelease];
+    [smallImage lockFocus];
+    [[NSGraphicsContext currentContext] setImageInterpolation:NSImageInterpolationHigh];
+    [sourceImage setSize:NSMakeSize(18, 18)];
+    [sourceImage compositeToPoint:NSZeroPoint operation:NSCompositeCopy];
+    [smallImage unlockFocus];
+    
+    statusItem = [[[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength] retain];
+    [statusItem setMenu:statusMenu];
+    [statusItem setImage:sourceImage];
+    [statusItem setHighlightMode:YES];
+    
+    self.alwaysNo = NO;
 }
 
 - (void)handleAppleEvent:(NSAppleEventDescriptor *)event withReplyEvent: (NSAppleEventDescriptor *)replyEvent {
     NSURL *url = [NSURL URLWithString:[[event paramDescriptorForKeyword:keyDirectObject] stringValue]];
     NSString *host = [url host];
        NSString *query = [url query];
-    NSLog(@"host : '%@', query: '%@'", host, query);
+    PithosAccount *pithosAccount = [pithosAccountsDictionary objectForKey:[url lastPathComponent]];
     NSProcessInfo *processInfo = [NSProcessInfo processInfo];
-    if ([host isEqualToString:[NSString stringWithFormat:@"%@_%d", [processInfo processName], [processInfo processIdentifier]]] && query) {
+    if ([host isEqualToString:[NSString stringWithFormat:@"%@_%d", [processInfo processName], [processInfo processIdentifier]]] && 
+        pithosAccount && query) {
         // user=
         NSString *authUser;
         NSRange userRange = [query rangeOfString:@"user=" options:NSCaseInsensitiveSearch];
             authToken = [[query substringFromIndex:authTokenStartLocation]
                          stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
         }
+        
         NSLog(@"query authUser: '%@', authToken: '%@'", authUser, authToken);
-        [pithosBrowserController authenticateFromURLWithAuthUser:authUser authToken:authToken];
+        if ([authUser length] && [authToken length]) {
+            [pithosAccount authenticateWithServerURL:nil authUser:authUser authToken:authToken];
+            [self savePithosAccounts:self];
+            if (pithosPreferencesController && [pithosPreferencesController.selectedPithosAccount isEqualTo:pithosAccount]) {
+                pithosPreferencesController.authUser = pithosAccount.authUser;
+                pithosPreferencesController.authToken = pithosAccount.authToken;
+            }
+            self.activated = YES;
+            if ([pithosAccount isEqualTo:currentPithosAccount]) {
+                [self showPithosBrowser:self];
+                pithosBrowserController.pithos = pithosAccount.pithos;
+            }
+        }
+        // XXX else maybe show an error message?
     }
     // XXX else maybe show an error message?
 }
 
+- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender {
+    [self savePithosAccounts:self];
+    if ([self.pithosBrowserController operationsPending]) {
+        NSAlert *alert = [[[NSAlert alloc] init] autorelease];
+        [alert setMessageText:@"Pending Operations"];
+        [alert setInformativeText:@"There are pending operations in the browser, do you want to quit and cancel them?"];
+        [alert addButtonWithTitle:@"OK"];
+        [alert addButtonWithTitle:@"Cancel"];
+        NSInteger choice = [alert runModal];
+        if (choice == NSAlertSecondButtonReturn) 
+            return NSTerminateCancel;
+    }
+    return NSTerminateNow;
+}
+
+#pragma mark -
+#pragma mark Properties
+
+- (NSString *)aboutVersion {
+    return [NSString stringWithFormat:@"About Pithos+ %@ (%@)", 
+            [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"], 
+            [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]];
+}
+
+#pragma mark -
+#pragma mark NSMenuDelegate
+
+- (void)menuNeedsUpdate:(NSMenu *)menu {
+    NSMenuItem *menuItem;
+    [menu removeAllItems];
+    if ([menu isEqualTo:accountsMenu]) {
+        [menu setAutoenablesItems:NO];
+        for (PithosAccount *pithosAccount in pithosAccounts) {
+            menuItem = [[[NSMenuItem alloc] initWithTitle:pithosAccount.name 
+                                                   action:@selector(menuChangePithosAccount:) 
+                                            keyEquivalent:@""] autorelease];
+            [menuItem setRepresentedObject:pithosAccount];
+            [menuItem setEnabled:pithosAccount.active];
+            [menuItem setState:((pithosAccount.active && [currentPithosAccount isEqualTo:pithosAccount]) ? NSOnState : NSOffState)];
+            [menu addItem:menuItem];
+        }
+    } else if ([menu isEqualTo:lastSyncMenu]) {
+        NSString *menuItemTitle;
+        [menu setAutoenablesItems:NO];
+        for (PithosAccount *pithosAccount in pithosAccounts) {
+            menuItemTitle = [NSString stringWithFormat:@"%@: %@", 
+                             pithosAccount.name, 
+                             [[[[LastCompletedSyncTransformer alloc] init] autorelease] transformedValue:pithosAccount.syncLastCompleted]];
+            if ([pithosAccount isEqualTo:syncPithosAccount] && [pithosAccount.syncDaemon isSyncing])
+                menuItemTitle = [menuItemTitle stringByAppendingString:@" (syncing)"];
+            menuItem = [[[NSMenuItem alloc] initWithTitle:menuItemTitle 
+                                                   action:@selector(menuChangeSyncActive:) 
+                                            keyEquivalent:@""] autorelease];
+            [menuItem setRepresentedObject:pithosAccount];
+            [menuItem setEnabled:pithosAccount.active];
+            [menuItem setState:((pithosAccount.active && pithosAccount.syncActive) ? NSOnState : NSOffState)];
+            [menu addItem:menuItem];
+        }
+        [menu addItem:[NSMenuItem separatorItem]];
+        [menu addItem:[[[NSMenuItem alloc] initWithTitle:@"Next Sync" 
+                                                  action:@selector(sync) 
+                                           keyEquivalent:@""] autorelease]];
+    }
+}
+
+#pragma mark -
+#pragma mark Actions
+
+- (IBAction)showPithosBrowser:(id)sender {
+    if (!activated)
+        return;
+    [pithosBrowserController showWindow:sender];
+    [[pithosBrowserController window] makeKeyAndOrderFront:sender];
+    [NSApp activateIgnoringOtherApps:YES];
+}
+
+- (IBAction)showPithosPreferences:(id)sender {
+    [pithosPreferencesController showWindow:sender];
+    [[pithosPreferencesController window] makeKeyAndOrderFront:sender];
+    [NSApp activateIgnoringOtherApps:YES];
+}
+
+- (IBAction)aboutPithos:(id)sender {
+    [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:[userDefaults stringForKey:@"aboutURL"]]];
+}
+
+- (void)sync {
+    if (!activated || ![pithosAccounts count])
+        return;
+    NSUInteger syncIndex;
+    BOOL syncPithosAccountFound = [pithosAccounts containsObject:syncPithosAccount];
+    if (syncPithosAccountFound)
+         syncIndex = [pithosAccounts indexOfObject:syncPithosAccount];
+    
+    PithosAccount *singleSyncPithosAccount = nil;
+    for (PithosAccount *pithosAccount in pithosAccounts) {
+        if (!singleSyncPithosAccount && pithosAccount.active && pithosAccount.syncActive && pithosAccount.syncDaemon) {
+            singleSyncPithosAccount = pithosAccount;
+        } else if (singleSyncPithosAccount && pithosAccount.active && pithosAccount.syncActive && pithosAccount.syncDaemon) {
+            singleSyncPithosAccount = nil;
+            break;
+        }
+    }
+    
+    if (syncPithosAccount && syncPithosAccount.active && syncPithosAccount.syncActive && syncPithosAccount.syncDaemon) {
+        // An active syncDaemon was previously syncing
+        if (singleSyncPithosAccount && [singleSyncPithosAccount isEqualTo:syncPithosAccount]) {
+            // It's the only one, sync again
+            [syncPithosAccount.syncDaemon startDaemon];
+            [syncPithosAccount.syncDaemon sync];
+            return;
+        } else if ([syncPithosAccount.syncDaemon isSyncing]) {
+            // It's still syncing, mark it as late and return
+            [syncPithosAccount.syncDaemon syncLate];
+            return;
+        }
+    }
+    PithosAccount *newSyncPithosAccount = nil;
+    if (syncPithosAccountFound) {
+        for (PithosAccount *pithosAccount in [pithosAccounts objectsAtIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(syncIndex + 1, [pithosAccounts count] - syncIndex - 1)]]) {
+            if (pithosAccount.active && pithosAccount.syncActive && pithosAccount.syncDaemon) {
+                newSyncPithosAccount = pithosAccount;
+                break;
+            }
+        }
+        if (!newSyncPithosAccount) {
+            for (PithosAccount *pithosAccount in [pithosAccounts objectsAtIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, syncIndex)]]) {
+                if (pithosAccount.active && pithosAccount.syncActive && pithosAccount.syncDaemon) {
+                    newSyncPithosAccount = pithosAccount;
+                    break;
+                }
+            }
+        }
+    } else {
+        for (PithosAccount *pithosAccount in pithosAccounts) {
+            if (pithosAccount.active && pithosAccount.syncActive && pithosAccount.syncDaemon) {
+                newSyncPithosAccount = pithosAccount;
+                break;
+            }
+        }
+    }        
+    if (newSyncPithosAccount) {
+        // A different syncDaemon is found, sync it
+        self.syncPithosAccount = newSyncPithosAccount;
+        [syncPithosAccount.syncDaemon startDaemon];
+        [syncPithosAccount.syncDaemon sync];
+    } else if (syncPithosAccountFound && syncPithosAccount && syncPithosAccount.active && syncPithosAccount.syncActive && syncPithosAccount.syncDaemon) {
+        [syncPithosAccount.syncDaemon startDaemon];
+        [syncPithosAccount.syncDaemon sync];
+    } else {
+        self.syncPithosAccount = nil;
+    }
+}
+
+- (void)savePithosAccounts:(id)sender {
+    [userDefaults setObject:[NSKeyedArchiver archivedDataWithRootObject:pithosAccounts] forKey:@"pithosAccounts"];
+    [userDefaults synchronize];
+}
+
+- (void)removedPithosAccount:(PithosAccount *)removedPithosAccount {
+    if ([self.currentPithosAccount isEqualTo:removedPithosAccount]) {
+        for (PithosAccount *pithosAccount in pithosAccounts) {
+            if (pithosAccount.active) {
+                self.currentPithosAccount = pithosAccount;
+                pithosBrowserController.pithos = currentPithosAccount.pithos;
+                break;
+            }
+        }
+        if ([self.currentPithosAccount isEqualTo:removedPithosAccount]) {
+            self.activated = NO;
+            [pithosBrowserController.window close];
+            [pithosBrowserController resetBrowser];
+            self.currentPithosAccount = [pithosAccounts objectAtIndex:0];
+        }
+    }
+    if ([self.syncPithosAccount isEqualTo:removedPithosAccount])
+        self.syncPithosAccount = nil;
+}
+
+#pragma mark -
+#pragma mark Menu Actions
+
+- (void)menuChangePithosAccount:(NSMenuItem *)sender {
+    PithosAccount *pithosAccount = (PithosAccount *)[sender representedObject];
+    if (!pithosAccount.active)
+        return;
+    if (![currentPithosAccount isEqualTo:pithosAccount] && [pithosAccounts containsObject:pithosAccount]) {
+        if ([self.pithosBrowserController operationsPending]) {
+            NSAlert *alert = [[[NSAlert alloc] init] autorelease];
+            [alert setMessageText:@"Pending Operations"];
+            [alert setInformativeText:@"There are pending operations in the browser, do you want to change accounts and cancel them?"];
+            [alert addButtonWithTitle:@"OK"];
+            [alert addButtonWithTitle:@"Cancel"];
+            NSInteger choice = [alert runModal];
+            if (choice == NSAlertSecondButtonReturn) 
+                return;
+        }
+        self.currentPithosAccount = pithosAccount;
+        [self showPithosBrowser:self];
+        pithosBrowserController.pithos = currentPithosAccount.pithos;
+    }
+}
+
+- (void)menuChangeSyncActive:(NSMenuItem *)sender {
+    PithosAccount *pithosAccount = (PithosAccount *)[sender representedObject];
+    if (!pithosAccount.active)
+        return;
+    pithosAccount.syncActive = !pithosAccount.syncActive;
+    if (pithosPreferencesController && [pithosPreferencesController.selectedPithosAccount isEqualTo:pithosAccount])
+        pithosPreferencesController.syncActive = pithosAccount.syncActive;
+    [self savePithosAccounts:self];
+}
+
 @end