Modified preferences to save account additions/deletions only when the user save...
[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("Pithos");
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 ShowFileProperties()
427                 {
428                         var account = Settings.Accounts.First(acc => acc.IsActive);            
429                         var dir = new DirectoryInfo(account.RootPath + @"\pithos");
430                         var files=dir.GetFiles();
431                         var r=new Random();
432                         var idx=r.Next(0, files.Length);
433                         ShowFileProperties(files[idx].FullName);            
434                 }
435
436                 public void ShowFileProperties(string filePath)
437                 {
438                         if (String.IsNullOrWhiteSpace(filePath))
439                                 throw new ArgumentNullException("filePath");
440                         if (!File.Exists(filePath) && !Directory.Exists(filePath))
441                                 throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath");
442                         Contract.EndContractBlock();
443
444                         var pair=(from monitor in  Monitors
445                                                            where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)
446                                                                    select monitor).FirstOrDefault();
447                         var accountMonitor = pair.Value;
448
449                         if (accountMonitor == null)
450                                 return;
451
452                         var infoTask=Task.Factory.StartNew(()=>accountMonitor.GetObjectInfo(filePath));
453
454                         
455
456                         var fileProperties = new FilePropertiesViewModel(this, infoTask,filePath);
457                         _windowManager.ShowWindow(fileProperties);
458                 } 
459                 
460                 public void ShowContainerProperties()
461                 {
462                         var account = Settings.Accounts.First(acc => acc.IsActive);            
463                         var dir = new DirectoryInfo(account.RootPath);
464                         var fullName = (from folder in dir.EnumerateDirectories()
465                                                         where (folder.Attributes & FileAttributes.Hidden) == 0
466                                                         select folder.FullName).First();
467                         ShowContainerProperties(fullName);            
468                 }
469
470                 public void ShowContainerProperties(string filePath)
471                 {
472                         if (String.IsNullOrWhiteSpace(filePath))
473                                 throw new ArgumentNullException("filePath");
474                         if (!Directory.Exists(filePath))
475                                 throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath");
476                         Contract.EndContractBlock();
477
478                         var pair=(from monitor in  Monitors
479                                                            where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)
480                                                                    select monitor).FirstOrDefault();
481                         var accountMonitor = pair.Value;            
482                         var info = accountMonitor.GetContainerInfo(filePath);
483
484                         
485
486                         var containerProperties = new ContainerPropertiesViewModel(this, info,filePath);
487                         _windowManager.ShowWindow(containerProperties);
488                 }
489
490                 public void SynchNow()
491                 {
492                         _pollAgent.SynchNow();
493                 }
494
495                 public ObjectInfo RefreshObjectInfo(ObjectInfo currentInfo)
496                 {
497                         if (currentInfo==null)
498                                 throw new ArgumentNullException("currentInfo");
499                         Contract.EndContractBlock();
500
501                         var monitor = Monitors[currentInfo.Account];
502                         var newInfo=monitor.CloudClient.GetObjectInfo(currentInfo.Account, currentInfo.Container, currentInfo.Name);
503                         return newInfo;
504                 }
505
506                 public ContainerInfo RefreshContainerInfo(ContainerInfo container)
507                 {
508                         if (container == null)
509                                 throw new ArgumentNullException("container");
510                         Contract.EndContractBlock();
511
512                         var monitor = Monitors[container.Account];
513                         var newInfo = monitor.CloudClient.GetContainerInfo(container.Account, container.Name);
514                         return newInfo;
515                 }
516
517
518                 public void ToggleSynching()
519                 {
520                         bool isPaused=false;
521                         foreach (var pair in Monitors)
522                         {
523                                 var monitor = pair.Value;
524                                 monitor.Pause = !monitor.Pause;
525                                 isPaused = monitor.Pause;
526                         }
527
528                         PauseSyncCaption = isPaused ? "Resume syncing" : "Pause syncing";
529                         var iconKey = isPaused? "TraySyncPaused" : "TrayInSynch";
530                         StatusIcon = String.Format(@"../Images/{0}.ico", iconKey);
531                 }
532
533                 public void ExitPithos()
534                 {
535                         foreach (var pair in Monitors)
536                         {
537                                 var monitor = pair.Value;
538                                 monitor.Stop();
539                         }
540
541                         ((Window)GetView()).Close();
542                 }
543                 #endregion
544
545
546                 private readonly Dictionary<PithosStatus, StatusInfo> _iconNames = new List<StatusInfo>
547                         {
548                                 new StatusInfo(PithosStatus.InSynch, "All files up to date", "TrayInSynch"),
549                                 new StatusInfo(PithosStatus.Syncing, "Syncing Files", "TraySynching"),
550                                 new StatusInfo(PithosStatus.SyncPaused, "Sync Paused", "TraySyncPaused")
551                         }.ToDictionary(s => s.Status);
552
553                 readonly IWindowManager _windowManager;
554                 
555
556                 ///<summary>
557                 /// Updates the visual status indicators of the application depending on status changes, e.g. icon, stat                
558                 ///</summary>
559                 public void UpdateStatus()
560                 {
561                         var pithosStatus = _statusChecker.GetPithosStatus();
562
563                         if (_iconNames.ContainsKey(pithosStatus))
564                         {
565                                 var info = _iconNames[pithosStatus];
566                                 StatusIcon = String.Format(@"../Images/{0}.ico", info.IconName);
567
568
569
570                                 StatusMessage = String.Format("Pithos {0}\r\n{1}", _fileVersion.Value.FileVersion,info.StatusText);
571                         }
572                         
573                         //_events.Publish(new Notification { Title = "Start", Message = "Start Monitoring", Level = TraceLevel.Info});
574                 }
575
576
577            
578                 private Task StartMonitor(PithosMonitor monitor,int retries=0)
579                 {
580                         return Task.Factory.StartNew(() =>
581                         {
582                                 using (log4net.ThreadContext.Stacks["Monitor"].Push("Start"))
583                                 {
584                                         try
585                                         {
586                                                 Log.InfoFormat("Start Monitoring {0}", monitor.UserName);
587
588                                                 monitor.Start();
589                                         }
590                                         catch (WebException exc)
591                                         {
592                                                 if (AbandonRetry(monitor, retries))
593                                                         return;
594
595                                                 HttpStatusCode statusCode =HttpStatusCode.OK;
596                                                 var response = exc.Response as HttpWebResponse;
597                                                 if(response!=null)
598                                                         statusCode = response.StatusCode;
599
600                                                 switch (statusCode)
601                                                 {
602                                                         case HttpStatusCode.Unauthorized:
603                                                                 var message = String.Format("API Key Expired for {0}. Starting Renewal",
604                                                                                                                         monitor.UserName);
605                                                                 Log.Error(message, exc);
606                                                         var account = Settings.Accounts.Find(acc => acc.AccountName == monitor.UserName);                                
607                                                         account.IsExpired = true;
608                                 Notify(new ExpirationNotification(account));
609                                                                 //TryAuthorize(monitor.UserName, retries).Wait();
610                                                                 break;
611                                                         case HttpStatusCode.ProxyAuthenticationRequired:
612                                                                 TryAuthenticateProxy(monitor,retries);
613                                                                 break;
614                                                         default:
615                                                                 TryLater(monitor, exc, retries);
616                                                                 break;
617                                                 }
618                                         }
619                                         catch (Exception exc)
620                                         {
621                                                 if (AbandonRetry(monitor, retries)) 
622                                                         return;
623
624                                                 TryLater(monitor,exc,retries);
625                                         }
626                                 }
627                         });
628                 }
629
630                 private void TryAuthenticateProxy(PithosMonitor monitor,int retries)
631                 {
632                         Execute.OnUIThread(() =>
633                                                                    {                                       
634                                                                            var proxyAccount = IoC.Get<ProxyAccountViewModel>();
635                                                                                 proxyAccount.Settings = Settings;
636                                                                            if (true != _windowManager.ShowDialog(proxyAccount)) 
637                                                                                    return;
638                                                                            StartMonitor(monitor, retries);
639                                                                            NotifyOfPropertyChange(() => Accounts);
640                                                                    });
641                 }
642
643                 private bool AbandonRetry(PithosMonitor monitor, int retries)
644                 {
645                         if (retries > 1)
646                         {
647                                 var message = String.Format("Monitoring of account {0} has failed too many times. Will not retry",
648                                                                                         monitor.UserName);
649                                 _events.Publish(new Notification
650                                                                         {Title = "Account monitoring failed", Message = message, Level = TraceLevel.Error});
651                                 return true;
652                         }
653                         return false;
654                 }
655
656
657             private void TryLater(PithosMonitor monitor, Exception exc,int retries)
658                 {
659                         var message = String.Format("An exception occured. Can't start monitoring\nWill retry in 10 seconds");
660                         Task.Factory.StartNewDelayed(10000, () => StartMonitor(monitor,retries+1));
661                         _events.Publish(new Notification
662                                                                 {Title = "Error", Message = message, Level = TraceLevel.Error});
663                         Log.Error(message, exc);
664                 }
665
666
667                 public void NotifyChange(string status, TraceLevel level=TraceLevel.Info)
668                 {
669                         StatusMessage = status;
670                         
671                         _events.Publish(new Notification { Title = "Pithos", Message = status, Level = level });
672                 }
673
674                 public void NotifyChangedFile(string filePath)
675                 {
676             if (RecentFiles.Any(e => e.FullPath == filePath))
677                 return;
678             
679                         IProducerConsumerCollection<FileEntry> files=RecentFiles;
680                         FileEntry popped;
681                         while (files.Count > 5)
682                                 files.TryTake(out popped);
683             var entry = new FileEntry { FullPath = filePath };
684                         files.TryAdd(entry);
685                 }
686
687                 public void NotifyAccount(AccountInfo account)
688                 {
689                         if (account== null)
690                                 return;
691                         //TODO: What happens to an existing account whose Token has changed?
692                         account.SiteUri= String.Format("{0}/ui/?token={1}&user={2}",
693                                 account.SiteUri, Uri.EscapeDataString(account.Token),
694                                 Uri.EscapeDataString(account.UserName));
695
696                         if (Accounts.All(item => item.UserName != account.UserName))
697                                 Accounts.TryAdd(account);
698
699                 }
700
701                 public void NotifyConflicts(IEnumerable<FileSystemInfo> conflictFiles, string message)
702                 {
703                         if (conflictFiles == null)
704                                 return;
705                     //Convert to list to avoid multiple iterations
706             var files = conflictFiles.ToList();
707                         if (files.Count==0)
708                                 return;
709
710                         UpdateStatus();
711                         //TODO: Create a more specific message. For now, just show a warning
712                         NotifyForFiles(files,message,TraceLevel.Warning);
713
714                 }
715
716                 public void NotifyForFiles(IEnumerable<FileSystemInfo> files, string message,TraceLevel level=TraceLevel.Info)
717                 {
718                         if (files == null)
719                                 return;
720                         if (!files.Any())
721                                 return;
722
723                         StatusMessage = message;
724
725                         _events.Publish(new Notification { Title = "Pithos", Message = message, Level = level});
726                 }
727
728                 public void Notify(Notification notification)
729                 {
730                         _events.Publish(notification);
731                 }
732
733
734                 public void RemoveMonitor(string accountName)
735                 {
736                         if (String.IsNullOrWhiteSpace(accountName))
737                                 return;
738
739                         var accountInfo=_accounts.FirstOrDefault(account => account.UserName == accountName);
740             if (accountInfo != null)
741             {
742                 _accounts.TryRemove(accountInfo);
743                 _pollAgent.RemoveAccount(accountInfo);
744             }
745
746                     PithosMonitor monitor;
747                         if (Monitors.TryRemove(accountName, out monitor))
748                         {
749                                 monitor.Stop();
750                 //TODO: Also remove any pending actions for this account
751                 //from the network queue                
752                         }
753                 }
754
755                 public void RefreshOverlays()
756                 {
757                         foreach (var pair in Monitors)
758                         {
759                                 var monitor = pair.Value;
760
761                                 var path = monitor.RootPath;
762
763                                 if (String.IsNullOrWhiteSpace(path))
764                                         continue;
765
766                                 if (!Directory.Exists(path) && !File.Exists(path))
767                                         continue;
768
769                                 IntPtr pathPointer = Marshal.StringToCoTaskMemAuto(path);
770
771                                 try
772                                 {
773                                         NativeMethods.SHChangeNotify(HChangeNotifyEventID.SHCNE_UPDATEITEM,
774                                                                                                  HChangeNotifyFlags.SHCNF_PATHW | HChangeNotifyFlags.SHCNF_FLUSHNOWAIT,
775                                                                                                  pathPointer, IntPtr.Zero);
776                                 }
777                                 finally
778                                 {
779                                         Marshal.FreeHGlobal(pathPointer);
780                                 }
781                         }
782                 }
783
784                 #region Event Handlers
785                 
786                 public void Handle(SelectiveSynchChanges message)
787                 {
788                         var accountName = message.Account.AccountName;
789                         PithosMonitor monitor;
790                         if (_monitors.TryGetValue(accountName, out monitor))
791                         {
792                                 monitor.SetSelectivePaths(message.Uris,message.Added,message.Removed);
793
794                         }
795                         
796                 }
797
798
799                 private bool _pollStarted;
800
801                 //SMELL: Doing so much work for notifications in the shell is wrong
802                 //The notifications should be moved to their own view/viewmodel pair
803                 //and different templates should be used for different message types
804                 //This will also allow the addition of extra functionality, eg. actions
805                 //
806                 public void Handle(Notification notification)
807                 {
808                         UpdateStatus();
809
810                         if (!Settings.ShowDesktopNotifications)
811                                 return;
812
813                         if (notification is PollNotification)
814                         {
815                                 _pollStarted = true;
816                                 return;
817                         }
818                         if (notification is CloudNotification)
819                         {
820                                 if (!_pollStarted) 
821                                         return;
822                                 _pollStarted= false;
823                                 notification.Title = "Pithos";
824                                 notification.Message = "Start Synchronisation";
825                         }
826
827                         if (String.IsNullOrWhiteSpace(notification.Message) && String.IsNullOrWhiteSpace(notification.Title))
828                                 return;
829
830                         BalloonIcon icon;
831                         switch (notification.Level)
832                         {
833                                 case TraceLevel.Error:
834                                         icon = BalloonIcon.Error;
835                                         break;
836                                 case TraceLevel.Info:
837                                 case TraceLevel.Verbose:
838                                         icon = BalloonIcon.Info;
839                                         break;
840                                 case TraceLevel.Warning:
841                                         icon = BalloonIcon.Warning;
842                                         break;
843                                 default:
844                                         icon = BalloonIcon.None;
845                                         break;
846                         }
847                         
848                         if (Settings.ShowDesktopNotifications)
849                         {
850                                 var tv = (ShellView) GetView();
851                             System.Action clickAction = null;
852                 if (notification is ExpirationNotification)
853                 {
854                     clickAction = ()=>ShowPreferences("AccountTab");
855                 }
856                                 var balloon=new PithosBalloon{Title=notification.Title,Message=notification.Message,Icon=icon,ClickAction=clickAction};
857                                 tv.TaskbarView.ShowCustomBalloon(balloon,PopupAnimation.Fade,4000);
858 //                              tv.TaskbarView.ShowBalloonTip(notification.Title, notification.Message, icon);
859                         }
860                 }
861                 #endregion
862
863                 public void Handle(ShowFilePropertiesEvent message)
864                 {
865                         if (message == null)
866                                 throw new ArgumentNullException("message");
867                         if (String.IsNullOrWhiteSpace(message.FileName) )
868                                 throw new ArgumentException("message");
869                         Contract.EndContractBlock();
870
871                         var fileName = message.FileName;
872                         //TODO: Display file properties for non-container folders
873                         if (File.Exists(fileName))
874                                 //Retrieve the full name with exact casing. Pithos names are case sensitive                             
875                                 ShowFileProperties(FileInfoExtensions.GetProperFilePathCapitalization(fileName));
876                         else if (Directory.Exists(fileName))
877                                 //Retrieve the full name with exact casing. Pithos names are case sensitive
878                         {
879                                 var path = FileInfoExtensions.GetProperDirectoryCapitalization(fileName);
880                                 if (IsContainer(path))
881                                         ShowContainerProperties(path);
882                                 else
883                                         ShowFileProperties(path);
884                         }
885                 }
886
887                 private bool IsContainer(string path)
888                 {
889                         var matchingFolders = from account in _accounts
890                                                                   from rootFolder in Directory.GetDirectories(account.AccountPath)
891                                                                   where rootFolder.Equals(path, StringComparison.InvariantCultureIgnoreCase)
892                                                                   select rootFolder;
893                         return matchingFolders.Any();
894                 }
895
896                 public FileStatus GetFileStatus(string localFileName)
897                 {
898                         if (String.IsNullOrWhiteSpace(localFileName))
899                                 throw new ArgumentNullException("localFileName");
900                         Contract.EndContractBlock();
901                         
902                         var statusKeeper = IoC.Get<IStatusKeeper>();
903                         var status=statusKeeper.GetFileStatus(localFileName);
904                         return status;
905                 }
906         }
907 }