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, 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 userDefaults = [[NSUserDefaults standardUserDefaults] retain];
60 syncTimeInterval = [userDefaults doubleForKey:@"syncTimeInterval"];
61 if (syncTimeInterval <= 0.0) {
62 syncTimeInterval = 180.0;
63 [userDefaults setDouble:syncTimeInterval forKey:@"syncTimeInterval"];
64 [userDefaults synchronize];
67 activityFacilityTimeInterval = [userDefaults doubleForKey:@"activityFacilityTimeInterval"];
68 if (activityFacilityTimeInterval <= 0.0) {
69 activityFacilityTimeInterval = 0.05;
70 [userDefaults setDouble:activityFacilityTimeInterval forKey:@"activityFacilityTimeInterval"];
71 [userDefaults synchronize];
74 NSData *tmpData = [userDefaults objectForKey:@"pithosAccounts"];
76 if (tmpData && (tmpArray = [NSKeyedUnarchiver unarchiveObjectWithData:tmpData]))
77 self.pithosAccounts = [NSMutableArray arrayWithArray:tmpArray];
79 self.pithosAccounts = [NSMutableArray array];
81 if (![pithosAccounts count]) {
82 [pithosAccounts addObject:[PithosAccount pithosAccount]];
83 self.pithosAccounts = self.pithosAccounts;
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];
94 if (!currentPithosAccount)
95 self.currentPithosAccount = [pithosAccounts objectAtIndex:0];
97 if (currentPithosAccount.active) {
98 [self savePithosAccounts:self];
99 [self showPithosBrowser:self];
100 self.pithosBrowserController.pithos = currentPithosAccount.pithos;
102 // XXX maybe call specifically to go to new account tab
103 [self showPithosPreferences:self];
106 syncTimer = [[NSTimer scheduledTimerWithTimeInterval:syncTimeInterval
108 selector:@selector(sync)
110 repeats:YES] retain];
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"];
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];
126 statusItem = [[[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength] retain];
127 [statusItem setMenu:statusMenu];
128 [statusItem setImage:sourceImage];
129 [statusItem setHighlightMode:YES];
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) {
144 NSRange userRange = [query rangeOfString:@"user=" options:NSCaseInsensitiveSearch];
145 if (userRange.length == 0)
146 // XXX maybe show an error message?
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];
155 authUser = [[query substringFromIndex:authUserStartLocation]
156 stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
160 NSRange tokenRange = [query rangeOfString:@"token=" options:NSCaseInsensitiveSearch];
161 if (tokenRange.length == 0)
162 // XXX maybe show an error message?
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];
171 authToken = [[query substringFromIndex:authTokenStartLocation]
172 stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
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;
183 self.activated = YES;
184 if ([pithosAccount isEqualTo:currentPithosAccount]) {
185 [self showPithosBrowser:self];
186 self.pithosBrowserController.pithos = pithosAccount.pithos;
189 // XXX else maybe show an error message?
191 // XXX else maybe show an error message?
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;
206 return NSTerminateNow;
210 #pragma mark Properties
212 - (PithosBrowserController *)pithosBrowserController {
213 if (!pithosBrowserController) {
214 pithosBrowserController = [[PithosBrowserController alloc] init];
216 return pithosBrowserController;
219 - (PithosPreferencesController *)pithosPreferencesController {
220 if (!pithosPreferencesController) {
221 pithosPreferencesController = [[PithosPreferencesController alloc] init];
223 return pithosPreferencesController;
227 #pragma mark NSMenuDelegate
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];
243 } else if ([menu isEqualTo:lastSyncMenu]) {
244 NSString *menuItemTitle;
245 [menu setAutoenablesItems:NO];
246 for (PithosAccount *pithosAccount in pithosAccounts) {
247 menuItemTitle = [NSString stringWithFormat:@"%@: %@",
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];
263 [menu addItem:[NSMenuItem separatorItem]];
264 [menu addItem:[[[NSMenuItem alloc] initWithTitle:@"Next Sync"
265 action:@selector(sync)
266 keyEquivalent:@""] autorelease]];
273 - (IBAction)showPithosBrowser:(id)sender {
276 [self.pithosBrowserController showWindow:sender];
277 [[self.pithosBrowserController window] makeKeyAndOrderFront:sender];
278 [NSApp activateIgnoringOtherApps:YES];
281 - (IBAction)showPithosPreferences:(id)sender {
282 [self.pithosPreferencesController showWindow:sender];
283 [[self.pithosPreferencesController window] makeKeyAndOrderFront:sender];
284 [NSApp activateIgnoringOtherApps:YES];
288 if (!activated || ![pithosAccounts count])
290 NSUInteger syncIndex;
291 BOOL syncPithosAccountFound = [pithosAccounts containsObject:syncPithosAccount];
292 if (syncPithosAccountFound)
293 syncIndex = [pithosAccounts indexOfObject:syncPithosAccount];
295 PithosAccount *singleSyncPithosAccount = nil;
296 for (PithosAccount *pithosAccount in pithosAccounts) {
297 if (!singleSyncPithosAccount && pithosAccount.active && pithosAccount.syncActive && pithosAccount.syncDaemon) {
298 singleSyncPithosAccount = pithosAccount;
299 } else if (singleSyncPithosAccount && pithosAccount.active && pithosAccount.syncActive && pithosAccount.syncDaemon) {
300 singleSyncPithosAccount = nil;
305 if (syncPithosAccount && syncPithosAccount.active && syncPithosAccount.syncActive && syncPithosAccount.syncDaemon) {
306 // An active syncDaemon was previously syncing
307 if (singleSyncPithosAccount && [singleSyncPithosAccount isEqualTo:syncPithosAccount]) {
308 // It's the only one, sync again
309 [syncPithosAccount.syncDaemon startDaemon];
310 [syncPithosAccount.syncDaemon sync];
312 } else if ([syncPithosAccount.syncDaemon isSyncing]) {
313 // It's still syncing, mark it as late and return
314 [syncPithosAccount.syncDaemon syncLate];
318 PithosAccount *newSyncPithosAccount = nil;
319 if (syncPithosAccountFound) {
320 for (PithosAccount *pithosAccount in [pithosAccounts objectsAtIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(syncIndex + 1, [pithosAccounts count] - syncIndex - 1)]]) {
321 if (pithosAccount.active && pithosAccount.syncActive && pithosAccount.syncDaemon) {
322 newSyncPithosAccount = pithosAccount;
326 if (!newSyncPithosAccount) {
327 for (PithosAccount *pithosAccount in [pithosAccounts objectsAtIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, syncIndex)]]) {
328 if (pithosAccount.active && pithosAccount.syncActive && pithosAccount.syncDaemon) {
329 newSyncPithosAccount = pithosAccount;
335 for (PithosAccount *pithosAccount in pithosAccounts) {
336 if (pithosAccount.active && pithosAccount.syncActive && pithosAccount.syncDaemon) {
337 newSyncPithosAccount = pithosAccount;
342 if (newSyncPithosAccount) {
343 // A different syncDaemon is found, sync it
344 self.syncPithosAccount = newSyncPithosAccount;
345 [syncPithosAccount.syncDaemon startDaemon];
346 [syncPithosAccount.syncDaemon sync];
347 } else if (syncPithosAccountFound && syncPithosAccount && syncPithosAccount.active && syncPithosAccount.syncActive && syncPithosAccount.syncDaemon) {
348 [syncPithosAccount.syncDaemon startDaemon];
349 [syncPithosAccount.syncDaemon sync];
351 self.syncPithosAccount = nil;
355 - (void)savePithosAccounts:(id)sender {
356 [userDefaults setObject:[NSKeyedArchiver archivedDataWithRootObject:pithosAccounts] forKey:@"pithosAccounts"];
357 [userDefaults synchronize];
360 - (void)removedPithosAccount:(PithosAccount *)removedPithosAccount {
361 if ([self.currentPithosAccount isEqualTo:removedPithosAccount]) {
362 for (PithosAccount *pithosAccount in pithosAccounts) {
363 if (pithosAccount.active) {
364 self.currentPithosAccount = pithosAccount;
365 self.pithosBrowserController.pithos = currentPithosAccount.pithos;
369 if ([self.currentPithosAccount isEqualTo:removedPithosAccount]) {
371 [self.pithosBrowserController.window close];
372 [self.pithosBrowserController resetBrowser];
373 self.currentPithosAccount = [pithosAccounts objectAtIndex:0];
376 if ([self.syncPithosAccount isEqualTo:removedPithosAccount])
377 self.syncPithosAccount = nil;
381 #pragma mark Menu Actions
383 - (void)menuChangePithosAccount:(NSMenuItem *)sender {
384 PithosAccount *pithosAccount = (PithosAccount *)[sender representedObject];
385 if (!pithosAccount.active)
387 if (![currentPithosAccount isEqualTo:pithosAccount] && [pithosAccounts containsObject:pithosAccount]) {
388 if ([self.pithosBrowserController operationsPending]) {
389 NSAlert *alert = [[[NSAlert alloc] init] autorelease];
390 [alert setMessageText:@"Pending Operations"];
391 [alert setInformativeText:@"There are pending operations in the browser, do you want to change accounts and cancel them?"];
392 [alert addButtonWithTitle:@"OK"];
393 [alert addButtonWithTitle:@"Cancel"];
394 NSInteger choice = [alert runModal];
395 if (choice == NSAlertSecondButtonReturn)
398 self.currentPithosAccount = pithosAccount;
399 [self showPithosBrowser:self];
400 self.pithosBrowserController.pithos = currentPithosAccount.pithos;
404 //- (void)menuChangeSyncActive:(NSMenuItem *)sender {
405 // PithosAccount *pithosAccount = (PithosAccount *)[sender representedObject];
406 // if (!pithosAccount.active)
408 // pithosAccount.syncActive = !pithosAccount.syncActive;
409 // if (self.pithosPreferencesController && [self.pithosPreferencesController.selectedPithosAccount isEqualTo:pithosAccount])
410 // self.pithosPreferencesController.syncActive = pithosAccount.syncActive;
411 // [self savePithosAccounts:self];