Modified loggers to use their enclosing class
[pithos-ms-client] / trunk / Pithos.Client.WPF / Shell / ShellViewModel.cs
1 #region
2 /* -----------------------------------------------------------------------
3  * <copyright file="ShellViewModel.cs" company="GRNet">
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  *
21  * THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
22  * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
23  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
24  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
25  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
26  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
27  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
28  * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
29  * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
30  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
31  * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32  * POSSIBILITY OF SUCH DAMAGE.
33  *
34  * The views and conclusions contained in the software and
35  * documentation are those of the authors and should not be
36  * interpreted as representing official policies, either expressed
37  * or implied, of GRNET S.A.
38  * </copyright>
39  * -----------------------------------------------------------------------
40  */
41 #endregion
42 using System.Collections.Concurrent;
43 using System.Diagnostics;
44 using System.Diagnostics.Contracts;
45 using System.IO;
46 using System.Net;
47 using System.Reflection;
48 using System.Runtime.InteropServices;
49 using System.ServiceModel;
50 using System.Threading.Tasks;
51 using System.Windows;
52 using System.Windows.Controls.Primitives;
53 using Caliburn.Micro;
54 using Hardcodet.Wpf.TaskbarNotification;
55 using Pithos.Client.WPF.Configuration;
56 using Pithos.Client.WPF.FileProperties;
57 using Pithos.Client.WPF.Preferences;
58 using Pithos.Client.WPF.SelectiveSynch;
59 using Pithos.Client.WPF.Services;
60 using Pithos.Client.WPF.Shell;
61 using Pithos.Core;
62 using Pithos.Core.Agents;
63 using Pithos.Interfaces;
64 using System;
65 using System.Collections.Generic;
66 using System.Linq;
67 using Pithos.Network;
68 using StatusService = Pithos.Client.WPF.Services.StatusService;
69
70 namespace Pithos.Client.WPF {
71         using System.ComponentModel.Composition;
72
73         
74         ///<summary>
75         /// The "shell" of the Pithos application displays the taskbar  icon, menu and notifications.
76         /// The shell also hosts the status service called by shell extensions to retrieve file info
77         ///</summary>
78         ///<remarks>
79         /// It is a strange "shell" as its main visible element is an icon instead of a window
80         /// The shell subscribes to the following events:
81         /// * Notification:  Raised by components that want to notify the user. Usually displayed in a balloon
82         /// * SelectiveSynchChanges: Notifies that the user made changes to the selective synch folders for an account. Raised by the Selective Synch dialog. Located here because the monitors are here
83         /// * ShowFilePropertiesEvent: Raised when a shell command requests the display of the file/container properties dialog
84         ///</remarks>           
85         //TODO: CODE SMELL Why does the shell handle the SelectiveSynchChanges?
86         [Export(typeof(IShell))]
87         public class ShellViewModel : Screen, IStatusNotification, IShell,
88                 IHandle<Notification>, IHandle<SelectiveSynchChanges>, IHandle<ShowFilePropertiesEvent>
89         {
90                 //The Status Checker provides the current synch state
91                 //TODO: Could we remove the status checker and use events in its place?
92                 private readonly IStatusChecker _statusChecker;
93                 private readonly IEventAggregator _events;
94
95                 public PithosSettings Settings { get; private set; }
96
97
98                 private readonly ConcurrentDictionary<string, PithosMonitor> _monitors = new ConcurrentDictionary<string, PithosMonitor>();
99                 ///<summary>
100                 /// Dictionary of account monitors, keyed by account
101                 ///</summary>
102                 ///<remarks>
103                 /// One monitor class is created for each account. The Shell needs access to the monitors to execute start/stop/pause commands,
104                 /// retrieve account and boject info            
105                 ///</remarks>
106                 // TODO: Does the Shell REALLY need access to the monitors? Could we achieve the same results with a better design?
107                 // TODO: The monitors should be internal to Pithos.Core, even though exposing them makes coding of the Object and Container windows easier
108                 public ConcurrentDictionary<string, PithosMonitor> Monitors
109                 {
110                         get { return _monitors; }
111                 }
112
113
114                 ///<summary>
115                 /// The status service is used by Shell extensions to retrieve file status information
116                 ///</summary>
117                 //TODO: CODE SMELL! This is the shell! While hosting in the shell makes executing start/stop commands easier, it is still a smell
118                 private ServiceHost _statusService;
119
120                 //Logging in the Pithos client is provided by log4net
121         private static readonly log4net.ILog Log = log4net.LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
122
123                 //Lazily initialized File Version info. This is done once and lazily to avoid blocking the UI
124                 private readonly Lazy<FileVersionInfo> _fileVersion;
125
126             private readonly PollAgent _pollAgent;
127
128                 ///<summary>
129                 /// The Shell depends on MEF to provide implementations for windowManager, events, the status checker service and the settings
130                 ///</summary>
131                 ///<remarks>
132                 /// The PithosSettings class encapsulates the app's settings to abstract their storage mechanism (App settings, a database or registry)
133                 ///</remarks>
134                 [ImportingConstructor]          
135                 public ShellViewModel(IWindowManager windowManager, IEventAggregator events, IStatusChecker statusChecker, PithosSettings settings,PollAgent pollAgent)
136                 {
137                         try
138                         {
139
140                                 _windowManager = windowManager;
141                                 //CHECK: Caliburn doesn't need explicit command construction
142                                 //OpenPithosFolderCommand = new PithosCommand(OpenPithosFolder);
143                                 _statusChecker = statusChecker;
144                                 //The event subst
145                                 _events = events;
146                                 _events.Subscribe(this);
147
148                             _pollAgent = pollAgent;
149                                 Settings = settings;
150
151                                 Proxy.SetFromSettings(settings);
152
153                                 StatusMessage = "In Synch";
154
155                                 _fileVersion=  new Lazy<FileVersionInfo>(() =>
156                                 {
157                                         Assembly assembly = Assembly.GetExecutingAssembly();
158                                         var fileVersion = FileVersionInfo.GetVersionInfo(assembly.Location);
159                                         return fileVersion;
160                                 });
161                                 _accounts.CollectionChanged += (sender, e) =>
162                                                                                                    {
163                                                                                                            NotifyOfPropertyChange(() => OpenFolderCaption);
164                                                                                                            NotifyOfPropertyChange(() => HasAccounts);
165                                                                                                    };
166
167                         }
168                         catch (Exception exc)
169                         {
170                                 Log.Error("Error while starting the ShellViewModel",exc);
171                                 throw;
172                         }
173                 }
174
175
176                 protected override void OnActivate()
177                 {
178                         base.OnActivate();
179
180                         
181
182                         StartMonitoring();                    
183                 }
184
185
186
187                 private async void StartMonitoring()
188                 {
189                         try
190                         {
191                                 var accounts = Settings.Accounts.Select(MonitorAccount);
192                                 await TaskEx.WhenAll(accounts);
193                                 _statusService = StatusService.Start();
194
195 /*
196                                 foreach (var account in Settings.Accounts)
197                                 {
198                                         await MonitorAccount(account);
199                                 }
200 */
201                                 
202                         }
203                         catch (AggregateException exc)
204                         {
205                                 exc.Handle(e =>
206                                 {
207                                         Log.Error("Error while starting monitoring", e);
208                                         return true;
209                                 });
210                                 throw;
211                         }
212                 }
213
214                 protected override void OnDeactivate(bool close)
215                 {
216                         base.OnDeactivate(close);
217                         if (close)
218                         {
219                                 StatusService.Stop(_statusService);
220                                 _statusService = null;
221                         }
222                 }
223
224                 public Task MonitorAccount(AccountSettings account)
225                 {
226                         return Task.Factory.StartNew(() =>
227                         {                                                
228                                 PithosMonitor monitor;
229                                 var accountName = account.AccountName;
230
231                                 if (_monitors.TryGetValue(accountName, out monitor))
232                                 {
233                                         //If the account is active
234                     if (account.IsActive)
235                     {
236                         //The Api Key may have changed throuth the Preferences dialog
237                         monitor.ApiKey = account.ApiKey;
238                                                 Debug.Assert(monitor.StatusNotification == this,"An existing monitor should already have a StatusNotification service object");
239                         monitor.RootPath = account.RootPath;
240                         //Start the monitor. It's OK to start an already started monitor,
241                         //it will just ignore the call                        
242                         StartMonitor(monitor).Wait();
243                     }
244                     else
245                     {
246                         //If the account is inactive
247                         //Stop and remove the monitor
248                         RemoveMonitor(accountName);
249                     }
250                                         return;
251                                 }
252
253                                 
254                                 //Create a new monitor/ Can't use MEF here, it would return a single instance for all monitors
255                                 monitor = new PithosMonitor
256                                                           {
257                                                                   UserName = accountName,
258                                                                   ApiKey = account.ApiKey,                                  
259                                                                   StatusNotification = this,
260                                                                   RootPath = account.RootPath
261                                                           };
262                                 //PithosMonitor uses MEF so we need to resolve it
263                                 IoC.BuildUp(monitor);
264
265                                 monitor.AuthenticationUrl = account.ServerUrl;
266
267                                 _monitors[accountName] = monitor;
268
269                                 if (account.IsActive)
270                                 {
271                                         //Don't start a monitor if it doesn't have an account and ApiKey
272                                         if (String.IsNullOrWhiteSpace(monitor.UserName) ||
273                                                 String.IsNullOrWhiteSpace(monitor.ApiKey))
274                                                 return;
275                                         StartMonitor(monitor);
276                                 }
277                         });
278                 }
279
280
281                 protected override void OnViewLoaded(object view)
282                 {
283                         UpdateStatus();
284                         var window = (Window)view;            
285                         TaskEx.Delay(1000).ContinueWith(t => Execute.OnUIThread(window.Hide));
286                         base.OnViewLoaded(view);
287                 }
288
289
290                 #region Status Properties
291
292                 private string _statusMessage;
293                 public string StatusMessage
294                 {
295                         get { return _statusMessage; }
296                         set
297                         {
298                                 _statusMessage = value;
299                                 NotifyOfPropertyChange(() => StatusMessage);
300                         }
301                 }
302
303                 private readonly ObservableConcurrentCollection<AccountInfo> _accounts = new ObservableConcurrentCollection<AccountInfo>();
304                 public ObservableConcurrentCollection<AccountInfo> Accounts
305                 {
306                         get { return _accounts; }
307                 }
308
309                 public bool HasAccounts
310                 {
311                         get { return _accounts.Count > 0; }
312                 }
313
314
315                 public string OpenFolderCaption
316                 {
317                         get
318                         {
319                                 return (_accounts.Count == 0)
320                                                 ? "No Accounts Defined"
321                                                 : "Open Pithos Folder";
322                         }
323                 }
324
325                 private string _pauseSyncCaption="Pause Synching";
326                 public string PauseSyncCaption
327                 {
328                         get { return _pauseSyncCaption; }
329                         set
330                         {
331                                 _pauseSyncCaption = value;
332                                 NotifyOfPropertyChange(() => PauseSyncCaption);
333                         }
334                 }
335
336                 private readonly ObservableConcurrentCollection<FileEntry> _recentFiles = new ObservableConcurrentCollection<FileEntry>();
337                 public ObservableConcurrentCollection<FileEntry> RecentFiles
338                 {
339                         get { return _recentFiles; }
340                 }
341
342
343                 private string _statusIcon="../Images/Pithos.ico";
344                 public string StatusIcon
345                 {
346                         get { return _statusIcon; }
347                         set
348                         {
349                                 //TODO: Ensure all status icons use the Pithos logo
350                                 _statusIcon = value;
351                                 NotifyOfPropertyChange(() => StatusIcon);
352                         }
353                 }
354
355                 #endregion
356
357                 #region Commands
358
359         public void ShowPreferences()
360         {
361             ShowPreferences(null);
362         }
363
364                 public void ShowPreferences(string currentTab)
365                 {
366                         //Settings.Reload();
367                     var preferences = new PreferencesViewModel(_windowManager, _events, this, Settings,currentTab);
368                     _windowManager.ShowDialog(preferences);
369                         
370                 }
371
372                 public void AboutPithos()
373                 {
374                         var about = new AboutViewModel();
375                         _windowManager.ShowWindow(about);
376                 }
377
378                 public void SendFeedback()
379                 {
380                         var feedBack =  IoC.Get<FeedbackViewModel>();
381                         _windowManager.ShowWindow(feedBack);
382                 }
383
384                 //public PithosCommand OpenPithosFolderCommand { get; private set; }
385
386                 public void OpenPithosFolder()
387                 {
388                         var account = Settings.Accounts.FirstOrDefault(acc => acc.IsActive);
389                         if (account == null)
390                                 return;
391                         Process.Start(account.RootPath);
392                 }
393
394                 public void OpenPithosFolder(AccountInfo account)
395                 {
396                         Process.Start(account.AccountPath);
397                 }
398
399                 
400 /*
401                 public void GoToSite()
402                 {            
403                         var site = Properties.Settings.Default.PithosSite;
404                         Process.Start(site);            
405                 }
406 */
407
408                 public void GoToSite(AccountInfo account)
409                 {
410                         Process.Start(account.SiteUri);
411                 }
412
413         /// <summary>
414         /// Open an explorer window to the target path's directory
415         /// and select the file
416         /// </summary>
417         /// <param name="entry"></param>
418         public void GoToFile(FileEntry entry)
419         {
420             var fullPath = entry.FullPath;
421             if (!File.Exists(fullPath) && !Directory.Exists(fullPath))
422                 return;
423             Process.Start("explorer.exe","/select, " + fullPath);
424         }
425
426         public void OpenLogPath()
427         {
428             var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
429             var pithosDataPath = Path.Combine(appDataPath, "GRNET");
430
431             Process.Start(pithosDataPath);
432         }
433         
434         public void ShowFileProperties()
435                 {
436                         var account = Settings.Accounts.First(acc => acc.IsActive);            
437                         var dir = new DirectoryInfo(account.RootPath + @"\pithos");
438                         var files=dir.GetFiles();
439                         var r=new Random();
440                         var idx=r.Next(0, files.Length);
441                         ShowFileProperties(files[idx].FullName);            
442                 }
443
444                 public void ShowFileProperties(string filePath)
445                 {
446                         if (String.IsNullOrWhiteSpace(filePath))
447                                 throw new ArgumentNullException("filePath");
448                         if (!File.Exists(filePath) && !Directory.Exists(filePath))
449                                 throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath");
450                         Contract.EndContractBlock();
451
452                         var pair=(from monitor in  Monitors
453                                                            where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)
454                                                                    select monitor).FirstOrDefault();
455                         var accountMonitor = pair.Value;
456
457                         if (accountMonitor == null)
458                                 return;
459
460                         var infoTask=Task.Factory.StartNew(()=>accountMonitor.GetObjectInfo(filePath));
461
462                         
463
464                         var fileProperties = new FilePropertiesViewModel(this, infoTask,filePath);
465                         _windowManager.ShowWindow(fileProperties);
466                 } 
467                 
468                 public void ShowContainerProperties()
469                 {
470                         var account = Settings.Accounts.First(acc => acc.IsActive);            
471                         var dir = new DirectoryInfo(account.RootPath);
472                         var fullName = (from folder in dir.EnumerateDirectories()
473                                                         where (folder.Attributes & FileAttributes.Hidden) == 0
474                                                         select folder.FullName).First();
475                         ShowContainerProperties(fullName);            
476                 }
477
478                 public void ShowContainerProperties(string filePath)
479                 {
480                         if (String.IsNullOrWhiteSpace(filePath))
481                                 throw new ArgumentNullException("filePath");
482                         if (!Directory.Exists(filePath))
483                                 throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath");
484                         Contract.EndContractBlock();
485
486                         var pair=(from monitor in  Monitors
487                                                            where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)
488                                                                    select monitor).FirstOrDefault();
489                         var accountMonitor = pair.Value;            
490                         var info = accountMonitor.GetContainerInfo(filePath);
491
492                         
493
494                         var containerProperties = new ContainerPropertiesViewModel(this, info,filePath);
495                         _windowManager.ShowWindow(containerProperties);
496                 }
497
498                 public void SynchNow()
499                 {
500                         _pollAgent.SynchNow();
501                 }
502
503                 public ObjectInfo RefreshObjectInfo(ObjectInfo currentInfo)
504                 {
505                         if (currentInfo==null)
506                                 throw new ArgumentNullException("currentInfo");
507                         Contract.EndContractBlock();
508
509                         var monitor = Monitors[currentInfo.Account];
510                         var newInfo=monitor.CloudClient.GetObjectInfo(currentInfo.Account, currentInfo.Container, currentInfo.Name);
511                         return newInfo;
512                 }
513
514                 public ContainerInfo RefreshContainerInfo(ContainerInfo container)
515                 {
516                         if (container == null)
517                                 throw new ArgumentNullException("container");
518                         Contract.EndContractBlock();
519
520                         var monitor = Monitors[container.Account];
521                         var newInfo = monitor.CloudClient.GetContainerInfo(container.Account, container.Name);
522                         return newInfo;
523                 }
524
525
526                 public void ToggleSynching()
527                 {
528                         bool isPaused=false;
529                         foreach (var pair in Monitors)
530                         {
531                                 var monitor = pair.Value;
532                                 monitor.Pause = !monitor.Pause;
533                                 isPaused = monitor.Pause;
534                         }
535
536                         PauseSyncCaption = isPaused ? "Resume syncing" : "Pause syncing";
537                         var iconKey = isPaused? "TraySyncPaused" : "TrayInSynch";
538                         StatusIcon = String.Format(@"../Images/{0}.ico", iconKey);
539                 }
540
541                 public void ExitPithos()
542                 {
543                         foreach (var pair in Monitors)
544                         {
545                                 var monitor = pair.Value;
546                                 monitor.Stop();
547                         }
548
549                         ((Window)GetView()).Close();
550                 }
551                 #endregion
552
553
554                 private readonly Dictionary<PithosStatus, StatusInfo> _iconNames = new List<StatusInfo>
555                         {
556                                 new StatusInfo(PithosStatus.InSynch, "All files up to date", "TrayInSynch"),
557                                 new StatusInfo(PithosStatus.Syncing, "Syncing Files", "TraySynching"),
558                                 new StatusInfo(PithosStatus.SyncPaused, "Sync Paused", "TraySyncPaused")
559                         }.ToDictionary(s => s.Status);
560
561                 readonly IWindowManager _windowManager;
562                 
563
564                 ///<summary>
565                 /// Updates the visual status indicators of the application depending on status changes, e.g. icon, stat                
566                 ///</summary>
567                 public void UpdateStatus()
568                 {
569                         var pithosStatus = _statusChecker.GetPithosStatus();
570
571                         if (_iconNames.ContainsKey(pithosStatus))
572                         {
573                                 var info = _iconNames[pithosStatus];
574                                 StatusIcon = String.Format(@"../Images/{0}.ico", info.IconName);
575
576
577
578                                 StatusMessage = String.Format("Pithos {0}\r\n{1}", _fileVersion.Value.FileVersion,info.StatusText);
579                         }
580                         
581                         //_events.Publish(new Notification { Title = "Start", Message = "Start Monitoring", Level = TraceLevel.Info});
582                 }
583
584
585            
586                 private Task StartMonitor(PithosMonitor monitor,int retries=0)
587                 {
588                         return Task.Factory.StartNew(() =>
589                         {
590                                 using (log4net.ThreadContext.Stacks["Monitor"].Push("Start"))
591                                 {
592                                         try
593                                         {
594                                                 Log.InfoFormat("Start Monitoring {0}", monitor.UserName);
595
596                                                 monitor.Start();
597                                         }
598                                         catch (WebException exc)
599                                         {
600                                                 if (AbandonRetry(monitor, retries))
601                                                         return;
602
603                                                 HttpStatusCode statusCode =HttpStatusCode.OK;
604                                                 var response = exc.Response as HttpWebResponse;
605                                                 if(response!=null)
606                                                         statusCode = response.StatusCode;
607
608                                                 switch (statusCode)
609                                                 {
610                                                         case HttpStatusCode.Unauthorized:
611                                                                 var message = String.Format("API Key Expired for {0}. Starting Renewal",
612                                                                                                                         monitor.UserName);
613                                                                 Log.Error(message, exc);
614                                                         var account = Settings.Accounts.Find(acc => acc.AccountName == monitor.UserName);                                
615                                                         account.IsExpired = true;
616                                 Notify(new ExpirationNotification(account));
617                                                                 //TryAuthorize(monitor.UserName, retries).Wait();
618                                                                 break;
619                                                         case HttpStatusCode.ProxyAuthenticationRequired:
620                                                                 TryAuthenticateProxy(monitor,retries);
621                                                                 break;
622                                                         default:
623                                                                 TryLater(monitor, exc, retries);
624                                                                 break;
625                                                 }
626                                         }
627                                         catch (Exception exc)
628                                         {
629                                                 if (AbandonRetry(monitor, retries)) 
630                                                         return;
631
632                                                 TryLater(monitor,exc,retries);
633                                         }
634                                 }
635                         });
636                 }
637
638                 private void TryAuthenticateProxy(PithosMonitor monitor,int retries)
639                 {
640                         Execute.OnUIThread(() =>
641                                                                    {                                       
642                                                                            var proxyAccount = IoC.Get<ProxyAccountViewModel>();
643                                                                                 proxyAccount.Settings = Settings;
644                                                                            if (true != _windowManager.ShowDialog(proxyAccount)) 
645                                                                                    return;
646                                                                            StartMonitor(monitor, retries);
647                                                                            NotifyOfPropertyChange(() => Accounts);
648                                                                    });
649                 }
650
651                 private bool AbandonRetry(PithosMonitor monitor, int retries)
652                 {
653                         if (retries > 1)
654                         {
655                                 var message = String.Format("Monitoring of account {0} has failed too many times. Will not retry",
656                                                                                         monitor.UserName);
657                                 _events.Publish(new Notification
658                                                                         {Title = "Account monitoring failed", Message = message, Level = TraceLevel.Error});
659                                 return true;
660                         }
661                         return false;
662                 }
663
664
665             private void TryLater(PithosMonitor monitor, Exception exc,int retries)
666                 {
667                         var message = String.Format("An exception occured. Can't start monitoring\nWill retry in 10 seconds");
668                         Task.Factory.StartNewDelayed(10000, () => StartMonitor(monitor,retries+1));
669                         _events.Publish(new Notification
670                                                                 {Title = "Error", Message = message, Level = TraceLevel.Error});
671                         Log.Error(message, exc);
672                 }
673
674
675                 public void NotifyChange(string status, TraceLevel level=TraceLevel.Info)
676                 {
677                         StatusMessage = status;
678                         
679                         _events.Publish(new Notification { Title = "Pithos", Message = status, Level = level });
680                 }
681
682                 public void NotifyChangedFile(string filePath)
683                 {
684             if (RecentFiles.Any(e => e.FullPath == filePath))
685                 return;
686             
687                         IProducerConsumerCollection<FileEntry> files=RecentFiles;
688                         FileEntry popped;
689                         while (files.Count > 5)
690                                 files.TryTake(out popped);
691             var entry = new FileEntry { FullPath = filePath };
692                         files.TryAdd(entry);
693                 }
694
695                 public void NotifyAccount(AccountInfo account)
696                 {
697                         if (account== null)
698                                 return;
699                         //TODO: What happens to an existing account whose Token has changed?
700                         account.SiteUri= String.Format("{0}/ui/?token={1}&user={2}",
701                                 account.SiteUri, Uri.EscapeDataString(account.Token),
702                                 Uri.EscapeDataString(account.UserName));
703
704                         if (Accounts.All(item => item.UserName != account.UserName))
705                                 Accounts.TryAdd(account);
706
707                 }
708
709                 public void NotifyConflicts(IEnumerable<FileSystemInfo> conflictFiles, string message)
710                 {
711                         if (conflictFiles == null)
712                                 return;
713                     //Convert to list to avoid multiple iterations
714             var files = conflictFiles.ToList();
715                         if (files.Count==0)
716                                 return;
717
718                         UpdateStatus();
719                         //TODO: Create a more specific message. For now, just show a warning
720                         NotifyForFiles(files,message,TraceLevel.Warning);
721
722                 }
723
724                 public void NotifyForFiles(IEnumerable<FileSystemInfo> files, string message,TraceLevel level=TraceLevel.Info)
725                 {
726                         if (files == null)
727                                 return;
728                         if (!files.Any())
729                                 return;
730
731                         StatusMessage = message;
732
733                         _events.Publish(new Notification { Title = "Pithos", Message = message, Level = level});
734                 }
735
736                 public void Notify(Notification notification)
737                 {
738                         _events.Publish(notification);
739                 }
740
741
742                 public void RemoveMonitor(string accountName)
743                 {
744                         if (String.IsNullOrWhiteSpace(accountName))
745                                 return;
746
747                         var accountInfo=_accounts.FirstOrDefault(account => account.UserName == accountName);
748             if (accountInfo != null)
749             {
750                 _accounts.TryRemove(accountInfo);
751                 _pollAgent.RemoveAccount(accountInfo);
752             }
753
754                     PithosMonitor monitor;
755                         if (Monitors.TryRemove(accountName, out monitor))
756                         {
757                                 monitor.Stop();
758                 //TODO: Also remove any pending actions for this account
759                 //from the network queue                
760                         }
761                 }
762
763                 public void RefreshOverlays()
764                 {
765                         foreach (var pair in Monitors)
766                         {
767                                 var monitor = pair.Value;
768
769                                 var path = monitor.RootPath;
770
771                                 if (String.IsNullOrWhiteSpace(path))
772                                         continue;
773
774                                 if (!Directory.Exists(path) && !File.Exists(path))
775                                         continue;
776
777                                 IntPtr pathPointer = Marshal.StringToCoTaskMemAuto(path);
778
779                                 try
780                                 {
781                                         NativeMethods.SHChangeNotify(HChangeNotifyEventID.SHCNE_UPDATEITEM,
782                                                                                                  HChangeNotifyFlags.SHCNF_PATHW | HChangeNotifyFlags.SHCNF_FLUSHNOWAIT,
783                                                                                                  pathPointer, IntPtr.Zero);
784                                 }
785                                 finally
786                                 {
787                                         Marshal.FreeHGlobal(pathPointer);
788                                 }
789                         }
790                 }
791
792                 #region Event Handlers
793                 
794                 public void Handle(SelectiveSynchChanges message)
795                 {
796                         var accountName = message.Account.AccountName;
797                         PithosMonitor monitor;
798                         if (_monitors.TryGetValue(accountName, out monitor))
799                         {
800                                 monitor.SetSelectivePaths(message.Uris,message.Added,message.Removed);
801
802                         }
803                         
804                 }
805
806
807                 private bool _pollStarted;
808
809                 //SMELL: Doing so much work for notifications in the shell is wrong
810                 //The notifications should be moved to their own view/viewmodel pair
811                 //and different templates should be used for different message types
812                 //This will also allow the addition of extra functionality, eg. actions
813                 //
814                 public void Handle(Notification notification)
815                 {
816                         UpdateStatus();
817
818                         if (!Settings.ShowDesktopNotifications)
819                                 return;
820
821                         if (notification is PollNotification)
822                         {
823                                 _pollStarted = true;
824                                 return;
825                         }
826                         if (notification is CloudNotification)
827                         {
828                                 if (!_pollStarted) 
829                                         return;
830                                 _pollStarted= false;
831                                 notification.Title = "Pithos";
832                                 notification.Message = "Start Synchronisation";
833                         }
834
835                         if (String.IsNullOrWhiteSpace(notification.Message) && String.IsNullOrWhiteSpace(notification.Title))
836                                 return;
837
838                         BalloonIcon icon;
839                         switch (notification.Level)
840                         {
841                                 case TraceLevel.Error:
842                                         icon = BalloonIcon.Error;
843                                         break;
844                                 case TraceLevel.Info:
845                                 case TraceLevel.Verbose:
846                                         icon = BalloonIcon.Info;
847                                         break;
848                                 case TraceLevel.Warning:
849                                         icon = BalloonIcon.Warning;
850                                         break;
851                                 default:
852                                         icon = BalloonIcon.None;
853                                         break;
854                         }
855                         
856                         if (Settings.ShowDesktopNotifications)
857                         {
858                                 var tv = (ShellView) GetView();
859                             System.Action clickAction = null;
860                 if (notification is ExpirationNotification)
861                 {
862                     clickAction = ()=>ShowPreferences("AccountTab");
863                 }
864                                 var balloon=new PithosBalloon{Title=notification.Title,Message=notification.Message,Icon=icon,ClickAction=clickAction};
865                                 tv.TaskbarView.ShowCustomBalloon(balloon,PopupAnimation.Fade,4000);
866 //                              tv.TaskbarView.ShowBalloonTip(notification.Title, notification.Message, icon);
867                         }
868                 }
869                 #endregion
870
871                 public void Handle(ShowFilePropertiesEvent message)
872                 {
873                         if (message == null)
874                                 throw new ArgumentNullException("message");
875                         if (String.IsNullOrWhiteSpace(message.FileName) )
876                                 throw new ArgumentException("message");
877                         Contract.EndContractBlock();
878
879                         var fileName = message.FileName;
880                         //TODO: Display file properties for non-container folders
881                         if (File.Exists(fileName))
882                                 //Retrieve the full name with exact casing. Pithos names are case sensitive                             
883                                 ShowFileProperties(FileInfoExtensions.GetProperFilePathCapitalization(fileName));
884                         else if (Directory.Exists(fileName))
885                                 //Retrieve the full name with exact casing. Pithos names are case sensitive
886                         {
887                                 var path = FileInfoExtensions.GetProperDirectoryCapitalization(fileName);
888                                 if (IsContainer(path))
889                                         ShowContainerProperties(path);
890                                 else
891                                         ShowFileProperties(path);
892                         }
893                 }
894
895                 private bool IsContainer(string path)
896                 {
897                         var matchingFolders = from account in _accounts
898                                                                   from rootFolder in Directory.GetDirectories(account.AccountPath)
899                                                                   where rootFolder.Equals(path, StringComparison.InvariantCultureIgnoreCase)
900                                                                   select rootFolder;
901                         return matchingFolders.Any();
902                 }
903
904                 public FileStatus GetFileStatus(string localFileName)
905                 {
906                         if (String.IsNullOrWhiteSpace(localFileName))
907                                 throw new ArgumentNullException("localFileName");
908                         Contract.EndContractBlock();
909                         
910                         var statusKeeper = IoC.Get<IStatusKeeper>();
911                         var status=statusKeeper.GetFileStatus(localFileName);
912                         return status;
913                 }
914         }
915 }