2 // pithos_macosAppDelegate.m
5 // Copyright 2011-2012 GRNET S.A. All rights reserved.
7 // Redistribution and use in source and binary forms, with or
8 // without modification, are permitted provided that the following
11 // 1. Redistributions of source code must retain the above
12 // copyright notice, this list of conditions and the following
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.
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.
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.
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"
45 #import "ASIDownloadCache.h"
46 #import "LastCompletedSyncTransformer.h"
48 @implementation pithos_macosAppDelegate
49 @synthesize pithosBrowserController, pithosPreferencesController, alwaysNo, openAtLoginEnabled, openAtLogin, activated,
50 currentPithosAccount, pithosAccounts, pithosAccountsDictionary, syncPithosAccount, activityFacilityTimeInterval;
52 - (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
53 [[NSAppleEventManager sharedAppleEventManager] setEventHandler:self
54 andSelector:@selector(handleAppleEvent:withReplyEvent:)
55 forEventClass:kInternetEventClass
56 andEventID:kAEGetURL];
58 // Based on: https://github.com/Mozketo/LaunchAtLoginController
59 // and: http://cocoatutorial.grapewave.com/2010/02/creating-andor-removing-a-login-item/
60 loginItems = LSSharedFileListCreate(NULL, kLSSharedFileListSessionLoginItems, NULL);
62 LSSharedFileListAddObserver(loginItems, CFRunLoopGetMain(), (CFStringRef)NSDefaultRunLoopMode, LSSharedFileListChanged, self);
63 LSSharedFileListChanged(loginItems, self);
64 self.openAtLoginEnabled = YES;
67 userDefaults = [[NSUserDefaults standardUserDefaults] retain];
69 syncTimeInterval = [userDefaults doubleForKey:@"syncTimeInterval"];
70 if (syncTimeInterval <= 0.0) {
71 syncTimeInterval = 180.0;
72 [userDefaults setDouble:syncTimeInterval forKey:@"syncTimeInterval"];
73 [userDefaults synchronize];
76 activityFacilityTimeInterval = [userDefaults doubleForKey:@"activityFacilityTimeInterval"];
77 if (activityFacilityTimeInterval <= 0.0) {
78 activityFacilityTimeInterval = 0.05;
79 [userDefaults setDouble:activityFacilityTimeInterval forKey:@"activityFacilityTimeInterval"];
80 [userDefaults synchronize];
83 NSData *tmpData = [userDefaults objectForKey:@"pithosAccounts"];
85 if (tmpData && (tmpArray = [NSKeyedUnarchiver unarchiveObjectWithData:tmpData]))
86 self.pithosAccounts = [NSMutableArray arrayWithArray:tmpArray];
88 self.pithosAccounts = [NSMutableArray array];
90 if (![pithosAccounts count]) {
91 [pithosAccounts addObject:[PithosAccount pithosAccount]];
92 self.pithosAccounts = self.pithosAccounts;
97 pithosAccountsDictionary = [[NSMutableDictionary alloc] initWithCapacity:[pithosAccounts count]];
98 for (PithosAccount *pithosAccount in pithosAccounts) {
99 [pithosAccountsDictionary setObject:pithosAccount forKey:pithosAccount.name];
100 if (!currentPithosAccount && pithosAccount.active)
101 currentPithosAccount = [pithosAccount retain];
103 if (!currentPithosAccount)
104 self.currentPithosAccount = [pithosAccounts objectAtIndex:0];
106 if (currentPithosAccount.active) {
107 [self savePithosAccounts:self];
108 [self showPithosBrowser:self];
109 self.pithosBrowserController.pithos = currentPithosAccount.pithos;
111 // XXX maybe call specifically to go to new account tab
112 [self showPithosPreferences:self];
115 syncTimer = [[NSTimer scheduledTimerWithTimeInterval:syncTimeInterval
117 selector:@selector(sync)
119 repeats:YES] retain];
123 // Based on: http://cocoatutorial.grapewave.com/2010/01/creating-a-status-bar-application/
124 // and: http://www.cocoadev.com/index.pl?ThumbnailImages
125 - (void)awakeFromNib {
126 NSImage *sourceImage = [NSImage imageNamed:@"pithos-large.png"];
128 NSImage *smallImage = [[[NSImage alloc] initWithSize:NSMakeSize(18, 18)] autorelease];
129 [smallImage lockFocus];
130 [[NSGraphicsContext currentContext] setImageInterpolation:NSImageInterpolationHigh];
131 [sourceImage setSize:NSMakeSize(18, 18)];
132 [sourceImage compositeToPoint:NSZeroPoint operation:NSCompositeCopy];
133 [smallImage unlockFocus];
135 statusItem = [[[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength] retain];
136 [statusItem setMenu:statusMenu];
137 [statusItem setImage:sourceImage];
138 [statusItem setHighlightMode:YES];
143 - (void)handleAppleEvent:(NSAppleEventDescriptor *)event withReplyEvent: (NSAppleEventDescriptor *)replyEvent {
144 NSURL *url = [NSURL URLWithString:[[event paramDescriptorForKeyword:keyDirectObject] stringValue]];
145 NSString *host = [url host];
146 NSString *query = [url query];
147 PithosAccount *pithosAccount = [pithosAccountsDictionary objectForKey:[url lastPathComponent]];
148 NSProcessInfo *processInfo = [NSProcessInfo processInfo];
149 if ([host isEqualToString:[NSString stringWithFormat:@"%d", [processInfo processIdentifier]]] && pithosAccount && query) {
152 NSRange userRange = [query rangeOfString:@"user=" options:NSCaseInsensitiveSearch];
153 if (userRange.length == 0)
154 // XXX maybe show an error message?
156 NSUInteger authUserStartLocation = userRange.location + userRange.length;
157 NSRange userEndRange = [query rangeOfString:@"&" options:NSCaseInsensitiveSearch
158 range:NSMakeRange(authUserStartLocation, [query length] - authUserStartLocation)];
159 if (userEndRange.length) {
160 authUser = [[query substringWithRange:NSMakeRange(authUserStartLocation, userEndRange.location - authUserStartLocation)]
161 stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
163 authUser = [[query substringFromIndex:authUserStartLocation]
164 stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
168 NSRange tokenRange = [query rangeOfString:@"token=" options:NSCaseInsensitiveSearch];
169 if (tokenRange.length == 0)
170 // XXX maybe show an error message?
172 NSUInteger authTokenStartLocation = tokenRange.location + tokenRange.length;
173 NSRange tokenEndRange = [query rangeOfString:@"&" options:NSCaseInsensitiveSearch
174 range:NSMakeRange(authTokenStartLocation, [query length] - authTokenStartLocation)];
175 if (tokenEndRange.length) {
176 authToken = [[query substringWithRange:NSMakeRange(authTokenStartLocation, tokenEndRange.location - authTokenStartLocation)]
177 stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
179 authToken = [[query substringFromIndex:authTokenStartLocation]
180 stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
183 NSLog(@"query authUser: '%@', authToken: '%@'", authUser, authToken);
184 if ([authUser length] && [authToken length]) {
185 [pithosAccount authenticateWithServerURL:nil authUser:authUser authToken:authToken];
186 [self savePithosAccounts:self];
187 if (self.pithosPreferencesController && [self.pithosPreferencesController.selectedPithosAccount isEqualTo:pithosAccount]) {
188 self.pithosPreferencesController.authUser = pithosAccount.authUser;
189 self.pithosPreferencesController.authToken = pithosAccount.authToken;
191 self.activated = YES;
192 if ([pithosAccount isEqualTo:currentPithosAccount]) {
193 [self showPithosBrowser:self];
194 self.pithosBrowserController.pithos = pithosAccount.pithos;
197 // XXX else maybe show an error message?
199 // XXX else maybe show an error message?
202 - (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender {
203 [self savePithosAccounts:self];
204 if ([self.pithosBrowserController operationsPending]) {
205 NSAlert *alert = [[[NSAlert alloc] init] autorelease];
206 [alert setMessageText:@"Pending Operations"];
207 [alert setInformativeText:@"There are pending operations in the browser, do you want to quit and cancel them?"];
208 [alert addButtonWithTitle:@"OK"];
209 [alert addButtonWithTitle:@"Cancel"];
210 NSInteger choice = [alert runModal];
211 if (choice == NSAlertSecondButtonReturn)
212 return NSTerminateCancel;
215 LSSharedFileListRemoveObserver(loginItems, CFRunLoopGetMain(), (CFStringRef)NSDefaultRunLoopMode, LSSharedFileListChanged, self);
216 CFRelease(loginItems);
218 return NSTerminateNow;
222 #pragma mark Callbacks
224 - (void)loginItemsChanged {
225 NSURL *appURL = [[NSBundle mainBundle] bundleURL];
226 LSSharedFileListItemRef appItem = NULL;
227 NSArray *snapshot = [NSMakeCollectable(LSSharedFileListCopySnapshot(loginItems, NULL)) autorelease];
228 for (id itemObject in snapshot) {
229 LSSharedFileListItemRef item = (LSSharedFileListItemRef)itemObject;
230 UInt32 resolutionFlags = kLSSharedFileListNoUserInteraction | kLSSharedFileListDoNotMountVolumes;
231 CFURLRef currentItemURL = NULL;
232 LSSharedFileListItemResolve(item, resolutionFlags, ¤tItemURL, NULL);
233 if (currentItemURL && CFEqual(currentItemURL, appURL)) {
234 CFRelease(currentItemURL);
239 CFRelease(currentItemURL);
242 if (appItem && (!openAtLogin || !openAtLoginEnabled))
243 self.openAtLogin = YES;
244 else if (!appItem && (openAtLogin || !openAtLoginEnabled))
245 self.openAtLogin = NO;
248 void LSSharedFileListChanged(LSSharedFileListRef inList, void *context) {
249 pithos_macosAppDelegate *self = (id)context;
250 [self loginItemsChanged];
254 #pragma mark Properties
256 - (PithosBrowserController *)pithosBrowserController {
257 if (!pithosBrowserController) {
258 pithosBrowserController = [[PithosBrowserController alloc] init];
260 return pithosBrowserController;
263 - (PithosPreferencesController *)pithosPreferencesController {
264 if (!pithosPreferencesController) {
265 pithosPreferencesController = [[PithosPreferencesController alloc] init];
267 return pithosPreferencesController;
270 - (void)setOpenAtLogin:(BOOL)anOpenAtLogin {
271 if (!openAtLoginEnabled) {
272 openAtLogin = anOpenAtLogin;
273 } else if (anOpenAtLogin != openAtLogin) {
274 NSURL *appURL = [NSURL fileURLWithPath:[[NSBundle mainBundle] bundlePath]];
275 LSSharedFileListItemRef appItem = NULL;
276 NSArray *snapshot = [NSMakeCollectable(LSSharedFileListCopySnapshot(loginItems, NULL)) autorelease];
277 for (id itemObject in snapshot) {
278 LSSharedFileListItemRef item = (LSSharedFileListItemRef)itemObject;
279 UInt32 resolutionFlags = kLSSharedFileListNoUserInteraction | kLSSharedFileListDoNotMountVolumes;
280 CFURLRef currentItemURL = NULL;
281 LSSharedFileListItemResolve(item, resolutionFlags, ¤tItemURL, NULL);
282 if (currentItemURL && CFEqual(currentItemURL, appURL)) {
283 CFRelease(currentItemURL);
288 CFRelease(currentItemURL);
293 LSSharedFileListInsertItemURL(loginItems, kLSSharedFileListItemBeforeFirst, NULL, NULL, (CFURLRef)appURL, NULL, NULL);
297 LSSharedFileListItemRemove(loginItems, appItem);
304 #pragma mark NSMenuDelegate
306 - (void)menuNeedsUpdate:(NSMenu *)menu {
307 NSMenuItem *menuItem;
308 [menu removeAllItems];
309 if ([menu isEqualTo:accountsMenu]) {
310 [menu setAutoenablesItems:NO];
311 for (PithosAccount *pithosAccount in pithosAccounts) {
312 menuItem = [[[NSMenuItem alloc] initWithTitle:pithosAccount.name
313 action:@selector(menuChangePithosAccount:)
314 keyEquivalent:@""] autorelease];
315 [menuItem setRepresentedObject:pithosAccount];
316 [menuItem setEnabled:pithosAccount.active];
317 [menuItem setState:((pithosAccount.active && [currentPithosAccount isEqualTo:pithosAccount]) ? NSOnState : NSOffState)];
318 [menu addItem:menuItem];
320 } else if ([menu isEqualTo:lastSyncMenu]) {
321 NSString *menuItemTitle;
322 [menu setAutoenablesItems:NO];
323 for (PithosAccount *pithosAccount in pithosAccounts) {
324 menuItemTitle = [NSString stringWithFormat:@"%@: %@",
326 [[[[LastCompletedSyncTransformer alloc] init] autorelease] transformedValue:pithosAccount.syncLastCompleted]];
327 if ([pithosAccount isEqualTo:syncPithosAccount] && [pithosAccount.syncDaemon isSyncing])
328 menuItemTitle = [menuItemTitle stringByAppendingString:@" (syncing)"];
329 // menuItem = [[[NSMenuItem alloc] initWithTitle:menuItemTitle
330 // action:@selector(menuChangeSyncActive:)
331 // keyEquivalent:@""] autorelease];
332 // [menuItem setRepresentedObject:pithosAccount];
333 // [menuItem setEnabled:pithosAccount.active];
334 // [menuItem setState:((pithosAccount.active && pithosAccount.syncActive) ? NSOnState : NSOffState)];
335 menuItem = [[[NSMenuItem alloc] initWithTitle:menuItemTitle action:nil keyEquivalent:@""] autorelease];
336 [menuItem setEnabled:NO];
337 [menuItem setState:NO];
338 [menu addItem:menuItem];
340 [menu addItem:[NSMenuItem separatorItem]];
341 [menu addItem:[[[NSMenuItem alloc] initWithTitle:@"Next Sync"
342 action:@selector(sync)
343 keyEquivalent:@""] autorelease]];
350 - (IBAction)showPithosBrowser:(id)sender {
353 [self.pithosBrowserController showWindow:sender];
354 [[self.pithosBrowserController window] makeKeyAndOrderFront:sender];
355 [NSApp activateIgnoringOtherApps:YES];
358 - (IBAction)showPithosPreferences:(id)sender {
359 [self.pithosPreferencesController showWindow:sender];
360 [[self.pithosPreferencesController window] makeKeyAndOrderFront:sender];
361 [NSApp activateIgnoringOtherApps:YES];
364 - (IBAction)showPithosAbout:(id)sender {
365 [NSApp orderFrontStandardAboutPanel:sender];
366 [NSApp activateIgnoringOtherApps:YES];
370 if (!activated || ![pithosAccounts count])
372 NSUInteger syncIndex;
373 BOOL syncPithosAccountFound = [pithosAccounts containsObject:syncPithosAccount];
374 if (syncPithosAccountFound)
375 syncIndex = [pithosAccounts indexOfObject:syncPithosAccount];
377 PithosAccount *singleSyncPithosAccount = nil;
378 for (PithosAccount *pithosAccount in pithosAccounts) {
379 if (!singleSyncPithosAccount && pithosAccount.active && pithosAccount.syncActive && pithosAccount.syncDaemon) {
380 singleSyncPithosAccount = pithosAccount;
381 } else if (singleSyncPithosAccount && pithosAccount.active && pithosAccount.syncActive && pithosAccount.syncDaemon) {
382 singleSyncPithosAccount = nil;
387 if (syncPithosAccount && syncPithosAccount.active && syncPithosAccount.syncActive && syncPithosAccount.syncDaemon) {
388 // An active syncDaemon was previously syncing
389 if (singleSyncPithosAccount && [singleSyncPithosAccount isEqualTo:syncPithosAccount]) {
390 // It's the only one, sync again
391 [syncPithosAccount.syncDaemon startDaemon];
392 [syncPithosAccount.syncDaemon sync];
394 } else if ([syncPithosAccount.syncDaemon isSyncing]) {
395 // It's still syncing, mark it as late and return
396 [syncPithosAccount.syncDaemon syncLate];
400 PithosAccount *newSyncPithosAccount = nil;
401 if (syncPithosAccountFound) {
402 for (PithosAccount *pithosAccount in [pithosAccounts objectsAtIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(syncIndex + 1, [pithosAccounts count] - syncIndex - 1)]]) {
403 if (pithosAccount.active && pithosAccount.syncActive && pithosAccount.syncDaemon) {
404 newSyncPithosAccount = pithosAccount;
408 if (!newSyncPithosAccount) {
409 for (PithosAccount *pithosAccount in [pithosAccounts objectsAtIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, syncIndex)]]) {
410 if (pithosAccount.active && pithosAccount.syncActive && pithosAccount.syncDaemon) {
411 newSyncPithosAccount = pithosAccount;
417 for (PithosAccount *pithosAccount in pithosAccounts) {
418 if (pithosAccount.active && pithosAccount.syncActive && pithosAccount.syncDaemon) {
419 newSyncPithosAccount = pithosAccount;
424 if (newSyncPithosAccount) {
425 // A different syncDaemon is found, sync it
426 self.syncPithosAccount = newSyncPithosAccount;
427 [syncPithosAccount.syncDaemon startDaemon];
428 [syncPithosAccount.syncDaemon sync];
429 } else if (syncPithosAccountFound && syncPithosAccount && syncPithosAccount.active && syncPithosAccount.syncActive && syncPithosAccount.syncDaemon) {
430 [syncPithosAccount.syncDaemon startDaemon];
431 [syncPithosAccount.syncDaemon sync];
433 self.syncPithosAccount = nil;
437 - (void)savePithosAccounts:(id)sender {
438 [userDefaults setObject:[NSKeyedArchiver archivedDataWithRootObject:pithosAccounts] forKey:@"pithosAccounts"];
439 [userDefaults synchronize];
442 - (void)removedPithosAccount:(PithosAccount *)removedPithosAccount {
443 if ([self.currentPithosAccount isEqualTo:removedPithosAccount]) {
444 for (PithosAccount *pithosAccount in pithosAccounts) {
445 if (pithosAccount.active) {
446 self.currentPithosAccount = pithosAccount;
447 self.pithosBrowserController.pithos = currentPithosAccount.pithos;
451 if ([self.currentPithosAccount isEqualTo:removedPithosAccount]) {
453 [self.pithosBrowserController.window close];
454 [self.pithosBrowserController resetBrowser];
455 self.currentPithosAccount = [pithosAccounts objectAtIndex:0];
458 if ([self.syncPithosAccount isEqualTo:removedPithosAccount])
459 self.syncPithosAccount = nil;
463 #pragma mark Menu Actions
465 - (void)menuChangePithosAccount:(NSMenuItem *)sender {
466 PithosAccount *pithosAccount = (PithosAccount *)[sender representedObject];
467 if (!pithosAccount.active)
469 if (![currentPithosAccount isEqualTo:pithosAccount] && [pithosAccounts containsObject:pithosAccount]) {
470 if ([self.pithosBrowserController operationsPending]) {
471 NSAlert *alert = [[[NSAlert alloc] init] autorelease];
472 [alert setMessageText:@"Pending Operations"];
473 [alert setInformativeText:@"There are pending operations in the browser, do you want to change accounts and cancel them?"];
474 [alert addButtonWithTitle:@"OK"];
475 [alert addButtonWithTitle:@"Cancel"];
476 NSInteger choice = [alert runModal];
477 if (choice == NSAlertSecondButtonReturn)
480 self.currentPithosAccount = pithosAccount;
481 [self showPithosBrowser:self];
482 self.pithosBrowserController.pithos = currentPithosAccount.pithos;
486 //- (void)menuChangeSyncActive:(NSMenuItem *)sender {
487 // PithosAccount *pithosAccount = (PithosAccount *)[sender representedObject];
488 // if (!pithosAccount.active)
490 // pithosAccount.syncActive = !pithosAccount.syncActive;
491 // if (self.pithosPreferencesController && [self.pithosPreferencesController.selectedPithosAccount isEqualTo:pithosAccount])
492 // self.pithosPreferencesController.syncActive = pithosAccount.syncActive;
493 // [self savePithosAccounts:self];