Multiple changes:
[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
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(MethodBase.GetCurrentMethod().DeclaringType);
123
124                 //Lazily initialized File Version info. This is done once and lazily to avoid blocking the UI
125                 private readonly Lazy<FileVersionInfo> _fileVersion;
126
127             private readonly 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
178                 protected override void OnActivate()
179                 {
180                         base.OnActivate();
181
182                         
183
184                         StartMonitoring();                    
185                 }
186
187
188
189                 private async void StartMonitoring()
190                 {
191                         try
192                         {
193                                 var accounts = Settings.Accounts.Select(MonitorAccount);
194                                 await TaskEx.WhenAll(accounts);
195                                 _statusService = StatusService.Start();
196
197 /*
198                                 foreach (var account in Settings.Accounts)
199                                 {
200                                         await MonitorAccount(account);
201                                 }
202 */
203                                 
204                         }
205                         catch (AggregateException exc)
206                         {
207                                 exc.Handle(e =>
208                                 {
209                                         Log.Error("Error while starting monitoring", e);
210                                         return true;
211                                 });
212                                 throw;
213                         }
214                 }
215
216                 protected override void OnDeactivate(bool close)
217                 {
218                         base.OnDeactivate(close);
219                         if (close)
220                         {
221                                 StatusService.Stop(_statusService);
222                                 _statusService = null;
223                         }
224                 }
225
226                 public Task MonitorAccount(AccountSettings account)
227                 {
228                         return Task.Factory.StartNew(() =>
229                         {                                                
230                                 PithosMonitor monitor;
231                                 var accountName = account.AccountName;
232
233                                 if (_monitors.TryGetValue(accountName, out monitor))
234                                 {
235                                         //If the account is active
236                     if (account.IsActive)
237                     {
238                         //The Api Key may have changed throuth the Preferences dialog
239                         monitor.ApiKey = account.ApiKey;
240                                                 Debug.Assert(monitor.StatusNotification == this,"An existing monitor should already have a StatusNotification service object");
241                         monitor.RootPath = account.RootPath;
242                         //Start the monitor. It's OK to start an already started monitor,
243                         //it will just ignore the call                        
244                         StartMonitor(monitor).Wait();
245                     }
246                     else
247                     {
248                         //If the account is inactive
249                         //Stop and remove the monitor
250                         RemoveMonitor(accountName);
251                     }
252                                         return;
253                                 }
254
255                                 
256                                 //Create a new monitor/ Can't use MEF here, it would return a single instance for all monitors
257                                 monitor = new PithosMonitor
258                                                           {
259                                                                   UserName = accountName,
260                                                                   ApiKey = account.ApiKey,                                  
261                                                                   StatusNotification = this,
262                                                                   RootPath = account.RootPath
263                                                           };
264                                 //PithosMonitor uses MEF so we need to resolve it
265                                 IoC.BuildUp(monitor);
266
267                                 monitor.AuthenticationUrl = account.ServerUrl;
268
269                                 _monitors[accountName] = monitor;
270
271                                 if (account.IsActive)
272                                 {
273                                         //Don't start a monitor if it doesn't have an account and ApiKey
274                                         if (String.IsNullOrWhiteSpace(monitor.UserName) ||
275                                                 String.IsNullOrWhiteSpace(monitor.ApiKey))
276                                                 return;
277                                         StartMonitor(monitor);
278                                 }
279                         });
280                 }
281
282
283                 protected override void OnViewLoaded(object view)
284                 {
285                         UpdateStatus();
286                         var window = (Window)view;            
287                         TaskEx.Delay(1000).ContinueWith(t => Execute.OnUIThread(window.Hide));
288                         base.OnViewLoaded(view);
289                 }
290
291
292                 #region Status Properties
293
294                 private string _statusMessage;
295                 public string StatusMessage
296                 {
297                         get { return _statusMessage; }
298                         set
299                         {
300                                 _statusMessage = value;
301                                 NotifyOfPropertyChange(() => StatusMessage);
302                         }
303                 }
304
305                 private readonly ObservableConcurrentCollection<AccountInfo> _accounts = new ObservableConcurrentCollection<AccountInfo>();
306                 public ObservableConcurrentCollection<AccountInfo> Accounts
307                 {
308                         get { return _accounts; }
309                 }
310
311                 public bool HasAccounts
312                 {
313                         get { return _accounts.Count > 0; }
314                 }
315
316
317                 public string OpenFolderCaption
318                 {
319                         get
320                         {
321                                 return (_accounts.Count == 0)
322                                                 ? "No Accounts Defined"
323                                                 : "Open Pithos Folder";
324                         }
325                 }
326
327                 private string _pauseSyncCaption="Pause Synching";
328                 public string PauseSyncCaption
329                 {
330                         get { return _pauseSyncCaption; }
331                         set
332                         {
333                                 _pauseSyncCaption = value;
334                                 NotifyOfPropertyChange(() => PauseSyncCaption);
335                         }
336                 }
337
338                 private readonly ObservableConcurrentCollection<FileEntry> _recentFiles = new ObservableConcurrentCollection<FileEntry>();
339                 public ObservableConcurrentCollection<FileEntry> RecentFiles
340                 {
341                         get { return _recentFiles; }
342                 }
343
344
345                 private string _statusIcon="../Images/Pithos.ico";
346                 public string StatusIcon
347                 {
348                         get { return _statusIcon; }
349                         set
350                         {
351                                 //TODO: Ensure all status icons use the Pithos logo
352                                 _statusIcon = value;
353                                 NotifyOfPropertyChange(() => StatusIcon);
354                         }
355                 }
356
357                 #endregion
358
359                 #region Commands
360
361         public void ShowPreferences()
362         {
363             ShowPreferences(null);
364         }
365
366                 public void ShowPreferences(string currentTab)
367                 {
368                         //Settings.Reload();
369                     var preferences = new PreferencesViewModel(_windowManager, _events, this, Settings,currentTab);
370                     _windowManager.ShowDialog(preferences);
371                         
372                 }
373
374                 public void AboutPithos()
375                 {
376                         var about = new AboutViewModel();
377                         _windowManager.ShowWindow(about);
378                 }
379
380                 public void SendFeedback()
381                 {
382                         var feedBack =  IoC.Get<FeedbackViewModel>();
383                         _windowManager.ShowWindow(feedBack);
384                 }
385
386                 //public PithosCommand OpenPithosFolderCommand { get; private set; }
387
388                 public void OpenPithosFolder()
389                 {
390                         var account = Settings.Accounts.FirstOrDefault(acc => acc.IsActive);
391                         if (account == null)
392                                 return;
393                         Process.Start(account.RootPath);
394                 }
395
396                 public void OpenPithosFolder(AccountInfo account)
397                 {
398                         Process.Start(account.AccountPath);
399                 }
400
401                 
402 /*
403                 public void GoToSite()
404                 {            
405                         var site = Properties.Settings.Default.PithosSite;
406                         Process.Start(site);            
407                 }
408 */
409
410                 public void GoToSite(AccountInfo account)
411                 {
412                     var uri = account.SiteUri.Replace("http://","https://");            
413                     Process.Start(uri);
414                 }
415
416             /// <summary>
417         /// Open an explorer window to the target path's directory
418         /// and select the file
419         /// </summary>
420         /// <param name="entry"></param>
421         public void GoToFile(FileEntry entry)
422         {
423             var fullPath = entry.FullPath;
424             if (!File.Exists(fullPath) && !Directory.Exists(fullPath))
425                 return;
426             Process.Start("explorer.exe","/select, " + fullPath);
427         }
428
429         public void OpenLogPath()
430         {
431             var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
432             var pithosDataPath = Path.Combine(appDataPath, "GRNET");
433
434             Process.Start(pithosDataPath);
435         }
436         
437         public void ShowFileProperties()
438                 {
439                         var account = Settings.Accounts.First(acc => acc.IsActive);            
440                         var dir = new DirectoryInfo(account.RootPath + @"\pithos");
441                         var files=dir.GetFiles();
442                         var r=new Random();
443                         var idx=r.Next(0, files.Length);
444                         ShowFileProperties(files[idx].FullName);            
445                 }
446
447                 public void ShowFileProperties(string filePath)
448                 {
449                         if (String.IsNullOrWhiteSpace(filePath))
450                                 throw new ArgumentNullException("filePath");
451                         if (!File.Exists(filePath) && !Directory.Exists(filePath))
452                                 throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath");
453                         Contract.EndContractBlock();
454
455                         var pair=(from monitor in  Monitors
456                                                            where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)
457                                                                    select monitor).FirstOrDefault();
458                         var accountMonitor = pair.Value;
459
460                         if (accountMonitor == null)
461                                 return;
462
463                         var infoTask=Task.Factory.StartNew(()=>accountMonitor.GetObjectInfo(filePath));
464
465                         
466
467                         var fileProperties = new FilePropertiesViewModel(this, infoTask,filePath);
468                         _windowManager.ShowWindow(fileProperties);
469                 } 
470                 
471                 public void ShowContainerProperties()
472                 {
473                         var account = Settings.Accounts.First(acc => acc.IsActive);            
474                         var dir = new DirectoryInfo(account.RootPath);
475                         var fullName = (from folder in dir.EnumerateDirectories()
476                                                         where (folder.Attributes & FileAttributes.Hidden) == 0
477                                                         select folder.FullName).First();
478                         ShowContainerProperties(fullName);            
479                 }
480
481                 public void ShowContainerProperties(string filePath)
482                 {
483                         if (String.IsNullOrWhiteSpace(filePath))
484                                 throw new ArgumentNullException("filePath");
485                         if (!Directory.Exists(filePath))
486                                 throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath");
487                         Contract.EndContractBlock();
488
489                         var pair=(from monitor in  Monitors
490                                                            where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)
491                                                                    select monitor).FirstOrDefault();
492                         var accountMonitor = pair.Value;            
493                         var info = accountMonitor.GetContainerInfo(filePath);
494
495                         
496
497                         var containerProperties = new ContainerPropertiesViewModel(this, info,filePath);
498                         _windowManager.ShowWindow(containerProperties);
499                 }
500
501                 public void SynchNow()
502                 {
503                         _pollAgent.SynchNow();
504                 }
505
506                 public ObjectInfo RefreshObjectInfo(ObjectInfo currentInfo)
507                 {
508                         if (currentInfo==null)
509                                 throw new ArgumentNullException("currentInfo");
510                         Contract.EndContractBlock();
511
512                         var monitor = Monitors[currentInfo.Account];
513                         var newInfo=monitor.CloudClient.GetObjectInfo(currentInfo.Account, currentInfo.Container, currentInfo.Name);
514                         return newInfo;
515                 }
516
517                 public ContainerInfo RefreshContainerInfo(ContainerInfo container)
518                 {
519                         if (container == null)
520                                 throw new ArgumentNullException("container");
521                         Contract.EndContractBlock();
522
523                         var monitor = Monitors[container.Account];
524                         var newInfo = monitor.CloudClient.GetContainerInfo(container.Account, container.Name);
525                         return newInfo;
526                 }
527
528
529                 public void ToggleSynching()
530                 {
531                         bool isPaused=false;
532                         foreach (var pair in Monitors)
533                         {
534                                 var monitor = pair.Value;
535                                 monitor.Pause = !monitor.Pause;
536                                 isPaused = monitor.Pause;
537                         }
538                         
539
540                         PauseSyncCaption = isPaused ? "Resume syncing" : "Pause syncing";
541                         var iconKey = isPaused? "TraySyncPaused" : "TrayInSynch";
542                         StatusIcon = String.Format(@"../Images/{0}.ico", iconKey);
543                 }
544
545                 public void ExitPithos()
546                 {
547                         foreach (var pair in Monitors)
548                         {
549                                 var monitor = pair.Value;
550                                 monitor.Stop();
551                         }
552
553                         ((Window)GetView()).Close();
554                 }
555                 #endregion
556
557
558                 private readonly Dictionary<PithosStatus, StatusInfo> _iconNames = new List<StatusInfo>
559                         {
560                                 new StatusInfo(PithosStatus.InSynch, "All files up to date", "TrayInSynch"),
561                                 new StatusInfo(PithosStatus.PollSyncing, "Polling Files", "TraySynching"),
562                 new StatusInfo(PithosStatus.LocalSyncing, "Syncing Files", "TraySynching"),
563                                 new StatusInfo(PithosStatus.SyncPaused, "Sync Paused", "TraySyncPaused")
564                         }.ToDictionary(s => s.Status);
565
566                 readonly IWindowManager _windowManager;
567                 
568         //private int _syncCount=0;
569
570
571         private PithosStatus _pithosStatus = PithosStatus.Disconnected;
572
573         public void SetPithosStatus(PithosStatus status)
574         {
575             if (_pithosStatus == PithosStatus.LocalSyncing && status == PithosStatus.PollComplete)
576                 return;
577             if (_pithosStatus == PithosStatus.PollSyncing && status == PithosStatus.LocalComplete)
578                 return;
579             if (status == PithosStatus.LocalComplete || status == PithosStatus.PollComplete)
580                 _pithosStatus = PithosStatus.InSynch;
581             else
582                 _pithosStatus = status;
583             UpdateStatus();
584         }
585
586         public void SetPithosStatus(PithosStatus status,string message)
587         {
588             StatusMessage = message;
589             SetPithosStatus(status);
590         }
591
592
593
594                 ///<summary>
595                 /// Updates the visual status indicators of the application depending on status changes, e.g. icon, stat                
596                 ///</summary>
597                 public void UpdateStatus()
598                 {
599
600                         if (_iconNames.ContainsKey(_pithosStatus))
601                         {
602                                 var info = _iconNames[_pithosStatus];
603                                 StatusIcon = String.Format(@"../Images/{0}.ico", info.IconName);
604                         }
605
606             if (_pithosStatus == PithosStatus.InSynch)
607                 StatusMessage = "All files up to date";
608                 }
609
610
611            
612                 private Task StartMonitor(PithosMonitor monitor,int retries=0)
613                 {
614                         return Task.Factory.StartNew(() =>
615                         {
616                                 using (log4net.ThreadContext.Stacks["Monitor"].Push("Start"))
617                                 {
618                                         try
619                                         {
620                                                 Log.InfoFormat("Start Monitoring {0}", monitor.UserName);
621
622                                                 monitor.Start();
623                                         }
624                                         catch (WebException exc)
625                                         {
626                                                 if (AbandonRetry(monitor, retries))
627                                                         return;
628
629                                                 HttpStatusCode statusCode =HttpStatusCode.OK;
630                                                 var response = exc.Response as HttpWebResponse;
631                                                 if(response!=null)
632                                                         statusCode = response.StatusCode;
633
634                                                 switch (statusCode)
635                                                 {
636                                                         case HttpStatusCode.Unauthorized:
637                                                                 var message = String.Format("API Key Expired for {0}. Starting Renewal",
638                                                                                                                         monitor.UserName);
639                                                                 Log.Error(message, exc);
640                                                         var account = Settings.Accounts.Find(acc => acc.AccountName == monitor.UserName);                                
641                                                         account.IsExpired = true;
642                                 Notify(new ExpirationNotification(account));
643                                                                 //TryAuthorize(monitor.UserName, retries).Wait();
644                                                                 break;
645                                                         case HttpStatusCode.ProxyAuthenticationRequired:
646                                                                 TryAuthenticateProxy(monitor,retries);
647                                                                 break;
648                                                         default:
649                                                                 TryLater(monitor, exc, retries);
650                                                                 break;
651                                                 }
652                                         }
653                                         catch (Exception exc)
654                                         {
655                                                 if (AbandonRetry(monitor, retries)) 
656                                                         return;
657
658                                                 TryLater(monitor,exc,retries);
659                                         }
660                                 }
661                         });
662                 }
663
664                 private void TryAuthenticateProxy(PithosMonitor monitor,int retries)
665                 {
666                         Execute.OnUIThread(() =>
667                                                                    {                                       
668                                                                            var proxyAccount = IoC.Get<ProxyAccountViewModel>();
669                                                                                 proxyAccount.Settings = Settings;
670                                                                            if (true != _windowManager.ShowDialog(proxyAccount)) 
671                                                                                    return;
672                                                                            StartMonitor(monitor, retries);
673                                                                            NotifyOfPropertyChange(() => Accounts);
674                                                                    });
675                 }
676
677                 private bool AbandonRetry(PithosMonitor monitor, int retries)
678                 {
679                         if (retries > 1)
680                         {
681                                 var message = String.Format("Monitoring of account {0} has failed too many times. Will not retry",
682                                                                                         monitor.UserName);
683                                 _events.Publish(new Notification
684                                                                         {Title = "Account monitoring failed", Message = message, Level = TraceLevel.Error});
685                                 return true;
686                         }
687                         return false;
688                 }
689
690
691             private void TryLater(PithosMonitor monitor, Exception exc,int retries)
692                 {
693                         var message = String.Format("An exception occured. Can't start monitoring\nWill retry in 10 seconds");
694                         Task.Factory.StartNewDelayed(10000, () => StartMonitor(monitor,retries+1));
695                         _events.Publish(new Notification
696                                                                 {Title = "Error", Message = message, Level = TraceLevel.Error});
697                         Log.Error(message, exc);
698                 }
699
700
701                 public void NotifyChange(string status, TraceLevel level=TraceLevel.Info)
702                 {
703                         StatusMessage = status;
704                         
705                         _events.Publish(new Notification { Title = "Pithos", Message = status, Level = level });
706                 }
707
708                 public void NotifyChangedFile(string filePath)
709                 {
710             if (RecentFiles.Any(e => e.FullPath == filePath))
711                 return;
712             
713                         IProducerConsumerCollection<FileEntry> files=RecentFiles;
714                         FileEntry popped;
715                         while (files.Count > 5)
716                                 files.TryTake(out popped);
717             var entry = new FileEntry { FullPath = filePath };
718                         files.TryAdd(entry);
719                 }
720
721                 public void NotifyAccount(AccountInfo account)
722                 {
723                         if (account== null)
724                                 return;
725                         //TODO: What happens to an existing account whose Token has changed?
726                         account.SiteUri= String.Format("{0}/ui/?token={1}&user={2}",
727                                 account.SiteUri, Uri.EscapeDataString(account.Token),
728                                 Uri.EscapeDataString(account.UserName));
729
730                         if (Accounts.All(item => item.UserName != account.UserName))
731                                 Accounts.TryAdd(account);
732
733                 }
734
735                 public void NotifyConflicts(IEnumerable<FileSystemInfo> conflictFiles, string message)
736                 {
737                         if (conflictFiles == null)
738                                 return;
739                     //Convert to list to avoid multiple iterations
740             var files = conflictFiles.ToList();
741                         if (files.Count==0)
742                                 return;
743
744                         UpdateStatus();
745                         //TODO: Create a more specific message. For now, just show a warning
746                         NotifyForFiles(files,message,TraceLevel.Warning);
747
748                 }
749
750                 public void NotifyForFiles(IEnumerable<FileSystemInfo> files, string message,TraceLevel level=TraceLevel.Info)
751                 {
752                         if (files == null)
753                                 return;
754                         if (!files.Any())
755                                 return;
756
757                         StatusMessage = message;
758
759                         _events.Publish(new Notification { Title = "Pithos", Message = message, Level = level});
760                 }
761
762                 public void Notify(Notification notification)
763                 {
764                         _events.Publish(notification);
765                 }
766
767
768                 public void RemoveMonitor(string accountName)
769                 {
770                         if (String.IsNullOrWhiteSpace(accountName))
771                                 return;
772
773                         var accountInfo=_accounts.FirstOrDefault(account => account.UserName == accountName);
774             if (accountInfo != null)
775             {
776                 _accounts.TryRemove(accountInfo);
777                 _pollAgent.RemoveAccount(accountInfo);
778             }
779
780                     PithosMonitor monitor;
781                         if (Monitors.TryRemove(accountName, out monitor))
782                         {
783                                 monitor.Stop();
784                 //TODO: Also remove any pending actions for this account
785                 //from the network queue                
786                         }
787                 }
788
789                 public void RefreshOverlays()
790                 {
791                         foreach (var pair in Monitors)
792                         {
793                                 var monitor = pair.Value;
794
795                                 var path = monitor.RootPath;
796
797                                 if (String.IsNullOrWhiteSpace(path))
798                                         continue;
799
800                                 if (!Directory.Exists(path) && !File.Exists(path))
801                                         continue;
802
803                                 IntPtr pathPointer = Marshal.StringToCoTaskMemAuto(path);
804
805                                 try
806                                 {
807                                         NativeMethods.SHChangeNotify(HChangeNotifyEventID.SHCNE_UPDATEITEM,
808                                                                                                  HChangeNotifyFlags.SHCNF_PATHW | HChangeNotifyFlags.SHCNF_FLUSHNOWAIT,
809                                                                                                  pathPointer, IntPtr.Zero);
810                                 }
811                                 finally
812                                 {
813                                         Marshal.FreeHGlobal(pathPointer);
814                                 }
815                         }
816                 }
817
818                 #region Event Handlers
819                 
820                 public void Handle(SelectiveSynchChanges message)
821                 {
822                         var accountName = message.Account.AccountName;
823                         PithosMonitor monitor;
824                         if (_monitors.TryGetValue(accountName, out monitor))
825                         {
826                                 monitor.SetSelectivePaths(message.Uris,message.Added,message.Removed);
827
828                         }
829                         
830                 }
831
832
833                 private bool _pollStarted;
834
835                 //SMELL: Doing so much work for notifications in the shell is wrong
836                 //The notifications should be moved to their own view/viewmodel pair
837                 //and different templates should be used for different message types
838                 //This will also allow the addition of extra functionality, eg. actions
839                 //
840                 public void Handle(Notification notification)
841                 {
842                         UpdateStatus();
843
844                         if (!Settings.ShowDesktopNotifications)
845                                 return;
846
847                         if (notification is PollNotification)
848                         {
849                                 _pollStarted = true;
850                                 return;
851                         }
852                         if (notification is CloudNotification)
853                         {
854                                 if (!_pollStarted) 
855                                         return;
856                                 _pollStarted= false;
857                                 notification.Title = "Pithos";
858                                 notification.Message = "Start Synchronisation";
859                         }
860
861                     var progress = notification as ProgressNotification;
862                     if (progress != null)
863                     {
864                         StatusMessage = String.Format("Pithos {0}\r\n{1} {2:p2} of {3} - {4}",
865                                                       _fileVersion.Value.FileVersion, 
866                                               progress.Action,
867                                                       progress.Block/(double)progress.TotalBlocks,
868                                                       progress.FileSize.ToByteSize(),
869                                                       progress.FileName);
870                         return;
871                     }
872
873                     var info = notification as StatusNotification;
874             if (info != null)
875             {
876                 StatusMessage = String.Format("Pithos {0}\r\n{1}",
877                                               _fileVersion.Value.FileVersion,
878                                               info.Title);
879                 return;
880             }
881                         if (String.IsNullOrWhiteSpace(notification.Message) && String.IsNullOrWhiteSpace(notification.Title))
882                                 return;
883
884                         ShowBalloonFor(notification);
885                 }
886
887             private void ShowBalloonFor(Notification notification)
888             {
889             Contract.Requires(notification!=null);
890             
891             if (!Settings.ShowDesktopNotifications) 
892                 return;
893             
894             BalloonIcon icon;
895                 switch (notification.Level)
896                 {
897                     case TraceLevel.Info:
898                     case TraceLevel.Verbose:
899                         return;
900                 case TraceLevel.Error:
901                     icon = BalloonIcon.Error;
902                     break;
903                 case TraceLevel.Warning:
904                         icon = BalloonIcon.Warning;
905                         break;
906                     default:
907                         return;
908                 }
909
910                 var tv = (ShellView) GetView();
911                 System.Action clickAction = null;
912                 if (notification is ExpirationNotification)
913                 {
914                     clickAction = () => ShowPreferences("AccountTab");
915                 }
916                 var balloon = new PithosBalloon
917                                   {
918                                       Title = notification.Title,
919                                       Message = notification.Message,
920                                       Icon = icon,
921                                       ClickAction = clickAction
922                                   };
923                 tv.TaskbarView.ShowCustomBalloon(balloon, PopupAnimation.Fade, 4000);
924             }
925
926             #endregion
927
928                 public void Handle(ShowFilePropertiesEvent message)
929                 {
930                         if (message == null)
931                                 throw new ArgumentNullException("message");
932                         if (String.IsNullOrWhiteSpace(message.FileName) )
933                                 throw new ArgumentException("message");
934                         Contract.EndContractBlock();
935
936                         var fileName = message.FileName;
937                         //TODO: Display file properties for non-container folders
938                         if (File.Exists(fileName))
939                                 //Retrieve the full name with exact casing. Pithos names are case sensitive                             
940                                 ShowFileProperties(FileInfoExtensions.GetProperFilePathCapitalization(fileName));
941                         else if (Directory.Exists(fileName))
942                                 //Retrieve the full name with exact casing. Pithos names are case sensitive
943                         {
944                                 var path = FileInfoExtensions.GetProperDirectoryCapitalization(fileName);
945                                 if (IsContainer(path))
946                                         ShowContainerProperties(path);
947                                 else
948                                         ShowFileProperties(path);
949                         }
950                 }
951
952                 private bool IsContainer(string path)
953                 {
954                         var matchingFolders = from account in _accounts
955                                                                   from rootFolder in Directory.GetDirectories(account.AccountPath)
956                                                                   where rootFolder.Equals(path, StringComparison.InvariantCultureIgnoreCase)
957                                                                   select rootFolder;
958                         return matchingFolders.Any();
959                 }
960
961                 public FileStatus GetFileStatus(string localFileName)
962                 {
963                         if (String.IsNullOrWhiteSpace(localFileName))
964                                 throw new ArgumentNullException("localFileName");
965                         Contract.EndContractBlock();
966                         
967                         var statusKeeper = IoC.Get<IStatusKeeper>();
968                         var status=statusKeeper.GetFileStatus(localFileName);
969                         return status;
970                 }
971         }
972 }