Added `-[NSArray validateAsPropertyList]` and `-[NSDictionary validateAsPropertyList...
[adiumx.git] / Source / ESAccountNetworkConnectivityPlugin.m
blob817d7684393d40cdfcd96f4b894360a3a55b8f6a
1 /* 
2  * Adium is the legal property of its developers, whose names are listed in the copyright file included
3  * with this source distribution.
4  * 
5  * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6  * General Public License as published by the Free Software Foundation; either version 2 of the License,
7  * or (at your option) any later version.
8  * 
9  * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10  * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
11  * Public License for more details.
12  * 
13  * You should have received a copy of the GNU General Public License along with this program; if not,
14  * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
15  */
17 #import <Adium/AIAccountControllerProtocol.h>
18 #import <Adium/AIContactControllerProtocol.h>
19 #import "ESAccountNetworkConnectivityPlugin.h"
20 #import <AIUtilities/AIEventAdditions.h>
21 #import <AIUtilities/AIHostReachabilityMonitor.h>
22 #import <AIUtilities/AISleepNotification.h>
23 #import <Adium/AIAccount.h>
24 #import <Adium/AIListObject.h>
26 @interface ESAccountNetworkConnectivityPlugin (PRIVATE)
27 - (void)handleConnectivityForAccount:(AIAccount *)account reachable:(BOOL)reachable;
28 - (BOOL)_accountsAreOnlineOrDisconnecting:(BOOL)considerConnecting;
29 @end
31 /*!
32  * @class ESAccountNetworkConnectivityPlugin
33  * @brief Handle account connection and disconnection
34  *
35  * Accounts are automatically connected and disconnected based on:
36  *      - If the account is enabled (at Adium launch if the network is available)
37  *  - Network connectivity (disconnect when the Internet is not available and connect when it is available again)
38  *  - System sleep (disconnect when the system sleeps and connect when it wakes up)
39  *
40  * Uses AIHostReachabilityMonitor and AISleepNotification from AIUtilities.
41  */
42 @implementation ESAccountNetworkConnectivityPlugin
44 /*!
45  * @brief Install plugin
46  */
47 - (void)installPlugin
49         //Wait for Adium to finish launching to handle autoconnecting enabled accounts
50         [[adium notificationCenter] addObserver:self
51                                                                    selector:@selector(adiumFinishedLaunching:)
52                                                                            name:AIApplicationDidFinishLoadingNotification
53                                                                          object:nil];
55         NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
57         //Monitor system sleep so we can cleanly disconnect / reconnect
58     [notificationCenter addObserver:self
59                                                    selector:@selector(systemWillSleep:)
60                                                            name:AISystemWillSleep_Notification
61                                                          object:nil];
62     [notificationCenter addObserver:self
63                                                    selector:@selector(systemDidWake:)
64                                                            name:AISystemDidWake_Notification
65                                                          object:nil];
66         [[adium contactController] registerListObjectObserver:self];
69 /*!
70  * @brief Uninstall plugin
71  */
72 - (void)uninstallPlugin
74         [[adium           notificationCenter] removeObserver:self];
75         [[NSNotificationCenter defaultCenter] removeObserver:self];
76         [[adium contactController] unregisterListObjectObserver:self];
79 /*!
80  * @brief Deallocate
81  */
82 - (void)dealloc
84         [accountsToConnect    release];
85         [accountsToNotConnect release];
87         [super dealloc];
90 /*!
91  * @brief Adium finished launching
92  *
93  * Attempt to autoconnect accounts if shift is not being pressed
94  */
95 - (void)adiumFinishedLaunching:(NSNotification *)notification
97         NSArray                                         *accounts = [[adium accountController] accounts];
98         AIHostReachabilityMonitor       *monitor = [AIHostReachabilityMonitor defaultMonitor];
99         BOOL                                            shiftHeld = [NSEvent shiftKey];
100         
101         //Start off forbidding all accounts from auto-connecting.
102         accountsToConnect    = [[NSMutableSet alloc] initWithArray:accounts];
103         accountsToNotConnect = [accountsToConnect mutableCopy];
104         knownHosts                       = [[NSMutableSet alloc] init];
105         
106         /* Add ourselves to the default host-reachability monitor as an observer for each account's host.
107          * At the same time, weed accounts that are to be auto-connected out of the accountsToNotConnect set.
108          */
109         NSEnumerator    *enumerator;
110         AIAccount               *account;
111         
112         enumerator = [accounts objectEnumerator];
113         while ((account = [enumerator nextObject])) {
114                 BOOL    connectAccount = (!shiftHeld  &&
115                                                                   [account enabled] &&
116                                                                   [[account preferenceForKey:KEY_AUTOCONNECT
117                                                                                                           group:GROUP_ACCOUNT_STATUS] boolValue]);
119                 if ([account enabled] &&
120                         [account connectivityBasedOnNetworkReachability]) {
121                         NSString *host = [account host];
122                         if (host && ![knownHosts containsObject:host]) {
123                                 [monitor addObserver:self forHost:host];
124                                 [knownHosts addObject:host];
125                         }
126                         
127                         //If this is an account we should auto-connect, remove it from accountsToNotConnect so that we auto-connect it.
128                         if (connectAccount) {
129                                 [accountsToNotConnect removeObject:account];
130                                 [account setStatusObject:[NSNumber numberWithBool:YES] forKey:@"Waiting for Network" notify:NotifyNow];
131                                 continue; //prevent the account from being removed from accountsToConnect.
132                         }
133                         
134                 }  else if (connectAccount) {
135                         /* This account does not connect based on network reachability, but should autoconnect.
136                          * Connect it immediately.
137                          */
138                         [account setShouldBeOnline:YES];
139                 }
140                 
141                 [accountsToConnect removeObject:account];
142         }
143         
144         [knownHosts release];
145         
146         //Watch for future changes to our account list
147         [[adium notificationCenter] addObserver:self
148                                                                    selector:@selector(accountListChanged:)
149                                                                            name:Account_ListChanged
150                                                                          object:nil];
153 - (void)networkDidChange
155         [[adium notificationCenter] postNotificationName:AINetworkDidChangeNotification
156                                                                                           object:nil];
160  * @brief Network connectivity changed
162  * Connect or disconnect accounts as appropriate to the new network state.
164  * @param networkIsReachable Indicates whether the given host is now reachable.
165  * @param host The host that is now reachable (or not).
166  */
167 - (void)hostReachabilityChanged:(BOOL)networkIsReachable forHost:(NSString *)host
169         NSEnumerator    *enumerator;
170         AIAccount               *account;
171         
172         //Connect or disconnect accounts in response to the connectivity change
173         enumerator = [[[adium accountController] accounts] objectEnumerator];
174         while ((account = [enumerator nextObject])) {
175                 if (networkIsReachable && [accountsToNotConnect containsObject:account]) {
176                         [accountsToNotConnect removeObject:account];
177                 } else {
178                         if ([[account host] isEqualToString:host]) {
179                                 [self handleConnectivityForAccount:account reachable:networkIsReachable];
180                         }
181                 }
182         }
183         
184         //Collate reachability changes for multiple hosts into a single notification
185         [NSObject cancelPreviousPerformRequestsWithTarget:self
186                                                                                          selector:@selector(networkDidChange)
187                                                                                            object:nil];
188         [self performSelector:@selector(networkDidChange) withObject:nil afterDelay:1.0];
191 #pragma mark AIHostReachabilityObserver compliance
193 - (void)hostReachabilityMonitor:(AIHostReachabilityMonitor *)monitor hostIsReachable:(NSString *)host {
194         [self hostReachabilityChanged:YES forHost:host];
196 - (void)hostReachabilityMonitor:(AIHostReachabilityMonitor *)monitor hostIsNotReachable:(NSString *)host {
197         [self hostReachabilityChanged:NO forHost:host];
200 #pragma mark Connecting/Disconnecting Accounts
202  * @brief Connect or disconnect an account as appropriate to a new network reachable state
204  * This method uses the accountsToConnect collection to track which accounts were disconnected and should therefore be
205  * later reconnected.
207  * @param account The account to change if appropriate
208  * @param reachable The new network reachable state
209  */
210 - (void)handleConnectivityForAccount:(AIAccount *)account reachable:(BOOL)reachable
212         AILog(@"handleConnectivityForAccount: %@ reachable: %i",account,reachable);
214         if (reachable) {
215                 //If we are now online and are waiting to connect this account, do it if the account hasn't already
216                 //been taken care of.
217                 [account setStatusObject:nil forKey:@"Waiting for Network" notify:NotifyNow];
218                 if ([accountsToConnect containsObject:account]) {
219                         if (![account online] &&
220                                 ![account integerStatusObjectForKey:@"Connecting"]) {
221                                 [account setShouldBeOnline:YES];
222                                 [accountsToConnect removeObject:account];
223                         }
224                 }
225         } else {
226                 //If we are no longer online and this account is connected, disconnect it.
227                 [account setStatusObject:[NSNumber numberWithBool:YES] forKey:@"Waiting for Network" notify:NotifyNow];
228                 if (([account online] ||
229                          [account integerStatusObjectForKey:@"Connecting"]) &&
230                         ![account integerStatusObjectForKey:@"Disconnecting"]) {
231                         [account disconnectFromDroppedNetworkConnection];
232                         [accountsToConnect addObject:account];
233                 }
234         }
237 //Disconnect / Reconnect on sleep --------------------------------------------------------------------------------------
238 #pragma mark Disconnect/Reconnect On Sleep
240  * @brief System is sleeping
241  */
242 - (void)systemWillSleep:(NSNotification *)notification
244         AILog(@"***** System sleeping...");
245         //Disconnect all online or connecting accounts
246         if ([self _accountsAreOnlineOrDisconnecting:YES]) {
247                 NSEnumerator    *enumerator = [[[adium accountController] accounts] objectEnumerator];
248                 AIAccount               *account;
249                 
250                 while ((account = [enumerator nextObject])) {
251                         if ([account online] ||
252                                 [[account statusObjectForKey:@"Connecting"] boolValue] ||
253                                 [account statusObjectForKey:@"Waiting to Reconnect"]) {
255                                 // Disconnect the account if it's online
256                                 if ([account online]) {
257                                         [account disconnect];
258                                 // Cancel any reconnect attempts
259                                 } else if ([account statusObjectForKey:@"Waiting to Reconnect"]) {
260                                         [account cancelAutoReconnect];
261                                 }
262                                 // Add it to our list to reconnect
263                                 [accountsToConnect addObject:account];
264                         }
265                 }
266         }
267                 
268         //While some accounts disconnect immediately, others may need a second or two to finish the process.  For
269         //these accounts we'll want to hold system sleep until they are ready.  We monitor account status changes
270         //and will lift the hold once all accounts are finished.
271         //Don't delay sleep for connecting or reconnecting accounts
272         if ([self _accountsAreOnlineOrDisconnecting:NO]) {
273                 AILog(@"Posting AISystemHoldSleep_Notification...");
274                 [[NSNotificationCenter defaultCenter] postNotificationName:AISystemHoldSleep_Notification object:nil];
275                 waitingToSleep = YES;
276         }
280  * @brief Invoked when our accounts change status
282  * Once all accounts are offline we will remove our hold on system sleep
283  */
284 - (NSSet *)updateListObject:(AIListObject *)inObject keys:(NSSet *)inModifiedKeys silent:(BOOL)silent
286         if ([inObject isKindOfClass:[AIAccount class]]) {
287                 if (waitingToSleep &&
288                         [inModifiedKeys containsObject:@"Online"]) {
289                         //Don't delay sleep for connecting or reconnecting accounts
290                         if (![self _accountsAreOnlineOrDisconnecting:NO]) {
291                                 AILog(@"Posting AISystemContinueSleep_Notification...");
292                                 [[NSNotificationCenter defaultCenter] postNotificationName:AISystemContinueSleep_Notification object:nil];
293                                 waitingToSleep = NO;
295                         } else {
296                                 AILog(@"Continuing to wait to sleep...");
297                         }
298                 }
299                 if ([inModifiedKeys containsObject:@"Enabled"]) {
300                         AIAccount *account = (AIAccount *)inObject;
302                         if ([account enabled]) {
303                                 //Start observing for this host if we're not already
304                                 if ([account connectivityBasedOnNetworkReachability]) {
305                                         NSString *host = [account host];
306                                         AIHostReachabilityMonitor *monitor = [AIHostReachabilityMonitor defaultMonitor];
307         
308                                         [account setStatusObject:[NSNumber numberWithBool:YES] forKey:@"Waiting for Network" notify:NotifyNow];
309                                         if (host &&
310                                                 ![monitor observer:self isObservingHost:host]) {
311                                                 [monitor addObserver:self forHost:host];
312                                         }
313                                 }
314                                 
315                         } else {
316                                 NSEnumerator    *enumerator;
317                                 AIAccount               *anAccount;
318                                 BOOL                    enabledAccountUsingThisHost = NO;
319                                 NSString                *thisHost = [account host];
320                                 
321                                 [account setStatusObject:nil forKey:@"Waiting for Network" notify:NotifyNow];
323                                 //Check if any enabled accounts are still using this now-disabled account's host
324                                 enumerator = [[[adium accountController] accounts] objectEnumerator];   
325                                 while ((anAccount = [enumerator nextObject])) {
326                                         if ([anAccount enabled] &&
327                                                 [anAccount connectivityBasedOnNetworkReachability]) {
328                                                 if ([thisHost caseInsensitiveCompare:[anAccount host]] == NSOrderedSame) {
329                                                         enabledAccountUsingThisHost = YES;
330                                                         break;
331                                                 }
332                                         }
333                                 }
335                                 //If not, stop observing it entirely
336                                 if (!enabledAccountUsingThisHost) {
337                                         AIHostReachabilityMonitor *monitor = [AIHostReachabilityMonitor defaultMonitor];
338                                         [monitor removeObserver:self forHost:thisHost];
339                                 }
340                         }
341                 }
342         }
344         return nil;
348  * @brief Returns YES if any accounts are currently in the process of disconnecting
350  * @param considerConnecting Consider accounts which are connecting or waiting to reconnect
351  */
352 - (BOOL)_accountsAreOnlineOrDisconnecting:(BOOL)considerConnecting
354     NSEnumerator        *enumerator = [[[adium accountController] accounts] objectEnumerator];
355         AIAccount               *account;
356     
357         while ((account = [enumerator nextObject])) {
358                 if ([account online] ||
359                    [[account statusObjectForKey:@"Disconnecting"] boolValue]) {
360                         AILog(@"%@ (and possibly others) is still %@",account, ([account online] ? @"online" : @"disconnecting"));
361                         return YES;
362                 } else if (considerConnecting &&
363                                    ([[account statusObjectForKey:@"Connecting"] boolValue] ||
364                                         [account statusObjectForKey:@"Waiting to Reconnect"])) {
365                         return YES;
366                 }
367         }
368         
369         return NO;
373  * @brief System is waking from sleep
374  */
375 - (void)systemDidWake:(NSNotification *)notification
377         NSEnumerator    *enumerator;
378         AIAccount               *account;
380         AILog(@"***** System did wake...");
382         /* We could have been waiting to sleep but then timed out and slept anyways; clear the flag if that happened and it wasn't cleared
383          * in updateListObject::: above.
384          */
385         waitingToSleep = NO;
387         //Immediately re-connect accounts which are ignoring the server reachability
388         enumerator = [[[adium accountController] accounts] objectEnumerator];   
389         while ((account = [enumerator nextObject])) {
390                 if (![account connectivityBasedOnNetworkReachability] && [accountsToConnect containsObject:account]) {
391                         [account setShouldBeOnline:YES];
392                         [accountsToConnect removeObject:account];
393                 } else if ([accountsToConnect containsObject:account]) {
394                         [account setStatusObject:[NSNumber numberWithBool:YES] forKey:@"Waiting for Network" notify:NotifyNow];
395                 }
396         }
399 #pragma mark Changes to the account list
401  * @brief When the account list changes, ensure we're monitoring for each account
402  */
403 - (void)accountListChanged:(NSNotification *)notification
405         NSEnumerator    *enumerator;
406         AIAccount               *account;
407         AIHostReachabilityMonitor       *monitor = [AIHostReachabilityMonitor defaultMonitor];
409         //Immediately re-connect accounts which are ignoring the server reachability
410         enumerator = [[[adium accountController] accounts] objectEnumerator];   
411         while ((account = [enumerator nextObject])) {
412                 if ([account enabled] &&
413                         [account connectivityBasedOnNetworkReachability]) {
414                         NSString *host = [account host];
415                         
416                         if (host &&
417                                 ![monitor observer:self isObservingHost:host]) {
418                                 [monitor addObserver:self forHost:host];
419                         }
420                 }
421         }
424 @end