Merge branch 'master' of https://code.grnet.gr/git/pithos-macos
[pithos-macos] / pithos-macos / pithos_macosAppDelegate.m
1 //
2 //  pithos_macosAppDelegate.m
3 //  pithos-macos
4 //
5 // Copyright 2011-2012 GRNET S.A. All rights reserved.
6 //
7 // Redistribution and use in source and binary forms, with or
8 // without modification, are permitted provided that the following
9 // conditions are met:
10 // 
11 //   1. Redistributions of source code must retain the above
12 //      copyright notice, this list of conditions and the following
13 //      disclaimer.
14 // 
15 //   2. Redistributions in binary form must reproduce the above
16 //      copyright notice, this list of conditions and the following
17 //      disclaimer in the documentation and/or other materials
18 //      provided with the distribution.
19 // 
20 // THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
21 // OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
22 // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
23 // PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
24 // CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25 // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26 // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
27 // USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
28 // AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
29 // LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
30 // ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
31 // POSSIBILITY OF SUCH DAMAGE.
32 // 
33 // The views and conclusions contained in the software and
34 // documentation are those of the authors and should not be
35 // interpreted as representing official policies, either expressed
36 // or implied, of GRNET S.A.
37
38 #import "pithos_macosAppDelegate.h"
39 #import "PithosAccount.h"
40 #import "PithosBrowserController.h"
41 #import "PithosPreferencesController.h"
42 #import "PithosSyncDaemon.h"
43 #import "ASIPithosRequest.h"
44 #import "ASIPithos.h"
45 #import "ASIDownloadCache.h"
46 #import "LastCompletedSyncTransformer.h"
47
48 @implementation pithos_macosAppDelegate
49 @synthesize pithosBrowserController, pithosPreferencesController, alwaysNo, activated, 
50 currentPithosAccount, pithosAccounts, pithosAccountsDictionary, syncPithosAccount, activityFacilityTimeInterval;
51
52 - (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
53     [[NSAppleEventManager sharedAppleEventManager] setEventHandler:self 
54                                                        andSelector:@selector(handleAppleEvent:withReplyEvent:) 
55                                                      forEventClass:kInternetEventClass 
56                                                         andEventID:kAEGetURL];
57     
58     userDefaults = [[NSUserDefaults standardUserDefaults] retain];
59     
60     syncTimeInterval = [userDefaults doubleForKey:@"syncTimeInterval"];
61     if (syncTimeInterval <= 0.0) {
62         syncTimeInterval = 180.0;
63         [userDefaults setDouble:syncTimeInterval forKey:@"syncTimeInterval"];
64         [userDefaults synchronize];
65     }
66
67     activityFacilityTimeInterval = [userDefaults doubleForKey:@"activityFacilityTimeInterval"];
68     if (activityFacilityTimeInterval <= 0.0) {
69         activityFacilityTimeInterval = 0.05;
70         [userDefaults setDouble:activityFacilityTimeInterval forKey:@"activityFacilityTimeInterval"];
71         [userDefaults synchronize];
72     }
73
74     NSData *tmpData = [userDefaults objectForKey:@"pithosAccounts"];
75     NSArray *tmpArray;
76     if (tmpData && (tmpArray = [NSKeyedUnarchiver unarchiveObjectWithData:tmpData]))
77         self.pithosAccounts = [NSMutableArray arrayWithArray:tmpArray];
78     else
79         self.pithosAccounts = [NSMutableArray array];
80     
81     if (![pithosAccounts count]) {
82         [pithosAccounts addObject:[PithosAccount pithosAccount]];
83         self.pithosAccounts = self.pithosAccounts;
84     } else {
85         self.activated = YES;
86     }
87     
88     pithosAccountsDictionary = [[NSMutableDictionary alloc] initWithCapacity:[pithosAccounts count]];
89     for (PithosAccount *pithosAccount in pithosAccounts) {
90         [pithosAccountsDictionary setObject:pithosAccount forKey:pithosAccount.name];
91         if (!currentPithosAccount && pithosAccount.active)
92             currentPithosAccount = [pithosAccount retain];
93     }
94     if (!currentPithosAccount)
95         self.currentPithosAccount = [pithosAccounts objectAtIndex:0];
96     
97     if (currentPithosAccount.active) {
98         [self savePithosAccounts:self];
99         [self showPithosBrowser:self];
100         self.pithosBrowserController.pithos = currentPithosAccount.pithos;
101     } else {
102         // XXX maybe call specifically to go to new account tab
103         [self showPithosPreferences:self];
104     }
105
106     syncTimer = [[NSTimer scheduledTimerWithTimeInterval:syncTimeInterval 
107                                                   target:self 
108                                                 selector:@selector(sync) 
109                                                 userInfo:nil 
110                                                  repeats:YES] retain];
111     [syncTimer fire];
112 }
113
114 // Based on: http://cocoatutorial.grapewave.com/2010/01/creating-a-status-bar-application/
115 // and: http://www.cocoadev.com/index.pl?ThumbnailImages
116 - (void)awakeFromNib {
117     NSImage *sourceImage = [NSImage imageNamed:@"pithos-large.png"];
118     
119     NSImage *smallImage = [[[NSImage alloc] initWithSize:NSMakeSize(18, 18)] autorelease];
120     [smallImage lockFocus];
121     [[NSGraphicsContext currentContext] setImageInterpolation:NSImageInterpolationHigh];
122     [sourceImage setSize:NSMakeSize(18, 18)];
123     [sourceImage compositeToPoint:NSZeroPoint operation:NSCompositeCopy];
124     [smallImage unlockFocus];
125     
126     statusItem = [[[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength] retain];
127     [statusItem setMenu:statusMenu];
128     [statusItem setImage:sourceImage];
129     [statusItem setHighlightMode:YES];
130     
131     self.alwaysNo = NO;
132 }
133
134 - (void)handleAppleEvent:(NSAppleEventDescriptor *)event withReplyEvent: (NSAppleEventDescriptor *)replyEvent {
135     NSURL *url = [NSURL URLWithString:[[event paramDescriptorForKeyword:keyDirectObject] stringValue]];
136     NSString *host = [url host];
137         NSString *query = [url query];
138     PithosAccount *pithosAccount = [pithosAccountsDictionary objectForKey:[url lastPathComponent]];
139     NSProcessInfo *processInfo = [NSProcessInfo processInfo];
140     if ([host isEqualToString:[NSString stringWithFormat:@"%@_%d", [processInfo processName], [processInfo processIdentifier]]] && 
141         pithosAccount && query) {
142         // user=
143         NSString *authUser;
144         NSRange userRange = [query rangeOfString:@"user=" options:NSCaseInsensitiveSearch];
145         if (userRange.length == 0)
146             // XXX maybe show an error message?
147             return;
148         NSUInteger authUserStartLocation = userRange.location + userRange.length;
149         NSRange userEndRange = [query rangeOfString:@"&" options:NSCaseInsensitiveSearch 
150                                               range:NSMakeRange(authUserStartLocation, [query length] - authUserStartLocation)];
151         if (userEndRange.length) {
152             authUser = [[query substringWithRange:NSMakeRange(authUserStartLocation, userEndRange.location - authUserStartLocation)]
153                         stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
154         } else {
155             authUser = [[query substringFromIndex:authUserStartLocation]
156                         stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
157         }
158         // token=
159         NSString *authToken;
160         NSRange tokenRange = [query rangeOfString:@"token=" options:NSCaseInsensitiveSearch];
161         if (tokenRange.length == 0)
162             // XXX maybe show an error message?
163             return;
164         NSUInteger authTokenStartLocation = tokenRange.location + tokenRange.length;
165         NSRange tokenEndRange = [query rangeOfString:@"&" options:NSCaseInsensitiveSearch 
166                                               range:NSMakeRange(authTokenStartLocation, [query length] - authTokenStartLocation)];
167         if (tokenEndRange.length) {
168             authToken = [[query substringWithRange:NSMakeRange(authTokenStartLocation, tokenEndRange.location - authTokenStartLocation)]
169                          stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
170         } else {
171             authToken = [[query substringFromIndex:authTokenStartLocation]
172                          stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
173         }
174         
175         NSLog(@"query authUser: '%@', authToken: '%@'", authUser, authToken);
176         if ([authUser length] && [authToken length]) {
177             [pithosAccount authenticateWithServerURL:nil authUser:authUser authToken:authToken];
178             [self savePithosAccounts:self];
179             if (self.pithosPreferencesController && [self.pithosPreferencesController.selectedPithosAccount isEqualTo:pithosAccount]) {
180                 self.pithosPreferencesController.authUser = pithosAccount.authUser;
181                 self.pithosPreferencesController.authToken = pithosAccount.authToken;
182             }
183             self.activated = YES;
184             if ([pithosAccount isEqualTo:currentPithosAccount]) {
185                 [self showPithosBrowser:self];
186                 self.pithosBrowserController.pithos = pithosAccount.pithos;
187             }
188         }
189         // XXX else maybe show an error message?
190     }
191     // XXX else maybe show an error message?
192 }
193
194 - (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender {
195     [self savePithosAccounts:self];
196     if ([self.pithosBrowserController operationsPending]) {
197         NSAlert *alert = [[[NSAlert alloc] init] autorelease];
198         [alert setMessageText:@"Pending Operations"];
199         [alert setInformativeText:@"There are pending operations in the browser, do you want to quit and cancel them?"];
200         [alert addButtonWithTitle:@"OK"];
201         [alert addButtonWithTitle:@"Cancel"];
202         NSInteger choice = [alert runModal];
203         if (choice == NSAlertSecondButtonReturn) 
204             return NSTerminateCancel;
205     }
206     return NSTerminateNow;
207 }
208
209 #pragma mark -
210 #pragma mark Properties
211
212 - (PithosBrowserController *)pithosBrowserController {
213     if (!pithosBrowserController) {
214         pithosBrowserController = [[PithosBrowserController alloc] init];
215     }
216     return pithosBrowserController;
217 }
218
219 - (PithosPreferencesController *)pithosPreferencesController {
220     if (!pithosPreferencesController) {
221         pithosPreferencesController = [[PithosPreferencesController alloc] init];
222     }
223     return pithosPreferencesController;
224 }
225
226 #pragma mark -
227 #pragma mark NSMenuDelegate
228
229 - (void)menuNeedsUpdate:(NSMenu *)menu {
230     NSMenuItem *menuItem;
231     [menu removeAllItems];
232     if ([menu isEqualTo:accountsMenu]) {
233         [menu setAutoenablesItems:NO];
234         for (PithosAccount *pithosAccount in pithosAccounts) {
235             menuItem = [[[NSMenuItem alloc] initWithTitle:pithosAccount.name 
236                                                    action:@selector(menuChangePithosAccount:) 
237                                             keyEquivalent:@""] autorelease];
238             [menuItem setRepresentedObject:pithosAccount];
239             [menuItem setEnabled:pithosAccount.active];
240             [menuItem setState:((pithosAccount.active && [currentPithosAccount isEqualTo:pithosAccount]) ? NSOnState : NSOffState)];
241             [menu addItem:menuItem];
242         }
243     } else if ([menu isEqualTo:lastSyncMenu]) {
244         NSString *menuItemTitle;
245         [menu setAutoenablesItems:NO];
246         for (PithosAccount *pithosAccount in pithosAccounts) {
247             menuItemTitle = [NSString stringWithFormat:@"%@: %@", 
248                              pithosAccount.name, 
249                              [[[[LastCompletedSyncTransformer alloc] init] autorelease] transformedValue:pithosAccount.syncLastCompleted]];
250             if ([pithosAccount isEqualTo:syncPithosAccount] && [pithosAccount.syncDaemon isSyncing])
251                 menuItemTitle = [menuItemTitle stringByAppendingString:@" (syncing)"];
252 //            menuItem = [[[NSMenuItem alloc] initWithTitle:menuItemTitle 
253 //                                                   action:@selector(menuChangeSyncActive:) 
254 //                                            keyEquivalent:@""] autorelease];
255 //            [menuItem setRepresentedObject:pithosAccount];
256 //            [menuItem setEnabled:pithosAccount.active];
257 //            [menuItem setState:((pithosAccount.active && pithosAccount.syncActive) ? NSOnState : NSOffState)];
258             menuItem = [[[NSMenuItem alloc] initWithTitle:menuItemTitle action:nil keyEquivalent:@""] autorelease];
259             [menuItem setEnabled:NO];
260             [menuItem setState:NO];
261             [menu addItem:menuItem];
262         }
263         [menu addItem:[NSMenuItem separatorItem]];
264         [menu addItem:[[[NSMenuItem alloc] initWithTitle:@"Next Sync" 
265                                                   action:@selector(sync) 
266                                            keyEquivalent:@""] autorelease]];
267     }
268 }
269
270 #pragma mark -
271 #pragma mark Actions
272
273 - (IBAction)showPithosBrowser:(id)sender {
274     if (!activated)
275         return;
276     [self.pithosBrowserController showWindow:sender];
277     [[self.pithosBrowserController window] makeKeyAndOrderFront:sender];
278     [NSApp activateIgnoringOtherApps:YES];
279 }
280
281 - (IBAction)showPithosPreferences:(id)sender {
282     [self.pithosPreferencesController showWindow:sender];
283     [[self.pithosPreferencesController window] makeKeyAndOrderFront:sender];
284     [NSApp activateIgnoringOtherApps:YES];
285 }
286
287 - (IBAction)showPithosAbout:(id)sender {
288     [NSApp orderFrontStandardAboutPanel:sender];
289     [NSApp activateIgnoringOtherApps:YES];
290 }
291
292 - (void)sync {
293     if (!activated || ![pithosAccounts count])
294         return;
295     NSUInteger syncIndex;
296     BOOL syncPithosAccountFound = [pithosAccounts containsObject:syncPithosAccount];
297     if (syncPithosAccountFound)
298          syncIndex = [pithosAccounts indexOfObject:syncPithosAccount];
299     
300     PithosAccount *singleSyncPithosAccount = nil;
301     for (PithosAccount *pithosAccount in pithosAccounts) {
302         if (!singleSyncPithosAccount && pithosAccount.active && pithosAccount.syncActive && pithosAccount.syncDaemon) {
303             singleSyncPithosAccount = pithosAccount;
304         } else if (singleSyncPithosAccount && pithosAccount.active && pithosAccount.syncActive && pithosAccount.syncDaemon) {
305             singleSyncPithosAccount = nil;
306             break;
307         }
308     }
309     
310     if (syncPithosAccount && syncPithosAccount.active && syncPithosAccount.syncActive && syncPithosAccount.syncDaemon) {
311         // An active syncDaemon was previously syncing
312         if (singleSyncPithosAccount && [singleSyncPithosAccount isEqualTo:syncPithosAccount]) {
313             // It's the only one, sync again
314             [syncPithosAccount.syncDaemon startDaemon];
315             [syncPithosAccount.syncDaemon sync];
316             return;
317         } else if ([syncPithosAccount.syncDaemon isSyncing]) {
318             // It's still syncing, mark it as late and return
319             [syncPithosAccount.syncDaemon syncLate];
320             return;
321         }
322     }
323     PithosAccount *newSyncPithosAccount = nil;
324     if (syncPithosAccountFound) {
325         for (PithosAccount *pithosAccount in [pithosAccounts objectsAtIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(syncIndex + 1, [pithosAccounts count] - syncIndex - 1)]]) {
326             if (pithosAccount.active && pithosAccount.syncActive && pithosAccount.syncDaemon) {
327                 newSyncPithosAccount = pithosAccount;
328                 break;
329             }
330         }
331         if (!newSyncPithosAccount) {
332             for (PithosAccount *pithosAccount in [pithosAccounts objectsAtIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, syncIndex)]]) {
333                 if (pithosAccount.active && pithosAccount.syncActive && pithosAccount.syncDaemon) {
334                     newSyncPithosAccount = pithosAccount;
335                     break;
336                 }
337             }
338         }
339     } else {
340         for (PithosAccount *pithosAccount in pithosAccounts) {
341             if (pithosAccount.active && pithosAccount.syncActive && pithosAccount.syncDaemon) {
342                 newSyncPithosAccount = pithosAccount;
343                 break;
344             }
345         }
346     }        
347     if (newSyncPithosAccount) {
348         // A different syncDaemon is found, sync it
349         self.syncPithosAccount = newSyncPithosAccount;
350         [syncPithosAccount.syncDaemon startDaemon];
351         [syncPithosAccount.syncDaemon sync];
352     } else if (syncPithosAccountFound && syncPithosAccount && syncPithosAccount.active && syncPithosAccount.syncActive && syncPithosAccount.syncDaemon) {
353         [syncPithosAccount.syncDaemon startDaemon];
354         [syncPithosAccount.syncDaemon sync];
355     } else {
356         self.syncPithosAccount = nil;
357     }
358 }
359
360 - (void)savePithosAccounts:(id)sender {
361     [userDefaults setObject:[NSKeyedArchiver archivedDataWithRootObject:pithosAccounts] forKey:@"pithosAccounts"];
362     [userDefaults synchronize];
363 }
364
365 - (void)removedPithosAccount:(PithosAccount *)removedPithosAccount {
366     if ([self.currentPithosAccount isEqualTo:removedPithosAccount]) {
367         for (PithosAccount *pithosAccount in pithosAccounts) {
368             if (pithosAccount.active) {
369                 self.currentPithosAccount = pithosAccount;
370                 self.pithosBrowserController.pithos = currentPithosAccount.pithos;
371                 break;
372             }
373         }
374         if ([self.currentPithosAccount isEqualTo:removedPithosAccount]) {
375             self.activated = NO;
376             [self.pithosBrowserController.window close];
377             [self.pithosBrowserController resetBrowser];
378             self.currentPithosAccount = [pithosAccounts objectAtIndex:0];
379         }
380     }
381     if ([self.syncPithosAccount isEqualTo:removedPithosAccount])
382         self.syncPithosAccount = nil;
383 }
384
385 #pragma mark -
386 #pragma mark Menu Actions
387
388 - (void)menuChangePithosAccount:(NSMenuItem *)sender {
389     PithosAccount *pithosAccount = (PithosAccount *)[sender representedObject];
390     if (!pithosAccount.active)
391         return;
392     if (![currentPithosAccount isEqualTo:pithosAccount] && [pithosAccounts containsObject:pithosAccount]) {
393         if ([self.pithosBrowserController operationsPending]) {
394             NSAlert *alert = [[[NSAlert alloc] init] autorelease];
395             [alert setMessageText:@"Pending Operations"];
396             [alert setInformativeText:@"There are pending operations in the browser, do you want to change accounts and cancel them?"];
397             [alert addButtonWithTitle:@"OK"];
398             [alert addButtonWithTitle:@"Cancel"];
399             NSInteger choice = [alert runModal];
400             if (choice == NSAlertSecondButtonReturn) 
401                 return;
402         }
403         self.currentPithosAccount = pithosAccount;
404         [self showPithosBrowser:self];
405         self.pithosBrowserController.pithos = currentPithosAccount.pithos;
406     }
407 }
408
409 //- (void)menuChangeSyncActive:(NSMenuItem *)sender {
410 //    PithosAccount *pithosAccount = (PithosAccount *)[sender representedObject];
411 //    if (!pithosAccount.active)
412 //        return;
413 //    pithosAccount.syncActive = !pithosAccount.syncActive;
414 //    if (self.pithosPreferencesController && [self.pithosPreferencesController.selectedPithosAccount isEqualTo:pithosAccount])
415 //        self.pithosPreferencesController.syncActive = pithosAccount.syncActive;
416 //    [self savePithosAccounts:self];
417 //}
418
419 @end