Replaced Merkle hash with MD5 for change checking
[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;
51 using System.Threading.Tasks;
52 using System.Windows;
53 using System.Windows.Controls.Primitives;
54 using System.Windows.Input;
55 using AppLimit.NetSparkle;
56 using Caliburn.Micro;
57 using Hardcodet.Wpf.TaskbarNotification;
58 using Pithos.Client.WPF.Configuration;
59 using Pithos.Client.WPF.FileProperties;
60 using Pithos.Client.WPF.Preferences;
61 using Pithos.Client.WPF.SelectiveSynch;
62 using Pithos.Client.WPF.Services;
63 using Pithos.Client.WPF.Shell;
64 using Pithos.Core;
65 using Pithos.Core.Agents;
66 using Pithos.Interfaces;
67 using System;
68 using System.Collections.Generic;
69 using System.Linq;
70 using Pithos.Network;
71 using StatusService = Pithos.Client.WPF.Services.StatusService;
72
73 namespace Pithos.Client.WPF {
74         using System.ComponentModel.Composition;
75
76         public class ToggleStatusCommand:ICommand
77         {
78             private readonly ShellViewModel _model;
79             public ToggleStatusCommand(ShellViewModel model)
80             {
81                 _model = model;
82             }
83             public void Execute(object parameter)
84             {
85                 _model.CurrentSyncStatus();
86             }
87
88             public bool CanExecute(object parameter)
89             {
90                 return true;
91             }
92
93             public event EventHandler CanExecuteChanged;
94         }
95
96
97         ///<summary>
98         /// The "shell" of the Pithos application displays the taskbar  icon, menu and notifications.
99         /// The shell also hosts the status service called by shell extensions to retrieve file info
100         ///</summary>
101         ///<remarks>
102         /// It is a strange "shell" as its main visible element is an icon instead of a window
103         /// The shell subscribes to the following events:
104         /// * Notification:  Raised by components that want to notify the user. Usually displayed in a balloon
105         /// * 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
106         /// * ShowFilePropertiesEvent: Raised when a shell command requests the display of the file/container properties dialog
107         ///</remarks>           
108         //TODO: CODE SMELL Why does the shell handle the SelectiveSynchChanges?
109     [Export(typeof(IShell)), Export(typeof(ShellViewModel)),Export(typeof(IStatusNotification))]
110         public class ShellViewModel : Screen, IStatusNotification, IShell,
111                 IHandle<Notification>, IHandle<SelectiveSynchChanges>, IHandle<ShowFilePropertiesEvent>
112         {
113                 
114                 private readonly IEventAggregator _events;
115
116                 public PithosSettings Settings { get; private set; }
117
118
119                 private readonly ConcurrentDictionary<Uri, PithosMonitor> _monitors = new ConcurrentDictionary<Uri, PithosMonitor>();
120                 ///<summary>
121                 /// Dictionary of account monitors, keyed by account
122                 ///</summary>
123                 ///<remarks>
124                 /// One monitor class is created for each account. The Shell needs access to the monitors to execute start/stop/pause commands,
125                 /// retrieve account and boject info            
126                 ///</remarks>
127                 // TODO: Does the Shell REALLY need access to the monitors? Could we achieve the same results with a better design?
128                 // TODO: The monitors should be internal to Pithos.Core, even though exposing them makes coding of the Object and Container windows easier
129                 public ConcurrentDictionary<Uri, PithosMonitor> Monitors
130                 {
131                         get { return _monitors; }
132                 }
133
134
135                 ///<summary>
136                 /// The status service is used by Shell extensions to retrieve file status information
137                 ///</summary>
138                 //TODO: CODE SMELL! This is the shell! While hosting in the shell makes executing start/stop commands easier, it is still a smell
139                 private ServiceHost _statusService;
140
141                 //Logging in the Pithos client is provided by log4net
142         private static readonly log4net.ILog Log = log4net.LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
143
144         [Import]
145             private PollAgent _pollAgent;
146
147         [Import]
148             private NetworkAgent _networkAgent;
149
150             [Import]
151             public Selectives Selectives { get; set; }
152
153
154             public ToggleStatusCommand ToggleMiniStatusCommand { get; set; }
155
156             private MiniStatusViewModel _miniStatus;
157
158             [Import]
159         public MiniStatusViewModel MiniStatus
160             {
161                 get { return _miniStatus; }
162                 set
163                 {
164                     _miniStatus = value;
165                     _miniStatus.Shell = this;
166                     _miniStatus.Deactivated += (sender, arg) =>
167                                                    {
168                                                        _statusVisible = false;
169                                                    NotifyOfPropertyChange(()=>MiniStatusCaption);
170                                                    };
171                 }
172             }
173
174             ///<summary>
175                 /// The Shell depends on MEF to provide implementations for windowManager, events, the status checker service and the settings
176                 ///</summary>
177                 ///<remarks>
178                 /// The PithosSettings class encapsulates the app's settings to abstract their storage mechanism (App settings, a database or registry)
179                 ///</remarks>
180                 [ImportingConstructor]          
181                 public ShellViewModel(IWindowManager windowManager, IEventAggregator events, PithosSettings settings/*,PollAgent pollAgent,NetworkAgent networkAgent*/)
182                 {
183                         try
184                         {
185
186                                 _windowManager = windowManager;
187                                 //CHECK: Caliburn doesn't need explicit command construction
188                                 //CurrentSyncStatusCommand = new PithosCommand(OpenPithosFolder);
189                                 //The event subst
190                                 _events = events;
191                                 _events.Subscribe(this);
192
193 /*
194                             _pollAgent = pollAgent;
195                             _networkAgent = networkAgent;
196 */
197                                 Settings = settings;
198
199                                 Proxy.SetFromSettings(settings);
200
201                 StatusMessage = Settings.Accounts.Count==0 
202                     ? "No Accounts added\r\nPlease add an account" 
203                     : "Starting";
204
205                                 _accounts.CollectionChanged += (sender, e) =>
206                                                                                                    {
207                                                                                                            NotifyOfPropertyChange(() => OpenFolderCaption);
208                                                                                                            NotifyOfPropertyChange(() => HasAccounts);
209                                                                                                    };
210
211                 SetVersionMessage();
212
213                 ToggleMiniStatusCommand=new ToggleStatusCommand(this);
214                         }
215                         catch (Exception exc)
216                         {
217                                 Log.Error("Error while starting the ShellViewModel",exc);
218                                 throw;
219                         }
220
221                 }
222
223             private void SetVersionMessage()
224             {
225                 Assembly assembly = Assembly.GetExecutingAssembly();
226                 var fileVersion = FileVersionInfo.GetVersionInfo(assembly.Location);
227                 VersionMessage = String.Format("Pithos+ {0}", fileVersion.FileVersion);
228             }
229
230         public void CurrentSyncStatus()
231         {
232             if (Accounts.Count == 0)
233             {
234                 ShowPreferences("AccountTab");
235             }
236             else
237             {
238                 if (!_statusVisible)
239                 {
240                     _windowManager.ShowWindow(MiniStatus);
241                     _statusVisible = true;
242                 }
243                 else
244                 {
245                     if (MiniStatus.IsActive)
246                         MiniStatus.TryClose();
247                     _statusVisible = false;
248                 }
249
250                 NotifyOfPropertyChange(() => MiniStatusCaption);
251             }
252         }
253
254             protected override void OnActivate()
255                 {
256                         base.OnActivate();
257
258             InitializeSparkle();
259
260                 //Must delay opening the upgrade window
261             //to avoid Windows Messages sent by the TaskbarIcon
262             TaskEx.Delay(5000).ContinueWith(_=>
263                 Execute.OnUIThread(()=> _sparkle.StartLoop(true,Settings.UpdateForceCheck,Settings.UpdateCheckInterval)));
264
265
266                         StartMonitoring();                    
267                 }
268
269
270             private void OnCheckFinished(object sender, bool updaterequired)
271             {
272             
273             Log.InfoFormat("Upgrade check finished. Need Upgrade: {0}", updaterequired);
274             if (_manualUpgradeCheck)
275             {
276                 _manualUpgradeCheck = false;
277                 if (!updaterequired)
278                     //Sparkle raises events on a background thread
279                     Execute.OnUIThread(()=>
280                         ShowBalloonFor(new Notification{Title="Pithos+ is up to date",Message="You have the latest Pithos+ version. No update is required"}));
281             }
282             }
283
284             private void OnUpgradeDetected(object sender, UpdateDetectedEventArgs e)
285             {            
286                 Log.InfoFormat("Update detected {0}",e.LatestVersion);
287             }
288
289         public void CheckForUpgrade()
290         {
291             ShowBalloonFor(new Notification{Title="Checking for upgrades",Message="Contacting the server to retrieve the latest Pithos+ version."});
292             _sparkle.StopLoop();
293             _sparkle.updateDetected -= OnUpgradeDetected;
294             _sparkle.checkLoopFinished -= OnCheckFinished;
295             _sparkle.Dispose();
296
297             _manualUpgradeCheck = true;
298             InitializeSparkle();
299             _sparkle.StartLoop(true,true,Settings.UpdateCheckInterval);
300         }
301
302         private void InitializeSparkle()
303         {
304             _sparkle = new Sparkle(Settings.UpdateUrl);
305             _sparkle.updateDetected += OnUpgradeDetected;
306             _sparkle.checkLoopFinished += OnCheckFinished;
307             _sparkle.ShowDiagnosticWindow = Settings.UpdateDiagnostics;
308         }
309
310             private async void StartMonitoring()
311                 {
312                         try
313                         {
314                 if (Settings.IgnoreCertificateErrors)
315                 {
316                     ServicePointManager.ServerCertificateValidationCallback = (sender, certificate, chain, errors) => true;
317                 }
318                     
319                                 var accounts = Settings.Accounts.Select(MonitorAccount);
320                 await TaskEx.WhenAll(accounts).ConfigureAwait(false);
321                                 _statusService = StatusService.Start();
322
323                         }
324                         catch (AggregateException exc)
325                         {
326                                 exc.Handle(e =>
327                                 {
328                                         Log.Error("Error while starting monitoring", e);
329                                         return true;
330                                 });
331                                 throw;
332                         }
333                 }
334
335                 protected override void OnDeactivate(bool close)
336                 {
337                         base.OnDeactivate(close);
338                         if (close)
339                         {
340                                 StatusService.Stop(_statusService);
341                                 _statusService = null;
342                         }
343                 }
344
345                 public Task MonitorAccount(AccountSettings account)
346                 {
347                         return Task.Factory.StartNew(() =>
348                         {                                                
349                                 PithosMonitor monitor;
350                                 var accountName = account.AccountName;
351
352                             MigrateFolders(account);
353
354                             Selectives.SetIsSelectiveEnabled(account.AccountKey, account.SelectiveSyncEnabled);
355
356                                 if (Monitors.TryGetValue(account.AccountKey, out monitor))
357                                 {
358                                         //If the account is active
359                     if (account.IsActive)
360                     {
361                         //The Api Key may have changed throuth the Preferences dialog
362                         monitor.ApiKey = account.ApiKey;
363                                                 Debug.Assert(monitor.StatusNotification == this,"An existing monitor should already have a StatusNotification service object");
364                         monitor.RootPath = account.RootPath;
365                         //Start the monitor. It's OK to start an already started monitor,
366                         //it will just ignore the call                        
367                         StartMonitor(monitor).Wait();
368                     }
369                     else
370                     {
371                         //If the account is inactive
372                         //Stop and remove the monitor
373                         RemoveMonitor(account.ServerUrl,accountName);
374                     }
375                                         return;
376                                 }
377
378                                 
379                                 //Create a new monitor/ Can't use MEF here, it would return a single instance for all monitors
380                                 monitor = new PithosMonitor
381                                                           {
382                                                                   UserName = accountName,
383                                                                   ApiKey = account.ApiKey,                                  
384                                                                   StatusNotification = this,
385                                                                   RootPath = account.RootPath
386                                                           };
387                                 //PithosMonitor uses MEF so we need to resolve it
388                                 IoC.BuildUp(monitor);
389
390                                 monitor.AuthenticationUrl = account.ServerUrl;
391
392                                 Monitors[account.AccountKey] = monitor;
393
394                                 if (account.IsActive)
395                                 {
396                                         //Don't start a monitor if it doesn't have an account and ApiKey
397                                         if (String.IsNullOrWhiteSpace(monitor.UserName) ||
398                                                 String.IsNullOrWhiteSpace(monitor.ApiKey))
399                                                 return;
400                                         StartMonitor(monitor);
401                                 }
402                         });
403                 }
404
405             private void MigrateFolders(AccountSettings account)
406             {
407                 var oldOthersFolder=Path.Combine(account.RootPath, FolderConstants.OldOthersFolder);
408                 var newOthersFolder = Path.Combine(account.RootPath, FolderConstants.OthersFolder);
409                 var oldFolder = new DirectoryInfo(oldOthersFolder);
410                 var newFolder = new DirectoryInfo(newOthersFolder);
411
412             if (oldFolder.Exists && !newFolder.Exists)
413             {
414                 oldFolder.MoveTo(newOthersFolder);
415             }
416             }
417
418
419             protected override void OnViewLoaded(object view)
420                 {
421                         UpdateStatus();
422                         var window = (Window)view;            
423                         TaskEx.Delay(1000).ContinueWith(t => Execute.OnUIThread(window.Hide));
424                         base.OnViewLoaded(view);
425                 }
426
427
428                 #region Status Properties
429
430                 private string _statusMessage;
431                 public string StatusMessage
432                 {
433                         get { return _statusMessage; }
434                         set
435                         {
436                                 _statusMessage = value;
437                                 NotifyOfPropertyChange(() => StatusMessage);
438                 NotifyOfPropertyChange(() => TooltipMessage);
439                         }
440                 }
441
442         public string VersionMessage { get; set; }
443
444             public string TooltipMessage
445             {
446                 get
447                 {
448                     return String.Format("{0}\r\n{1}",VersionMessage,StatusMessage);
449                 }
450             }
451
452         public string TooltipMiniStatus
453         {
454             get
455             {
456                 return String.Format("{0}\r\n{1}", "Status Window", "Enable / Disable the status window");
457             }
458         }
459
460         /*public string ToggleStatusWindowMessage
461         {
462             get
463             {
464                 return String.Format("{0}" + Environment.NewLine + "{1} Toggle Mini Status");
465             }
466         }*/
467
468             private readonly ObservableConcurrentCollection<AccountInfo> _accounts = new ObservableConcurrentCollection<AccountInfo>();
469                 public ObservableConcurrentCollection<AccountInfo> Accounts
470                 {
471                         get { return _accounts; }
472                 }
473
474                 public bool HasAccounts
475                 {
476                         get { return _accounts.Count > 0; }
477                 }
478
479
480                 public string OpenFolderCaption
481                 {
482                         get
483                         {
484                                 return (_accounts.Count == 0)
485                                                 ? "No Accounts Defined"
486                                                 : "Open Pithos Folder";
487                         }
488                 }
489
490                 private string _pauseSyncCaption="Pause Synching";
491                 public string PauseSyncCaption
492                 {
493                         get { return _pauseSyncCaption; }
494                         set
495                         {
496                                 _pauseSyncCaption = value;
497                                 NotifyOfPropertyChange(() => PauseSyncCaption);
498                         }
499                 }
500
501                 private readonly ObservableConcurrentCollection<FileEntry> _recentFiles = new ObservableConcurrentCollection<FileEntry>();
502                 public ObservableConcurrentCollection<FileEntry> RecentFiles
503                 {
504                         get { return _recentFiles; }
505                 }
506
507
508                 private string _statusIcon="../Images/Pithos.ico";
509                 public string StatusIcon
510                 {
511                         get { return _statusIcon; }
512                         set
513                         {
514                                 //TODO: Ensure all status icons use the Pithos logo
515                                 _statusIcon = value;
516                                 NotifyOfPropertyChange(() => StatusIcon);
517                         }
518                 }
519
520                 #endregion
521
522                 #region Commands
523
524         public void CancelCurrentOperation()
525         {
526             _networkAgent.CancelCurrentOperation();
527         }
528
529         public void ShowPreferences()
530         {
531             ShowPreferences(null);
532         }
533
534                 public void ShowPreferences(string currentTab)
535                 {
536                         //Settings.Reload();
537             
538                     var preferences = new PreferencesViewModel(_windowManager, _events, this, Settings,currentTab);
539                     _windowManager.ShowDialog(preferences);
540                         
541                 }
542
543                 public void AboutPithos()
544                 {
545                         var about = IoC.Get<AboutViewModel>();
546                     about.LatestVersion=_sparkle.LatestVersion;
547                         _windowManager.ShowWindow(about);
548                 }
549
550                 public void SendFeedback()
551                 {
552                         var feedBack =  IoC.Get<FeedbackViewModel>();
553                         _windowManager.ShowWindow(feedBack);
554                 }
555
556                 //public PithosCommand OpenPithosFolderCommand { get; private set; }
557
558                 public void OpenPithosFolder()
559                 {
560                         var account = Settings.Accounts.FirstOrDefault(acc => acc.IsActive);
561                         if (account == null)
562                                 return;
563                         Process.Start(account.RootPath);
564                 }
565
566                 public void OpenPithosFolder(AccountInfo account)
567                 {
568                         Process.Start(account.AccountPath);
569                 }
570
571                 
572
573                 public void GoToSite()
574                 {            
575                         var site = Properties.Settings.Default.ProductionServer;
576                         Process.Start(site);            
577                 }
578
579
580                 public void GoToSite(AccountInfo account)
581                 {
582                     var uri = account.SiteUri.Replace("http://","https://");            
583                     Process.Start(uri);
584                 }
585
586             private bool _statusVisible;
587
588             public string MiniStatusCaption
589             {
590                 get
591                 {
592                     return  _statusVisible ? "Hide Status Window" : "Show Status Window";
593                 }
594             }
595
596             public bool HasConflicts
597             {
598             get { return true; }
599             }
600         public void ShowConflicts()
601         {
602             _windowManager.ShowWindow(IoC.Get<ConflictsViewModel>());            
603         }
604
605             /// <summary>
606         /// Open an explorer window to the target path's directory
607         /// and select the file
608         /// </summary>
609         /// <param name="entry"></param>
610         public void GoToFile(FileEntry entry)
611         {
612             var fullPath = entry.FullPath;
613             if (!File.Exists(fullPath) && !Directory.Exists(fullPath))
614                 return;
615             Process.Start("explorer.exe","/select, " + fullPath);
616         }
617
618         public void OpenLogPath()
619         {
620             var pithosDataPath = PithosSettings.PithosDataPath;
621
622             Process.Start(pithosDataPath);
623         }
624         
625         public void ShowFileProperties()
626                 {
627                         var account = Settings.Accounts.First(acc => acc.IsActive);            
628                         var dir = new DirectoryInfo(account.RootPath + @"\pithos");
629                         var files=dir.GetFiles();
630                         var r=new Random();
631                         var idx=r.Next(0, files.Length);
632                         ShowFileProperties(files[idx].FullName);            
633                 }
634
635                 public void ShowFileProperties(string filePath)
636                 {
637                         if (String.IsNullOrWhiteSpace(filePath))
638                                 throw new ArgumentNullException("filePath");
639                         if (!File.Exists(filePath) && !Directory.Exists(filePath))
640                                 throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath");
641                         Contract.EndContractBlock();
642
643                         var pair=(from monitor in  Monitors
644                                                            where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)
645                                                                    select monitor).FirstOrDefault();
646                         var accountMonitor = pair.Value;
647
648                         if (accountMonitor == null)
649                                 return;
650
651                         var infoTask=Task.Factory.StartNew(()=>accountMonitor.GetObjectInfo(filePath));
652
653                         
654
655                         var fileProperties = new FilePropertiesViewModel(this, infoTask,filePath);
656                         _windowManager.ShowWindow(fileProperties);
657                 } 
658                 
659                 public void ShowContainerProperties()
660                 {
661                         var account = Settings.Accounts.First(acc => acc.IsActive);            
662                         var dir = new DirectoryInfo(account.RootPath);
663                         var fullName = (from folder in dir.EnumerateDirectories()
664                                                         where (folder.Attributes & FileAttributes.Hidden) == 0
665                                                         select folder.FullName).First();
666                         ShowContainerProperties(fullName);            
667                 }
668
669                 public void ShowContainerProperties(string filePath)
670                 {
671                         if (String.IsNullOrWhiteSpace(filePath))
672                                 throw new ArgumentNullException("filePath");
673                         if (!Directory.Exists(filePath))
674                                 throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath");
675                         Contract.EndContractBlock();
676
677                         var pair=(from monitor in  Monitors
678                                                            where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)
679                                                                    select monitor).FirstOrDefault();
680                         var accountMonitor = pair.Value;            
681                         var info = accountMonitor.GetContainerInfo(filePath);
682
683                         
684
685                         var containerProperties = new ContainerPropertiesViewModel(this, info,filePath);
686                         _windowManager.ShowWindow(containerProperties);
687                 }
688
689                 public void SynchNow()
690                 {
691                         _pollAgent.SynchNow();
692                 }
693
694                 public ObjectInfo RefreshObjectInfo(ObjectInfo currentInfo)
695                 {
696                         if (currentInfo==null)
697                                 throw new ArgumentNullException("currentInfo");
698                         Contract.EndContractBlock();                
699             var monitor = Monitors[currentInfo.AccountKey];
700                         var newInfo=monitor.CloudClient.GetObjectInfo(currentInfo.Account, currentInfo.Container, currentInfo.Name);
701                         return newInfo;
702                 }
703
704                 public ContainerInfo RefreshContainerInfo(ContainerInfo container)
705                 {
706                         if (container == null)
707                                 throw new ArgumentNullException("container");
708                         Contract.EndContractBlock();
709
710                         var monitor = Monitors[container.AccountKey];
711                         var newInfo = monitor.CloudClient.GetContainerInfo(container.Account, container.Name);
712                         return newInfo;
713                 }
714
715             private bool _isPaused;
716             public bool IsPaused
717             {
718                 get { return _isPaused; }
719                 set
720                 {
721                     _isPaused = value;
722                 PauseSyncCaption = IsPaused ? "Resume syncing" : "Pause syncing";
723                 var iconKey = IsPaused ? "TraySyncPaused" : "TrayInSynch";
724                 StatusIcon = String.Format(@"../Images/{0}.ico", iconKey);
725
726                 NotifyOfPropertyChange(() => IsPaused);
727                 }
728             }
729
730             public void ToggleSynching()
731                 {
732                         IsPaused=!IsPaused;
733                         foreach (var monitor in Monitors.Values)
734                         {
735                             monitor.Pause = IsPaused ;
736                         }
737             _pollAgent.Pause = IsPaused;
738             _networkAgent.Pause = IsPaused;
739
740
741                 }
742
743         public void ExitPithos()
744         {
745             try
746             {
747
748                 foreach (var monitor in Monitors.Select(pair => pair.Value))
749                 {
750                     monitor.Stop();
751                 }
752
753                 var view = GetView() as Window;
754                 if (view != null)
755                     view.Close();
756             }
757             catch (Exception exc)
758             {
759                 Log.Info("Exception while exiting", exc);                
760             }
761             finally
762             {
763                 Application.Current.Shutdown();
764             }
765         }
766
767             #endregion
768
769
770                 private readonly Dictionary<PithosStatus, StatusInfo> _iconNames = new List<StatusInfo>
771                         {
772                                 new StatusInfo(PithosStatus.InSynch, "All files up to date", "TrayInSynch"),
773                                 new StatusInfo(PithosStatus.PollSyncing, "Polling Files", "TraySynching"),
774                 new StatusInfo(PithosStatus.LocalSyncing, "Syncing Files", "TraySynching"),
775                                 new StatusInfo(PithosStatus.SyncPaused, "Sync Paused", "TraySyncPaused")
776                         }.ToDictionary(s => s.Status);
777
778                 readonly IWindowManager _windowManager;
779                 
780         //private int _syncCount=0;
781
782
783         private PithosStatus _pithosStatus = PithosStatus.Disconnected;
784
785         public void SetPithosStatus(PithosStatus status)
786         {
787             if (_pithosStatus == PithosStatus.LocalSyncing && status == PithosStatus.PollComplete)
788                 return;
789             if (_pithosStatus == PithosStatus.PollSyncing && status == PithosStatus.LocalComplete)
790                 return;
791             if (status == PithosStatus.LocalComplete || status == PithosStatus.PollComplete)
792                 _pithosStatus = PithosStatus.InSynch;
793             else
794                 _pithosStatus = status;
795             UpdateStatus();
796         }
797
798         public void SetPithosStatus(PithosStatus status,string message)
799         {
800             StatusMessage = message;
801             SetPithosStatus(status);
802         }
803
804           /*  public Notifier GetNotifier(Notification startNotification, Notification endNotification)
805             {
806                 return new Notifier(this, startNotification, endNotification);
807             }*/
808
809             public Notifier GetNotifier(string startMessage, string endMessage, params object[] args)
810             {
811                 return new Notifier(this, 
812                 new StatusNotification(String.Format(startMessage,args)), 
813                 new StatusNotification(String.Format(endMessage,args)));
814             }
815
816
817             ///<summary>
818                 /// Updates the visual status indicators of the application depending on status changes, e.g. icon, stat                
819                 ///</summary>
820                 public void UpdateStatus()
821                 {
822
823                         if (_iconNames.ContainsKey(_pithosStatus))
824                         {
825                                 var info = _iconNames[_pithosStatus];
826                                 StatusIcon = String.Format(@"../Images/{0}.ico", info.IconName);
827                         }
828
829             if (_pithosStatus == PithosStatus.InSynch)
830                 StatusMessage = "All files up to date";
831                 }
832
833
834            
835                 private Task StartMonitor(PithosMonitor monitor,int retries=0)
836                 {
837                         return Task.Factory.StartNew(() =>
838                         {
839                                 using (log4net.ThreadContext.Stacks["Monitor"].Push("Start"))
840                                 {
841                                         try
842                                         {
843                                                 Log.InfoFormat("Start Monitoring {0}", monitor.UserName);
844
845                                                 monitor.Start();
846                                         }
847                                         catch (WebException exc)
848                                         {
849                                                 if (AbandonRetry(monitor, retries))
850                                                         return;
851
852                                                 HttpStatusCode statusCode =HttpStatusCode.OK;
853                                                 var response = exc.Response as HttpWebResponse;
854                                                 if(response!=null)
855                                                         statusCode = response.StatusCode;
856
857                                                 switch (statusCode)
858                                                 {
859                                                         case HttpStatusCode.Unauthorized:
860                                                                 var message = String.Format("API Key Expired for {0}. Starting Renewal",
861                                                                                                                         monitor.UserName);
862                                                                 Log.Error(message, exc);
863                                 var account = Settings.Accounts.Find(acc => acc.AccountKey == new Uri(new Uri(monitor.AuthenticationUrl), monitor.UserName));                                
864                                                         account.IsExpired = true;
865                                 Notify(new ExpirationNotification(account));
866                                                                 //TryAuthorize(monitor.UserName, retries).Wait();
867                                                                 break;
868                                                         case HttpStatusCode.ProxyAuthenticationRequired:
869                                                                 TryAuthenticateProxy(monitor,retries);
870                                                                 break;
871                                                         default:
872                                                                 TryLater(monitor, exc, retries);
873                                                                 break;
874                                                 }
875                                         }
876                                         catch (Exception exc)
877                                         {
878                                                 if (AbandonRetry(monitor, retries)) 
879                                                         return;
880
881                                                 TryLater(monitor,exc,retries);
882                                         }
883                                 }
884                         });
885                 }
886
887                 private void TryAuthenticateProxy(PithosMonitor monitor,int retries)
888                 {
889                         Execute.OnUIThread(() =>
890                                                                    {                                       
891                                                                            var proxyAccount = IoC.Get<ProxyAccountViewModel>();
892                                                                                 proxyAccount.Settings = Settings;
893                                                                            if (true != _windowManager.ShowDialog(proxyAccount)) 
894                                                                                    return;
895                                                                            StartMonitor(monitor, retries);
896                                                                            NotifyOfPropertyChange(() => Accounts);
897                                                                    });
898                 }
899
900                 private bool AbandonRetry(PithosMonitor monitor, int retries)
901                 {
902                         if (retries > 1)
903                         {
904                                 var message = String.Format("Monitoring of account {0} has failed too many times. Will not retry",
905                                                                                         monitor.UserName);
906                                 _events.Publish(new Notification
907                                                                         {Title = "Account monitoring failed", Message = message, Level = TraceLevel.Error});
908                                 return true;
909                         }
910                         return false;
911                 }
912
913
914             private void TryLater(PithosMonitor monitor, Exception exc,int retries)
915                 {
916                         var message = String.Format("An exception occured. Can't start monitoring\nWill retry in 10 seconds");
917                         Task.Factory.StartNewDelayed(10000, () => StartMonitor(monitor,retries+1));
918                         _events.Publish(new Notification
919                                                                 {Title = "Error", Message = message, Level = TraceLevel.Error});
920                         Log.Error(message, exc);
921                 }
922
923
924                 public void NotifyChange(string status, TraceLevel level=TraceLevel.Info)
925                 {
926                         StatusMessage = status;
927                         
928                         _events.Publish(new Notification { Title = "Pithos+", Message = status, Level = level });
929                 }
930
931                 public void NotifyChangedFile(string filePath)
932                 {
933             if (RecentFiles.Any(e => e.FullPath == filePath))
934                 return;
935             
936                         IProducerConsumerCollection<FileEntry> files=RecentFiles;
937                         FileEntry popped;
938                         while (files.Count > 5)
939                                 files.TryTake(out popped);
940             var entry = new FileEntry { FullPath = filePath };
941                         files.TryAdd(entry);
942                 }
943
944                 public void NotifyAccount(AccountInfo account)
945                 {
946                         if (account== null)
947                                 return;
948                         //TODO: What happens to an existing account whose Token has changed?
949                         account.SiteUri= String.Format("{0}/ui/?token={1}&user={2}",
950                                 account.SiteUri, Uri.EscapeDataString(account.Token),
951                                 Uri.EscapeDataString(account.UserName));
952
953                         if (!Accounts.Any(item => item.UserName == account.UserName && item.SiteUri == account.SiteUri))
954                                 Accounts.TryAdd(account);
955
956                 }
957
958                 public void NotifyConflicts(IEnumerable<FileSystemInfo> conflictFiles, string message)
959                 {
960                         if (conflictFiles == null)
961                                 return;
962                     //Convert to list to avoid multiple iterations
963             var files = conflictFiles.ToList();
964                         if (files.Count==0)
965                                 return;
966
967                         UpdateStatus();
968                         //TODO: Create a more specific message. For now, just show a warning
969                         NotifyForFiles(files,message,TraceLevel.Warning);
970
971                 }
972
973                 public void NotifyForFiles(IEnumerable<FileSystemInfo> files, string message,TraceLevel level=TraceLevel.Info)
974                 {
975                         if (files == null)
976                                 return;
977                         if (!files.Any())
978                                 return;
979
980                         StatusMessage = message;
981
982                         _events.Publish(new Notification { Title = "Pithos+", Message = message, Level = level});
983                 }
984
985                 public void Notify(Notification notification)
986                 {
987             TaskEx.Run(()=> _events.Publish(notification));
988                 }
989
990
991                 public void RemoveMonitor(string serverUrl,string accountName)
992                 {
993                         if (String.IsNullOrWhiteSpace(accountName))
994                                 return;
995
996                         var accountInfo=_accounts.FirstOrDefault(account => account.UserName == accountName && account.StorageUri.ToString().StartsWith(serverUrl));
997             if (accountInfo != null)
998             {
999                 _accounts.TryRemove(accountInfo);
1000                 _pollAgent.RemoveAccount(accountInfo);
1001             }
1002
1003             var accountKey = new Uri(new Uri(serverUrl),accountName);
1004                     PithosMonitor monitor;
1005                         if (Monitors.TryRemove(accountKey, out monitor))
1006                         {
1007                                 monitor.Stop();
1008                 //TODO: Also remove any pending actions for this account
1009                 //from the network queue                
1010                         }
1011                 }
1012
1013                 public void RefreshOverlays()
1014                 {
1015                         foreach (var pair in Monitors)
1016                         {
1017                                 var monitor = pair.Value;
1018
1019                                 var path = monitor.RootPath;
1020
1021                                 if (String.IsNullOrWhiteSpace(path))
1022                                         continue;
1023
1024                                 if (!Directory.Exists(path) && !File.Exists(path))
1025                                         continue;
1026
1027                                 IntPtr pathPointer = Marshal.StringToCoTaskMemAuto(path);
1028
1029                                 try
1030                                 {
1031                                         NativeMethods.SHChangeNotify(HChangeNotifyEventID.SHCNE_UPDATEITEM,
1032                                                                                                  HChangeNotifyFlags.SHCNF_PATHW | HChangeNotifyFlags.SHCNF_FLUSHNOWAIT,
1033                                                                                                  pathPointer, IntPtr.Zero);
1034                                 }
1035                                 finally
1036                                 {
1037                                         Marshal.FreeHGlobal(pathPointer);
1038                                 }
1039                         }
1040                 }
1041
1042                 #region Event Handlers
1043                 
1044                 public void Handle(SelectiveSynchChanges message)
1045                 {
1046                     TaskEx.Run(() =>
1047                     {
1048                         PithosMonitor monitor;
1049                         if (Monitors.TryGetValue(message.Account.AccountKey, out monitor))
1050                         {
1051                     Selectives.SetIsSelectiveEnabled(message.Account.AccountKey, message.Enabled);
1052                             monitor.SetSelectivePaths(message.Uris, message.Added, message.Removed);
1053                         }
1054
1055                         var account = Accounts.FirstOrDefault(acc => acc.AccountKey == message.Account.AccountKey);
1056                         if (account != null)
1057                         {
1058                             var added=monitor.UrisToFilePaths(message.Added);
1059                     _pollAgent.SynchNow(added);
1060                         }
1061                     });
1062
1063                 }
1064
1065
1066                 private bool _pollStarted;
1067             private Sparkle _sparkle;
1068             private bool _manualUpgradeCheck;
1069
1070             //SMELL: Doing so much work for notifications in the shell is wrong
1071                 //The notifications should be moved to their own view/viewmodel pair
1072                 //and different templates should be used for different message types
1073                 //This will also allow the addition of extra functionality, eg. actions
1074                 //
1075                 public void Handle(Notification notification)
1076                 {
1077                         UpdateStatus();
1078
1079                         if (!Settings.ShowDesktopNotifications)
1080                                 return;
1081
1082                         if (notification is PollNotification)
1083                         {
1084                                 _pollStarted = true;
1085                                 return;
1086                         }
1087                         if (notification is CloudNotification)
1088                         {
1089                                 if (!_pollStarted) 
1090                                         return;
1091                                 _pollStarted= false;
1092                                 notification.Title = "Pithos+";
1093                                 notification.Message = "Start Synchronisation";
1094                         }
1095
1096                     var deleteNotification = notification as CloudDeleteNotification;
1097             if (deleteNotification != null)
1098             {
1099                 StatusMessage = String.Format("Deleted {0}", deleteNotification.Data.Name);
1100                 return;
1101             }
1102
1103                     var progress = notification as ProgressNotification;
1104                     
1105                     
1106             if (progress != null)
1107                     {
1108                         StatusMessage = String.Format("{0} {1:p2} of {2} - {3}",                                                      
1109                                               progress.Action,
1110                                                       (progress.Block + progress.BlockPercentage/100.0)/(double)progress.TotalBlocks,
1111                                                       progress.FileSize.ToByteSize(),
1112                                                       progress.FileName);
1113                         return;
1114                     }
1115
1116                     var info = notification as StatusNotification;
1117             if (info != null)
1118             {
1119                 StatusMessage = info.Title;
1120                 return;
1121             }
1122                         if (String.IsNullOrWhiteSpace(notification.Message) && String.IsNullOrWhiteSpace(notification.Title))
1123                                 return;
1124
1125             if (notification.Level <= TraceLevel.Warning)
1126                             ShowBalloonFor(notification);
1127                 }
1128
1129             private void ShowBalloonFor(Notification notification)
1130             {
1131             Contract.Requires(notification!=null);
1132             
1133             if (!Settings.ShowDesktopNotifications) 
1134                 return;
1135             
1136             BalloonIcon icon;
1137                 switch (notification.Level)
1138                 {
1139                 case TraceLevel.Verbose:
1140                         return;
1141                     case TraceLevel.Info:                   
1142                         icon = BalloonIcon.Info;
1143                         break;
1144                 case TraceLevel.Error:
1145                     icon = BalloonIcon.Error;
1146                     break;
1147                 case TraceLevel.Warning:
1148                         icon = BalloonIcon.Warning;
1149                         break;
1150                     default:
1151                         return;
1152                 }
1153
1154                 var tv = (ShellView) GetView();
1155                 System.Action clickAction = null;
1156                 if (notification is ExpirationNotification)
1157                 {
1158                     clickAction = () => ShowPreferences("AccountTab");
1159                 }
1160                 var balloon = new PithosBalloon
1161                                   {
1162                                       Title = notification.Title,
1163                                       Message = notification.Message,
1164                                       Icon = icon,
1165                                       ClickAction = clickAction
1166                                   };
1167                 tv.TaskbarView.ShowCustomBalloon(balloon, PopupAnimation.Fade, 4000);
1168             }
1169
1170             #endregion
1171
1172                 public void Handle(ShowFilePropertiesEvent message)
1173                 {
1174                         if (message == null)
1175                                 throw new ArgumentNullException("message");
1176                         if (String.IsNullOrWhiteSpace(message.FileName) )
1177                                 throw new ArgumentException("message");
1178                         Contract.EndContractBlock();
1179
1180                         var fileName = message.FileName;
1181                         //TODO: Display file properties for non-container folders
1182                         if (File.Exists(fileName))
1183                                 //Retrieve the full name with exact casing. Pithos names are case sensitive                             
1184                                 ShowFileProperties(FileInfoExtensions.GetProperFilePathCapitalization(fileName));
1185                         else if (Directory.Exists(fileName))
1186                                 //Retrieve the full name with exact casing. Pithos names are case sensitive
1187                         {
1188                                 var path = FileInfoExtensions.GetProperDirectoryCapitalization(fileName);
1189                                 if (IsContainer(path))
1190                                         ShowContainerProperties(path);
1191                                 else
1192                                         ShowFileProperties(path);
1193                         }
1194                 }
1195
1196                 private bool IsContainer(string path)
1197                 {
1198                         var matchingFolders = from account in _accounts
1199                                                                   from rootFolder in Directory.GetDirectories(account.AccountPath)
1200                                                                   where rootFolder.Equals(path, StringComparison.InvariantCultureIgnoreCase)
1201                                                                   select rootFolder;
1202                         return matchingFolders.Any();
1203                 }
1204
1205                 public FileStatus GetFileStatus(string localFileName)
1206                 {
1207                         if (String.IsNullOrWhiteSpace(localFileName))
1208                                 throw new ArgumentNullException("localFileName");
1209                         Contract.EndContractBlock();
1210                         
1211                         var statusKeeper = IoC.Get<IStatusKeeper>();
1212                         var status=statusKeeper.GetFileStatus(localFileName);
1213                         return status;
1214                 }
1215
1216             public void RemoveAccountFromDatabase(AccountSettings account)
1217             {
1218             var statusKeeper = IoC.Get<IStatusKeeper>();
1219             statusKeeper.ClearFolderStatus(account.RootPath);           
1220             }
1221         }
1222 }