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 "LastCompletedSyncTransformer.h"
47 @implementation pithos_macosAppDelegate
48 @synthesize pithosBrowserController, pithosPreferencesController, alwaysNo, openAtLoginEnabled, openAtLogin, activated,
49 currentPithosAccount, pithosAccounts, pithosAccountsDictionary, syncPithosAccount, activityFacilityTimeInterval;
51 - (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
52 [[NSAppleEventManager sharedAppleEventManager] setEventHandler:self
53 andSelector:@selector(handleAppleEvent:withReplyEvent:)
54 forEventClass:kInternetEventClass
55 andEventID:kAEGetURL];
57 // Based on: https://github.com/Mozketo/LaunchAtLoginController
58 // and: http://cocoatutorial.grapewave.com/2010/02/creating-andor-removing-a-login-item/
59 loginItems = LSSharedFileListCreate(NULL, kLSSharedFileListSessionLoginItems, NULL);
61 LSSharedFileListAddObserver(loginItems, CFRunLoopGetMain(), (CFStringRef)NSDefaultRunLoopMode, LSSharedFileListChanged, self);
62 LSSharedFileListChanged(loginItems, self);
63 self.openAtLoginEnabled = YES;
66 userDefaults = [[NSUserDefaults standardUserDefaults] retain];
68 syncTimeInterval = [userDefaults doubleForKey:@"syncTimeInterval"];
69 if (syncTimeInterval <= 0.0) {
70 syncTimeInterval = 180.0;
71 [userDefaults setDouble:syncTimeInterval forKey:@"syncTimeInterval"];
72 [userDefaults synchronize];
75 activityFacilityTimeInterval = [userDefaults doubleForKey:@"activityFacilityTimeInterval"];
76 if (activityFacilityTimeInterval <= 0.0) {
77 activityFacilityTimeInterval = 0.05;
78 [userDefaults setDouble:activityFacilityTimeInterval forKey:@"activityFacilityTimeInterval"];
79 [userDefaults synchronize];
82 NSData *tmpData = [userDefaults objectForKey:@"pithosAccounts"];
84 if (tmpData && (tmpArray = [NSKeyedUnarchiver unarchiveObjectWithData:tmpData]))
85 self.pithosAccounts = [NSMutableArray arrayWithArray:tmpArray];
87 self.pithosAccounts = [NSMutableArray array];
89 if (![pithosAccounts count]) {
90 [pithosAccounts addObject:[PithosAccount pithosAccount]];
91 self.pithosAccounts = self.pithosAccounts;
96 pithosAccountsDictionary = [[NSMutableDictionary alloc] initWithCapacity:[pithosAccounts count]];
97 for (PithosAccount *pithosAccount in pithosAccounts) {
98 [pithosAccountsDictionary setObject:pithosAccount forKey:pithosAccount.name];
99 if (!currentPithosAccount && pithosAccount.active)
100 currentPithosAccount = [pithosAccount retain];
102 if (!currentPithosAccount)
103 self.currentPithosAccount = [pithosAccounts objectAtIndex:0];
105 if (currentPithosAccount.active) {
106 [self savePithosAccounts:self];
107 [self showPithosBrowser:self];
108 self.pithosBrowserController.pithos = currentPithosAccount.pithos;
110 // XXX maybe call specifically to go to new account tab
111 [self showPithosPreferences:self];
114 syncTimer = [[NSTimer scheduledTimerWithTimeInterval:syncTimeInterval
116 selector:@selector(sync)
118 repeats:YES] retain];
122 // Based on: http://cocoatutorial.grapewave.com/2010/01/creating-a-status-bar-application/
123 // and: http://www.cocoadev.com/index.pl?ThumbnailImages
124 - (void)awakeFromNib {
125 NSImage *sourceImage = [NSImage imageNamed:@"pithos-large.png"];
127 NSImage *smallImage = [[[NSImage alloc] initWithSize:NSMakeSize(18, 18)] autorelease];
128 [smallImage lockFocus];
129 [[NSGraphicsContext currentContext] setImageInterpolation:NSImageInterpolationHigh];
130 [sourceImage setSize:NSMakeSize(18, 18)];
131 [sourceImage compositeToPoint:NSZeroPoint operation:NSCompositeCopy];
132 [smallImage unlockFocus];
134 statusItem = [[[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength] retain];
135 [statusItem setMenu:statusMenu];
136 [statusItem setImage:sourceImage];
137 [statusItem setHighlightMode:YES];
142 - (void)handleAppleEvent:(NSAppleEventDescriptor *)event withReplyEvent: (NSAppleEventDescriptor *)replyEvent {
143 NSURL *url = [NSURL URLWithString:[[event paramDescriptorForKeyword:keyDirectObject] stringValue]];
144 NSString *host = [url host];
145 NSString *query = [url query];
146 PithosAccount *pithosAccount = [pithosAccountsDictionary objectForKey:[url lastPathComponent]];
147 NSProcessInfo *processInfo = [NSProcessInfo processInfo];
148 if ([host isEqualToString:[NSString stringWithFormat:@"%d", [processInfo processIdentifier]]] && pithosAccount && query) {
151 NSRange userRange = [query rangeOfString:@"user=" options:NSCaseInsensitiveSearch];
152 if (userRange.length == 0)
153 // XXX maybe show an error message?
155 NSUInteger authUserStartLocation = userRange.location + userRange.length;
156 NSRange userEndRange = [query rangeOfString:@"&" options:NSCaseInsensitiveSearch
157 range:NSMakeRange(authUserStartLocation, [query length] - authUserStartLocation)];
158 if (userEndRange.length) {
159 authUser = [[query substringWithRange:NSMakeRange(authUserStartLocation, userEndRange.location - authUserStartLocation)]
160 stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
162 authUser = [[query substringFromIndex:authUserStartLocation]
163 stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
167 NSRange tokenRange = [query rangeOfString:@"token=" options:NSCaseInsensitiveSearch];
168 if (tokenRange.length == 0)
169 // XXX maybe show an error message?
171 NSUInteger authTokenStartLocation = tokenRange.location + tokenRange.length;
172 NSRange tokenEndRange = [query rangeOfString:@"&" options:NSCaseInsensitiveSearch
173 range:NSMakeRange(authTokenStartLocation, [query length] - authTokenStartLocation)];
174 if (tokenEndRange.length) {
175 authToken = [[query substringWithRange:NSMakeRange(authTokenStartLocation, tokenEndRange.location - authTokenStartLocation)]
176 stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
178 authToken = [[query substringFromIndex:authTokenStartLocation]
179 stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
182 NSLog(@"query authUser: '%@', authToken: '%@'", authUser, authToken);
183 if ([authUser length] && [authToken length]) {
184 [pithosAccount authenticateWithServerURL:nil authUser:authUser authToken:authToken];
185 [self savePithosAccounts:self];
186 if (self.pithosPreferencesController && [self.pithosPreferencesController.selectedPithosAccount isEqualTo:pithosAccount]) {
187 self.pithosPreferencesController.authUser = pithosAccount.authUser;
188 self.pithosPreferencesController.authToken = pithosAccount.authToken;
190 self.activated = YES;
191 if ([pithosAccount isEqualTo:currentPithosAccount]) {
192 [self showPithosBrowser:self];
193 self.pithosBrowserController.pithos = pithosAccount.pithos;
196 // XXX else maybe show an error message?
198 // XXX else maybe show an error message?
201 - (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender {
202 [self savePithosAccounts:self];
203 if ([self.pithosBrowserController operationsPending]) {
204 NSAlert *alert = [[[NSAlert alloc] init] autorelease];
205 [alert setMessageText:@"Pending Operations"];
206 [alert setInformativeText:@"There are pending operations in the browser, do you want to quit and cancel them?"];
207 [alert addButtonWithTitle:@"OK"];
208 [alert addButtonWithTitle:@"Cancel"];
209 NSInteger choice = [alert runModal];
210 if (choice == NSAlertSecondButtonReturn)
211 return NSTerminateCancel;
214 LSSharedFileListRemoveObserver(loginItems, CFRunLoopGetMain(), (CFStringRef)NSDefaultRunLoopMode, LSSharedFileListChanged, self);
215 CFRelease(loginItems);
217 return NSTerminateNow;
221 #pragma mark Callbacks
223 - (void)loginItemsChanged {
224 NSURL *appURL = [[NSBundle mainBundle] bundleURL];
225 LSSharedFileListItemRef appItem = NULL;
226 NSArray *snapshot = [NSMakeCollectable(LSSharedFileListCopySnapshot(loginItems, NULL)) autorelease];
227 for (id itemObject in snapshot) {
228 LSSharedFileListItemRef item = (LSSharedFileListItemRef)itemObject;
229 UInt32 resolutionFlags = kLSSharedFileListNoUserInteraction | kLSSharedFileListDoNotMountVolumes;
230 CFURLRef currentItemURL = NULL;
231 LSSharedFileListItemResolve(item, resolutionFlags, ¤tItemURL, NULL);
232 if (currentItemURL && CFEqual(currentItemURL, appURL)) {
233 CFRelease(currentItemURL);
238 CFRelease(currentItemURL);
241 if (appItem && (!openAtLogin || !openAtLoginEnabled))
242 self.openAtLogin = YES;
243 else if (!appItem && (openAtLogin || !openAtLoginEnabled))
244 self.openAtLogin = NO;
247 void LSSharedFileListChanged(LSSharedFileListRef inList, void *context) {
248 pithos_macosAppDelegate *self = (id)context;
249 [self loginItemsChanged];
253 #pragma mark Properties
255 - (PithosBrowserController *)pithosBrowserController {
256 if (!pithosBrowserController) {
257 pithosBrowserController = [[PithosBrowserController alloc] init];
259 return pithosBrowserController;
262 - (PithosPreferencesController *)pithosPreferencesController {
263 if (!pithosPreferencesController) {
264 pithosPreferencesController = [[PithosPreferencesController alloc] init];
266 return pithosPreferencesController;
269 - (void)setOpenAtLogin:(BOOL)anOpenAtLogin {
270 if (!openAtLoginEnabled) {
271 openAtLogin = anOpenAtLogin;
272 } else if (anOpenAtLogin != openAtLogin) {
273 NSURL *appURL = [NSURL fileURLWithPath:[[NSBundle mainBundle] bundlePath]];
274 LSSharedFileListItemRef appItem = NULL;
275 NSArray *snapshot = [NSMakeCollectable(LSSharedFileListCopySnapshot(loginItems, NULL)) autorelease];
276 for (id itemObject in snapshot) {
277 LSSharedFileListItemRef item = (LSSharedFileListItemRef)itemObject;
278 UInt32 resolutionFlags = kLSSharedFileListNoUserInteraction | kLSSharedFileListDoNotMountVolumes;
279 CFURLRef currentItemURL = NULL;
280 LSSharedFileListItemResolve(item, resolutionFlags, ¤tItemURL, NULL);
281 if (currentItemURL && CFEqual(currentItemURL, appURL)) {
282 CFRelease(currentItemURL);
287 CFRelease(currentItemURL);
292 LSSharedFileListInsertItemURL(loginItems, kLSSharedFileListItemBeforeFirst, NULL, NULL, (CFURLRef)appURL, NULL, NULL);
296 LSSharedFileListItemRemove(loginItems, appItem);
303 #pragma mark NSMenuDelegate
305 - (void)menuNeedsUpdate:(NSMenu *)menu {
306 NSMenuItem *menuItem;
307 [menu removeAllItems];
308 if ([menu isEqualTo:accountsMenu]) {
309 [menu setAutoenablesItems:NO];
310 for (PithosAccount *pithosAccount in pithosAccounts) {
311 menuItem = [[[NSMenuItem alloc] initWithTitle:pithosAccount.name
312 action:@selector(menuChangePithosAccount:)
313 keyEquivalent:@""] autorelease];
314 [menuItem setRepresentedObject:pithosAccount];
315 [menuItem setEnabled:pithosAccount.active];
316 [menuItem setState:((pithosAccount.active && [currentPithosAccount isEqualTo:pithosAccount]) ? NSOnState : NSOffState)];
317 [menu addItem:menuItem];
319 } else if ([menu isEqualTo:lastSyncMenu]) {
320 NSString *menuItemTitle;
321 [menu setAutoenablesItems:NO];
322 for (PithosAccount *pithosAccount in pithosAccounts) {
323 menuItemTitle = [NSString stringWithFormat:@"%@: %@",
325 [[[[LastCompletedSyncTransformer alloc] init] autorelease] transformedValue:pithosAccount.syncLastCompleted]];
326 if ([pithosAccount isEqualTo:syncPithosAccount] && [pithosAccount.syncDaemon isSyncing])
327 menuItemTitle = [menuItemTitle stringByAppendingString:@" (syncing)"];
328 // menuItem = [[[NSMenuItem alloc] initWithTitle:menuItemTitle
329 // action:@selector(menuChangeSyncActive:)
330 // keyEquivalent:@""] autorelease];
331 // [menuItem setRepresentedObject:pithosAccount];
332 // [menuItem setEnabled:pithosAccount.active];
333 // [menuItem setState:((pithosAccount.active && pithosAccount.syncActive) ? NSOnState : NSOffState)];
334 menuItem = [[[NSMenuItem alloc] initWithTitle:menuItemTitle action:nil keyEquivalent:@""] autorelease];
335 [menuItem setEnabled:NO];
336 [menuItem setState:NO];
337 [menu addItem:menuItem];
339 [menu addItem:[NSMenuItem separatorItem]];
340 [menu addItem:[[[NSMenuItem alloc] initWithTitle:@"Next Sync"
341 action:@selector(sync)
342 keyEquivalent:@""] autorelease]];
349 - (IBAction)showPithosBrowser:(id)sender {
352 [self.pithosBrowserController showWindow:sender];
353 [[self.pithosBrowserController window] makeKeyAndOrderFront:sender];
354 [NSApp activateIgnoringOtherApps:YES];
357 - (IBAction)showPithosPreferences:(id)sender {
358 [self.pithosPreferencesController showWindow:sender];
359 [[self.pithosPreferencesController window] makeKeyAndOrderFront:sender];
360 [NSApp activateIgnoringOtherApps:YES];
363 - (IBAction)showPithosAbout:(id)sender {
364 [NSApp orderFrontStandardAboutPanel:sender];
365 [NSApp activateIgnoringOtherApps:YES];
369 if (!activated || ![pithosAccounts count])
371 NSUInteger syncIndex;
372 BOOL syncPithosAccountFound = [pithosAccounts containsObject:syncPithosAccount];
373 if (syncPithosAccountFound)
374 syncIndex = [pithosAccounts indexOfObject:syncPithosAccount];
376 PithosAccount *singleSyncPithosAccount = nil;
377 for (PithosAccount *pithosAccount in pithosAccounts) {
378 if (!singleSyncPithosAccount && pithosAccount.active && pithosAccount.syncActive && pithosAccount.syncDaemon) {
379 singleSyncPithosAccount = pithosAccount;
380 } else if (singleSyncPithosAccount && pithosAccount.active && pithosAccount.syncActive && pithosAccount.syncDaemon) {
381 singleSyncPithosAccount = nil;
386 if (syncPithosAccount && syncPithosAccount.active && syncPithosAccount.syncActive && syncPithosAccount.syncDaemon) {
387 // An active syncDaemon was previously syncing
388 if (singleSyncPithosAccount && [singleSyncPithosAccount isEqualTo:syncPithosAccount]) {
389 // It's the only one, sync again
390 [syncPithosAccount.syncDaemon startDaemon];
391 [syncPithosAccount.syncDaemon sync];
393 } else if ([syncPithosAccount.syncDaemon isSyncing]) {
394 // It's still syncing, mark it as late and return
395 [syncPithosAccount.syncDaemon syncLate];
399 PithosAccount *newSyncPithosAccount = nil;
400 if (syncPithosAccountFound) {
401 for (PithosAccount *pithosAccount in [pithosAccounts objectsAtIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(syncIndex + 1, [pithosAccounts count] - syncIndex - 1)]]) {
402 if (pithosAccount.active && pithosAccount.syncActive && pithosAccount.syncDaemon) {
403 newSyncPithosAccount = pithosAccount;
407 if (!newSyncPithosAccount) {
408 for (PithosAccount *pithosAccount in [pithosAccounts objectsAtIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, syncIndex)]]) {
409 if (pithosAccount.active && pithosAccount.syncActive && pithosAccount.syncDaemon) {
410 newSyncPithosAccount = pithosAccount;
416 for (PithosAccount *pithosAccount in pithosAccounts) {
417 if (pithosAccount.active && pithosAccount.syncActive && pithosAccount.syncDaemon) {
418 newSyncPithosAccount = pithosAccount;
423 if (newSyncPithosAccount) {
424 // A different syncDaemon is found, sync it
425 self.syncPithosAccount = newSyncPithosAccount;
426 [syncPithosAccount.syncDaemon startDaemon];
427 [syncPithosAccount.syncDaemon sync];
428 } else if (syncPithosAccountFound && syncPithosAccount && syncPithosAccount.active && syncPithosAccount.syncActive && syncPithosAccount.syncDaemon) {
429 [syncPithosAccount.syncDaemon startDaemon];
430 [syncPithosAccount.syncDaemon sync];
432 self.syncPithosAccount = nil;
436 - (void)savePithosAccounts:(id)sender {
437 [userDefaults setObject:[NSKeyedArchiver archivedDataWithRootObject:pithosAccounts] forKey:@"pithosAccounts"];
438 [userDefaults synchronize];
441 - (void)removedPithosAccount:(PithosAccount *)removedPithosAccount {
442 if ([self.currentPithosAccount isEqualTo:removedPithosAccount]) {
443 for (PithosAccount *pithosAccount in pithosAccounts) {
444 if (pithosAccount.active) {
445 self.currentPithosAccount = pithosAccount;
446 self.pithosBrowserController.pithos = currentPithosAccount.pithos;
450 if ([self.currentPithosAccount isEqualTo:removedPithosAccount]) {
452 [self.pithosBrowserController.window close];
453 [self.pithosBrowserController resetBrowser];
454 self.currentPithosAccount = [pithosAccounts objectAtIndex:0];
457 if ([self.syncPithosAccount isEqualTo:removedPithosAccount])
458 self.syncPithosAccount = nil;
462 #pragma mark Menu Actions
464 - (void)menuChangePithosAccount:(NSMenuItem *)sender {
465 PithosAccount *pithosAccount = (PithosAccount *)[sender representedObject];
466 if (!pithosAccount.active)
468 if (![currentPithosAccount isEqualTo:pithosAccount] && [pithosAccounts containsObject:pithosAccount]) {
469 if ([self.pithosBrowserController operationsPending]) {
470 NSAlert *alert = [[[NSAlert alloc] init] autorelease];
471 [alert setMessageText:@"Pending Operations"];
472 [alert setInformativeText:@"There are pending operations in the browser, do you want to change accounts and cancel them?"];
473 [alert addButtonWithTitle:@"OK"];
474 [alert addButtonWithTitle:@"Cancel"];
475 NSInteger choice = [alert runModal];
476 if (choice == NSAlertSecondButtonReturn)
479 self.currentPithosAccount = pithosAccount;
480 [self showPithosBrowser:self];
481 self.pithosBrowserController.pithos = currentPithosAccount.pithos;
485 //- (void)menuChangeSyncActive:(NSMenuItem *)sender {
486 // PithosAccount *pithosAccount = (PithosAccount *)[sender representedObject];
487 // if (!pithosAccount.active)
489 // pithosAccount.syncActive = !pithosAccount.syncActive;
490 // if (self.pithosPreferencesController && [self.pithosPreferencesController.selectedPithosAccount isEqualTo:pithosAccount])
491 // self.pithosPreferencesController.syncActive = pithosAccount.syncActive;
492 // [self savePithosAccounts:self];