Statistics
| Branch: | Revision:

root / trunk / Pithos.Client.WPF / Shell / ShellViewModel.cs @ 34bdb91d

History | View | Annotate | Download (25.9 kB)

1
using System.Collections.Concurrent;
2
using System.Diagnostics;
3
using System.Diagnostics.Contracts;
4
using System.IO;
5
using System.Net;
6
using System.Reflection;
7
using System.Runtime.InteropServices;
8
using System.ServiceModel;
9
using System.Threading.Tasks;
10
using System.Windows;
11
using System.Windows.Controls.Primitives;
12
using Caliburn.Micro;
13
using Hardcodet.Wpf.TaskbarNotification;
14
using Pithos.Client.WPF.Configuration;
15
using Pithos.Client.WPF.FileProperties;
16
using Pithos.Client.WPF.Preferences;
17
using Pithos.Client.WPF.SelectiveSynch;
18
using Pithos.Client.WPF.Services;
19
using Pithos.Client.WPF.Shell;
20
using Pithos.Core;
21
using Pithos.Core.Agents;
22
using Pithos.Interfaces;
23
using System;
24
using System.Collections.Generic;
25
using System.Linq;
26
using Pithos.Network;
27
using StatusService = Pithos.Client.WPF.Services.StatusService;
28

    
29
namespace Pithos.Client.WPF {
30
	using System.ComponentModel.Composition;
31

    
32
	
33
	///<summary>
34
	/// The "shell" of the Pithos application displays the taskbar  icon, menu and notifications.
35
	/// The shell also hosts the status service called by shell extensions to retrieve file info
36
	///</summary>
37
	///<remarks>
38
	/// It is a strange "shell" as its main visible element is an icon instead of a window
39
	/// The shell subscribes to the following events:
40
	/// * Notification:  Raised by components that want to notify the user. Usually displayed in a balloon
41
	/// * 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
42
	/// * ShowFilePropertiesEvent: Raised when a shell command requests the display of the file/container properties dialog
43
	///</remarks>		
44
	//TODO: CODE SMELL Why does the shell handle the SelectiveSynchChanges?
45
	[Export(typeof(IShell))]
46
	public class ShellViewModel : Screen, IStatusNotification, IShell,
47
		IHandle<Notification>, IHandle<SelectiveSynchChanges>, IHandle<ShowFilePropertiesEvent>
48
	{
49
		//The Status Checker provides the current synch state
50
		//TODO: Could we remove the status checker and use events in its place?
51
		private readonly IStatusChecker _statusChecker;
52
		private readonly IEventAggregator _events;
53

    
54
		public PithosSettings Settings { get; private set; }
55

    
56

    
57
        private readonly ConcurrentDictionary<string, PithosMonitor> _monitors = new ConcurrentDictionary<string, PithosMonitor>();
58
		///<summary>
59
		/// Dictionary of account monitors, keyed by account
60
		///</summary>
61
		///<remarks>
62
		/// One monitor class is created for each account. The Shell needs access to the monitors to execute start/stop/pause commands,
63
		/// retrieve account and boject info		
64
		///</remarks>
65
		// TODO: Does the Shell REALLY need access to the monitors? Could we achieve the same results with a better design?
66
		// TODO: The monitors should be internal to Pithos.Core, even though exposing them makes coding of the Object and Container windows easier
67
        public ConcurrentDictionary<string, PithosMonitor> Monitors
68
		{
69
			get { return _monitors; }
70
		}
71

    
72

    
73
	    ///<summary>
74
	    /// The status service is used by Shell extensions to retrieve file status information
75
	    ///</summary>
76
	    //TODO: CODE SMELL! This is the shell! While hosting in the shell makes executing start/stop commands easier, it is still a smell
77
	    private ServiceHost _statusService;
78

    
79
		//Logging in the Pithos client is provided by log4net
80
		private static readonly log4net.ILog Log = log4net.LogManager.GetLogger("Pithos");
81

    
82
        //Lazily initialized File Version info. This is done once and lazily to avoid blocking the UI
83
        private Lazy<FileVersionInfo> _fileVersion;
84

    
85
		///<summary>
86
		/// The Shell depends on MEF to provide implementations for windowManager, events, the status checker service and the settings
87
		///</summary>
88
		///<remarks>
89
		/// The PithosSettings class encapsulates the app's settings to abstract their storage mechanism (App settings, a database or registry)
90
		///</remarks>
91
		[ImportingConstructor]		
92
		public ShellViewModel(IWindowManager windowManager, IEventAggregator events, IStatusChecker statusChecker, PithosSettings settings)
93
		{
94
			try
95
			{
96

    
97
				_windowManager = windowManager;
98
				//CHECK: Caliburn doesn't need explicit command construction
99
				//OpenPithosFolderCommand = new PithosCommand(OpenPithosFolder);
100
				_statusChecker = statusChecker;
101
				//The event subst
102
				_events = events;
103
				_events.Subscribe(this);
104

    
105
				Settings = settings;
106

    
107
                Proxy.SetFromSettings(settings);
108

    
109
				StatusMessage = "In Synch";
110

    
111
                _fileVersion=  new Lazy<FileVersionInfo>(() =>
112
                {
113
                    Assembly assembly = Assembly.GetExecutingAssembly();
114
                    var fileVersion = FileVersionInfo.GetVersionInfo(assembly.Location);
115
                    return fileVersion;
116
                });
117
				_accounts.CollectionChanged += (sender, e) =>
118
												   {
119
													   NotifyOfPropertyChange(() => OpenFolderCaption);
120
													   NotifyOfPropertyChange(() => HasAccounts);
121
												   };
122

    
123
			}
124
			catch (Exception exc)
125
			{
126
				Log.Error("Error while starting the ShellViewModel",exc);
127
				throw;
128
			}
129
		}
130

    
131

    
132
		protected override void OnActivate()
133
		{
134
			base.OnActivate();
135

    
136
            
137

    
138
			StartMonitoring();                    
139
		}
140

    
141

    
142

    
143
		private async void StartMonitoring()
144
		{
145
			try
146
			{
147
				var accounts = Settings.Accounts.Select(MonitorAccount);
148
				await TaskEx.WhenAll(accounts);
149
				_statusService = StatusService.Start();
150

    
151
/*
152
				foreach (var account in Settings.Accounts)
153
				{
154
					await MonitorAccount(account);
155
				}
156
*/
157
				
158
			}
159
			catch (AggregateException exc)
160
			{
161
				exc.Handle(e =>
162
				{
163
					Log.Error("Error while starting monitoring", e);
164
					return true;
165
				});
166
				throw;
167
			}
168
		}
169

    
170
		protected override void OnDeactivate(bool close)
171
		{
172
			base.OnDeactivate(close);
173
			if (close)
174
			{
175
				StatusService.Stop(_statusService);
176
				_statusService = null;
177
			}
178
		}
179

    
180
		public Task MonitorAccount(AccountSettings account)
181
		{
182
			return Task.Factory.StartNew(() =>
183
			{                                                
184
				PithosMonitor monitor;
185
				var accountName = account.AccountName;
186

    
187
				if (_monitors.TryGetValue(accountName, out monitor))
188
				{
189
					//If the account is active
190
					if (account.IsActive)
191
						//Start the monitor. It's OK to start an already started monitor,
192
						//it will just ignore the call                        
193
						StartMonitor(monitor).Wait();                        
194
					else
195
					{
196
						//If the account is inactive
197
						//Stop and remove the monitor
198
						RemoveMonitor(accountName);
199
					}
200
					return;
201
				}
202

    
203
                
204
				//Create a new monitor/ Can't use MEF here, it would return a single instance for all monitors
205
				monitor = new PithosMonitor
206
							  {
207
								  UserName = accountName,
208
								  ApiKey = account.ApiKey,                                  
209
								  StatusNotification = this,
210
								  RootPath = account.RootPath
211
							  };
212
				//PithosMonitor uses MEF so we need to resolve it
213
				IoC.BuildUp(monitor);
214

    
215
			    monitor.AuthenticationUrl = account.ServerUrl;
216

    
217
				_monitors[accountName] = monitor;
218

    
219
				if (account.IsActive)
220
				{
221
					//Don't start a monitor if it doesn't have an account and ApiKey
222
					if (String.IsNullOrWhiteSpace(monitor.UserName) ||
223
						String.IsNullOrWhiteSpace(monitor.ApiKey))
224
						return;
225
					StartMonitor(monitor);
226
				}
227
			});
228
		}
229

    
230

    
231
		protected override void OnViewLoaded(object view)
232
		{
233
			UpdateStatus();
234
			var window = (Window)view;            
235
			TaskEx.Delay(1000).ContinueWith(t => Execute.OnUIThread(window.Hide));
236
			base.OnViewLoaded(view);
237
		}
238

    
239

    
240
		#region Status Properties
241

    
242
		private string _statusMessage;
243
		public string StatusMessage
244
		{
245
			get { return _statusMessage; }
246
			set
247
			{
248
				_statusMessage = value;
249
				NotifyOfPropertyChange(() => StatusMessage);
250
			}
251
		}
252

    
253
		private readonly ObservableConcurrentCollection<AccountInfo> _accounts = new ObservableConcurrentCollection<AccountInfo>();
254
		public ObservableConcurrentCollection<AccountInfo> Accounts
255
		{
256
			get { return _accounts; }
257
		}
258

    
259
		public bool HasAccounts
260
		{
261
			get { return _accounts.Count > 0; }
262
		}
263

    
264

    
265
		public string OpenFolderCaption
266
		{
267
			get
268
			{
269
				return (_accounts.Count == 0)
270
						? "No Accounts Defined"
271
						: "Open Pithos Folder";
272
			}
273
		}
274

    
275
		private string _pauseSyncCaption="Pause Synching";
276
		public string PauseSyncCaption
277
		{
278
			get { return _pauseSyncCaption; }
279
			set
280
			{
281
				_pauseSyncCaption = value;
282
				NotifyOfPropertyChange(() => PauseSyncCaption);
283
			}
284
		}
285

    
286
		private readonly ObservableConcurrentCollection<FileEntry> _recentFiles = new ObservableConcurrentCollection<FileEntry>();
287
		public ObservableConcurrentCollection<FileEntry> RecentFiles
288
		{
289
			get { return _recentFiles; }
290
		}
291

    
292

    
293
		private string _statusIcon="../Images/Pithos.ico";
294
		public string StatusIcon
295
		{
296
			get { return _statusIcon; }
297
			set
298
			{
299
                //TODO: Ensure all status icons use the Pithos logo
300
				_statusIcon = value;
301
				NotifyOfPropertyChange(() => StatusIcon);
302
			}
303
		}
304

    
305
		#endregion
306

    
307
		#region Commands
308

    
309
		public void ShowPreferences()
310
		{
311
			Settings.Reload();
312
			var preferences = new PreferencesViewModel(_windowManager,_events, this,Settings);            
313
			_windowManager.ShowDialog(preferences);
314
			
315
		}
316

    
317
		public void AboutPithos()
318
		{
319
			var about = new AboutViewModel();
320
			_windowManager.ShowWindow(about);
321
		}
322

    
323
		public void SendFeedback()
324
		{
325
			var feedBack =  IoC.Get<FeedbackViewModel>();
326
			_windowManager.ShowWindow(feedBack);
327
		}
328

    
329
		//public PithosCommand OpenPithosFolderCommand { get; private set; }
330

    
331
		public void OpenPithosFolder()
332
		{
333
			var account = Settings.Accounts.FirstOrDefault(acc => acc.IsActive);
334
			if (account == null)
335
				return;
336
			Process.Start(account.RootPath);
337
		}
338

    
339
		public void OpenPithosFolder(AccountInfo account)
340
		{
341
			Process.Start(account.AccountPath);
342
		}
343

    
344
		
345
/*
346
		public void GoToSite()
347
		{            
348
			var site = Properties.Settings.Default.PithosSite;
349
			Process.Start(site);            
350
		}
351
*/
352

    
353
		public void GoToSite(AccountInfo account)
354
		{
355
			/*var site = String.Format("{0}/ui/?token={1}&user={2}",
356
				account.SiteUri,account.Token,
357
				account.UserName);*/
358
			Process.Start(account.SiteUri);
359
		}
360

    
361
		public void ShowFileProperties()
362
		{
363
			var account = Settings.Accounts.First(acc => acc.IsActive);            
364
			var dir = new DirectoryInfo(account.RootPath + @"\pithos");
365
			var files=dir.GetFiles();
366
			var r=new Random();
367
			var idx=r.Next(0, files.Length);
368
			ShowFileProperties(files[idx].FullName);            
369
		}
370

    
371
		public void ShowFileProperties(string filePath)
372
		{
373
			if (String.IsNullOrWhiteSpace(filePath))
374
				throw new ArgumentNullException("filePath");
375
			if (!File.Exists(filePath) && !Directory.Exists(filePath))
376
				throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath");
377
			Contract.EndContractBlock();
378

    
379
			var pair=(from monitor in  Monitors
380
							   where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)
381
								   select monitor).FirstOrDefault();
382
		    var accountMonitor = pair.Value;
383

    
384
			if (accountMonitor == null)
385
				return;
386

    
387
			var infoTask=Task.Factory.StartNew(()=>accountMonitor.GetObjectInfo(filePath));
388

    
389
			
390

    
391
			var fileProperties = new FilePropertiesViewModel(this, infoTask,filePath);
392
			_windowManager.ShowWindow(fileProperties);
393
		} 
394
		
395
		public void ShowContainerProperties()
396
		{
397
			var account = Settings.Accounts.First(acc => acc.IsActive);            
398
			var dir = new DirectoryInfo(account.RootPath);
399
			var fullName = (from folder in dir.EnumerateDirectories()
400
							where (folder.Attributes & FileAttributes.Hidden) == 0
401
							select folder.FullName).First();
402
			ShowContainerProperties(fullName);            
403
		}
404

    
405
		public void ShowContainerProperties(string filePath)
406
		{
407
			if (String.IsNullOrWhiteSpace(filePath))
408
				throw new ArgumentNullException("filePath");
409
			if (!Directory.Exists(filePath))
410
				throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath");
411
			Contract.EndContractBlock();
412

    
413
			var pair=(from monitor in  Monitors
414
							   where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)
415
								   select monitor).FirstOrDefault();
416
		    var accountMonitor = pair.Value;            
417
			var info = accountMonitor.GetContainerInfo(filePath);
418

    
419
			
420

    
421
			var containerProperties = new ContainerPropertiesViewModel(this, info,filePath);
422
			_windowManager.ShowWindow(containerProperties);
423
		}
424

    
425
        public void SynchNow()
426
        {
427
            var agent = IoC.Get<NetworkAgent>();
428
            agent.SynchNow();
429
        }
430

    
431
		public ObjectInfo RefreshObjectInfo(ObjectInfo currentInfo)
432
		{
433
			if (currentInfo==null)
434
				throw new ArgumentNullException("currentInfo");
435
			Contract.EndContractBlock();
436

    
437
			var monitor = Monitors[currentInfo.Account];
438
			var newInfo=monitor.CloudClient.GetObjectInfo(currentInfo.Account, currentInfo.Container, currentInfo.Name);
439
			return newInfo;
440
		}
441

    
442
		public ContainerInfo RefreshContainerInfo(ContainerInfo container)
443
		{
444
			if (container == null)
445
				throw new ArgumentNullException("container");
446
			Contract.EndContractBlock();
447

    
448
			var monitor = Monitors[container.Account];
449
			var newInfo = monitor.CloudClient.GetContainerInfo(container.Account, container.Name);
450
			return newInfo;
451
		}
452

    
453

    
454
		public void ToggleSynching()
455
		{
456
			bool isPaused=false;
457
			foreach (var pair in Monitors)
458
			{
459
				var monitor = pair.Value;
460
				monitor.Pause = !monitor.Pause;
461
				isPaused = monitor.Pause;
462
			}
463

    
464
			PauseSyncCaption = isPaused ? "Resume syncing" : "Pause syncing";
465
			var iconKey = isPaused? "TraySyncPaused" : "TrayInSynch";
466
			StatusIcon = String.Format(@"../Images/{0}.ico", iconKey);
467
		}
468

    
469
		public void ExitPithos()
470
		{
471
			foreach (var pair in Monitors)
472
			{
473
				var monitor = pair.Value;
474
				monitor.Stop();
475
			}
476

    
477
			((Window)GetView()).Close();
478
		}
479
		#endregion
480

    
481

    
482
		private readonly Dictionary<PithosStatus, StatusInfo> _iconNames = new List<StatusInfo>
483
			{
484
				new StatusInfo(PithosStatus.InSynch, "All files up to date", "TrayInSynch"),
485
				new StatusInfo(PithosStatus.Syncing, "Syncing Files", "TraySynching"),
486
				new StatusInfo(PithosStatus.SyncPaused, "Sync Paused", "TraySyncPaused")
487
			}.ToDictionary(s => s.Status);
488

    
489
		readonly IWindowManager _windowManager;
490
	    
491

    
492
		///<summary>
493
		/// Updates the visual status indicators of the application depending on status changes, e.g. icon, stat		
494
		///</summary>
495
		public void UpdateStatus()
496
		{
497
			var pithosStatus = _statusChecker.GetPithosStatus();
498

    
499
			if (_iconNames.ContainsKey(pithosStatus))
500
			{
501
				var info = _iconNames[pithosStatus];
502
				StatusIcon = String.Format(@"../Images/{0}.ico", info.IconName);
503

    
504

    
505

    
506
				StatusMessage = String.Format("Pithos {0}\r\n{1}", _fileVersion.Value.FileVersion,info.StatusText);
507
			}
508
			
509
			//_events.Publish(new Notification { Title = "Start", Message = "Start Monitoring", Level = TraceLevel.Info});
510
		}
511

    
512

    
513
	   
514
		private Task StartMonitor(PithosMonitor monitor,int retries=0)
515
		{
516
			return Task.Factory.StartNew(() =>
517
			{
518
				using (log4net.ThreadContext.Stacks["Monitor"].Push("Start"))
519
				{
520
					try
521
					{
522
						Log.InfoFormat("Start Monitoring {0}", monitor.UserName);
523

    
524
						monitor.Start();
525
					}
526
					catch (WebException exc)
527
					{
528
						if (AbandonRetry(monitor, retries))
529
							return;
530

    
531
                        HttpStatusCode statusCode =HttpStatusCode.OK;
532
			            var response = exc.Response as HttpWebResponse;
533
                        if(response!=null)
534
					        statusCode = response.StatusCode;
535

    
536
                        switch (statusCode)
537
                        {
538
                            case HttpStatusCode.Unauthorized:
539
                                var message = String.Format("API Key Expired for {0}. Starting Renewal",
540
                                                            monitor.UserName);
541
                                Log.Error(message, exc);
542
                                TryAuthorize(monitor, retries).Wait();
543
                                break;
544
                            case HttpStatusCode.ProxyAuthenticationRequired:
545
                                TryAuthenticateProxy(monitor,retries);
546
                                break;
547
                            default:
548
                                TryLater(monitor, exc, retries);
549
                                break;
550
                        }
551
					}
552
					catch (Exception exc)
553
					{
554
						if (AbandonRetry(monitor, retries)) 
555
							return;
556

    
557
						TryLater(monitor,exc,retries);
558
					}
559
				}
560
			});
561
		}
562

    
563
	    private void TryAuthenticateProxy(PithosMonitor monitor,int retries)
564
	    {
565
	        Execute.OnUIThread(() =>
566
	                               {                                       
567
	                                   var proxyAccount = IoC.Get<ProxyAccountViewModel>();
568
                                        proxyAccount.Settings = this.Settings;
569
	                                   if (true != _windowManager.ShowDialog(proxyAccount)) 
570
                                           return;
571
	                                   StartMonitor(monitor, retries);
572
	                                   NotifyOfPropertyChange(() => Accounts);
573
	                               });
574
	    }
575

    
576
	    private bool AbandonRetry(PithosMonitor monitor, int retries)
577
		{
578
			if (retries > 1)
579
			{
580
				var message = String.Format("Monitoring of account {0} has failed too many times. Will not retry",
581
											monitor.UserName);
582
				_events.Publish(new Notification
583
									{Title = "Account monitoring failed", Message = message, Level = TraceLevel.Error});
584
				return true;
585
			}
586
			return false;
587
		}
588

    
589

    
590
		private async Task TryAuthorize(PithosMonitor monitor,int retries)
591
		{
592
			_events.Publish(new Notification { Title = "Authorization failed", Message = "Your API Key has probably expired. You will be directed to a page where you can renew it", Level = TraceLevel.Error });
593

    
594
			try
595
			{
596

    
597
				var credentials = await PithosAccount.RetrieveCredentials(Settings.PithosLoginUrl);
598

    
599
				var account = Settings.Accounts.First(act => act.AccountName == credentials.UserName);
600
				account.ApiKey = credentials.Password;
601
				monitor.ApiKey = credentials.Password;
602
				Settings.Save();
603
				await TaskEx.Delay(10000);
604
				StartMonitor(monitor, retries + 1);
605
				NotifyOfPropertyChange(()=>Accounts);
606
			}
607
			catch (AggregateException exc)
608
			{
609
				string message = String.Format("API Key retrieval for {0} failed", monitor.UserName);
610
				Log.Error(message, exc.InnerException);
611
				_events.Publish(new Notification { Title = "Authorization failed", Message = message, Level = TraceLevel.Error });
612
			}
613
			catch (Exception exc)
614
			{
615
				string message = String.Format("API Key retrieval for {0} failed", monitor.UserName);
616
				Log.Error(message, exc);
617
				_events.Publish(new Notification { Title = "Authorization failed", Message = message, Level = TraceLevel.Error });
618
			}
619

    
620
		}
621

    
622
		private static bool IsUnauthorized(WebException exc)
623
		{
624
			if (exc==null)
625
				throw new ArgumentNullException("exc");
626
			Contract.EndContractBlock();
627

    
628
			var response = exc.Response as HttpWebResponse;
629
			if (response == null)
630
				return false;
631
			return (response.StatusCode == HttpStatusCode.Unauthorized);
632
		}
633

    
634
		private void TryLater(PithosMonitor monitor, Exception exc,int retries)
635
		{
636
			var message = String.Format("An exception occured. Can't start monitoring\nWill retry in 10 seconds");
637
			Task.Factory.StartNewDelayed(10000, () => StartMonitor(monitor,retries+1));
638
			_events.Publish(new Notification
639
								{Title = "Error", Message = message, Level = TraceLevel.Error});
640
			Log.Error(message, exc);
641
		}
642

    
643

    
644
		public void NotifyChange(string status, TraceLevel level=TraceLevel.Info)
645
		{
646
			StatusMessage = status;
647
			
648
			_events.Publish(new Notification { Title = "Pithos", Message = status, Level = level });
649
		}
650

    
651
		public void NotifyChangedFile(string filePath)
652
		{
653
			var entry = new FileEntry {FullPath=filePath};
654
			IProducerConsumerCollection<FileEntry> files=RecentFiles;
655
			FileEntry popped;
656
			while (files.Count > 5)
657
				files.TryTake(out popped);
658
			files.TryAdd(entry);
659
		}
660

    
661
		public void NotifyAccount(AccountInfo account)
662
		{
663
			if (account== null)
664
				return;
665
			//TODO: What happens to an existing account whose Token has changed?
666
			account.SiteUri= String.Format("{0}/ui/?token={1}&user={2}",
667
				account.SiteUri, Uri.EscapeDataString(account.Token),
668
                Uri.EscapeDataString(account.UserName));
669

    
670
			if (Accounts.All(item => item.UserName != account.UserName))
671
				Accounts.TryAdd(account);
672

    
673
		}
674

    
675
	    public void NotifyConflicts(IEnumerable<FileSystemInfo> conflictFiles, string message)
676
	    {
677
            if (conflictFiles == null)
678
                return;
679
            if (!conflictFiles.Any())
680
                return;
681

    
682
            UpdateStatus();
683
            //TODO: Create a more specific message. For now, just show a warning
684
            NotifyForFiles(conflictFiles,message,TraceLevel.Warning);
685

    
686
	    }
687

    
688
	    public void NotifyForFiles(IEnumerable<FileSystemInfo> files, string message,TraceLevel level=TraceLevel.Info)
689
	    {
690
            if (files == null)
691
                return;
692
            if (!files.Any())
693
                return;
694

    
695
            StatusMessage = message;
696

    
697
            _events.Publish(new Notification { Title = "Pithos", Message = message, Level = level});
698
        }
699

    
700
        public void Notify(Notification notification)
701
        {
702
            _events.Publish(notification);
703
        }
704

    
705

    
706
	    public void RemoveMonitor(string accountName)
707
		{
708
			if (String.IsNullOrWhiteSpace(accountName))
709
				return;
710

    
711
			var accountInfo=_accounts.FirstOrDefault(account => account.UserName == accountName);
712
			_accounts.TryRemove(accountInfo);
713

    
714
			PithosMonitor monitor;
715
			if (Monitors.TryRemove(accountName, out monitor))
716
			{
717
				monitor.Stop();
718
			}
719
		}
720

    
721
		public void RefreshOverlays()
722
		{
723
			foreach (var pair in Monitors)
724
			{
725
				var monitor = pair.Value;
726

    
727
				var path = monitor.RootPath;
728

    
729
				if (String.IsNullOrWhiteSpace(path))
730
					continue;
731

    
732
				if (!Directory.Exists(path) && !File.Exists(path))
733
					continue;
734

    
735
				IntPtr pathPointer = Marshal.StringToCoTaskMemAuto(path);
736

    
737
				try
738
				{
739
					NativeMethods.SHChangeNotify(HChangeNotifyEventID.SHCNE_UPDATEITEM,
740
												 HChangeNotifyFlags.SHCNF_PATHW | HChangeNotifyFlags.SHCNF_FLUSHNOWAIT,
741
												 pathPointer, IntPtr.Zero);
742
				}
743
				finally
744
				{
745
					Marshal.FreeHGlobal(pathPointer);
746
				}
747
			}
748
		}
749

    
750
		#region Event Handlers
751
		
752
		public void Handle(SelectiveSynchChanges message)
753
		{
754
			var accountName = message.Account.AccountName;
755
			PithosMonitor monitor;
756
			if (_monitors.TryGetValue(accountName, out monitor))
757
			{
758
				monitor.AddSelectivePaths(message.Added);
759
				monitor.RemoveSelectivePaths(message.Removed);
760

    
761
			}
762
			
763
		}
764

    
765

    
766
	    private bool _pollStarted = false;
767

    
768
        //SMELL: Doing so much work for notifications in the shell is wrong
769
        //The notifications should be moved to their own view/viewmodel pair
770
        //and different templates should be used for different message types
771
        //This will also allow the addition of extra functionality, eg. actions
772
        //
773
		public void Handle(Notification notification)
774
		{
775
            UpdateStatus();
776

    
777
			if (!Settings.ShowDesktopNotifications)
778
				return;
779

    
780
            if (notification is PollNotification)
781
            {
782
                _pollStarted = true;
783
                return;
784
            }
785
            if (notification is CloudNotification)
786
            {
787
                if (!_pollStarted) 
788
                    return;
789
                _pollStarted= false;
790
                notification.Title = "Pithos";
791
                notification.Message = "Start Synchronisation";
792
            }
793

    
794
            if (String.IsNullOrWhiteSpace(notification.Message) && String.IsNullOrWhiteSpace(notification.Title))
795
                return;
796

    
797
			BalloonIcon icon;
798
			switch (notification.Level)
799
			{
800
				case TraceLevel.Error:
801
					icon = BalloonIcon.Error;
802
					break;
803
				case TraceLevel.Info:
804
				case TraceLevel.Verbose:
805
					icon = BalloonIcon.Info;
806
					break;
807
				case TraceLevel.Warning:
808
					icon = BalloonIcon.Warning;
809
					break;
810
				default:
811
					icon = BalloonIcon.None;
812
					break;
813
			}
814
            
815
			if (Settings.ShowDesktopNotifications)
816
			{
817
				var tv = (ShellView) GetView();                
818
                var balloon=new PithosBalloon{Title=notification.Title,Message=notification.Message,Icon=icon};
819
                tv.TaskbarView.ShowCustomBalloon(balloon,PopupAnimation.Fade,4000);
820
//				tv.TaskbarView.ShowBalloonTip(notification.Title, notification.Message, icon);
821
			}
822
		}
823
		#endregion
824

    
825
		public void Handle(ShowFilePropertiesEvent message)
826
		{
827
			if (message == null)
828
				throw new ArgumentNullException("message");
829
			if (String.IsNullOrWhiteSpace(message.FileName) )
830
				throw new ArgumentException("message");
831
			Contract.EndContractBlock();
832

    
833
			var fileName = message.FileName;
834
            //TODO: Display file properties for non-container folders
835
			if (File.Exists(fileName))
836
                //Retrieve the full name with exact casing. Pithos names are case sensitive				
837
                ShowFileProperties(FileInfoExtensions.GetProperFilePathCapitalization(fileName));
838
			else if (Directory.Exists(fileName))
839
                //Retrieve the full name with exact casing. Pithos names are case sensitive
840
			{
841
                var path = FileInfoExtensions.GetProperDirectoryCapitalization(fileName);
842
                if (IsContainer(path))
843
			        ShowContainerProperties(path);
844
                else
845
                    ShowFileProperties(path);
846
			}
847
		}
848

    
849
	    private bool IsContainer(string path)
850
	    {
851
	        var matchingFolders = from account in _accounts
852
	                              from rootFolder in Directory.GetDirectories(account.AccountPath)
853
	                              where rootFolder.Equals(path, StringComparison.InvariantCultureIgnoreCase)
854
	                              select rootFolder;
855
	        return matchingFolders.Any();
856
	    }
857

    
858
	    public FileStatus GetFileStatus(string localFileName)
859
	    {
860
            if (String.IsNullOrWhiteSpace(localFileName))
861
                throw new ArgumentNullException("localFileName");
862
            Contract.EndContractBlock();
863
            
864
	        var statusKeeper = IoC.Get<IStatusKeeper>();
865
	        var status=statusKeeper.GetFileStatus(localFileName);
866
	        return status;
867
	    }
868
	}
869
}