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