Statistics
| Branch: | Revision:

root / trunk / Pithos.Client.WPF / Shell / ShellViewModel.cs @ db8a9589

History | View | Annotate | Download (26.8 kB)

1
#region
2
/* -----------------------------------------------------------------------
3
 * <copyright file="ShellViewModel.cs" company="GRNet">
4
 * 
5
 * Copyright 2011-2012 GRNET S.A. All rights reserved.
6
 *
7
 * Redistribution and use in source and binary forms, with or
8
 * without modification, are permitted provided that the following
9
 * conditions are met:
10
 *
11
 *   1. Redistributions of source code must retain the above
12
 *      copyright notice, this list of conditions and the following
13
 *      disclaimer.
14
 *
15
 *   2. Redistributions in binary form must reproduce the above
16
 *      copyright notice, this list of conditions and the following
17
 *      disclaimer in the documentation and/or other materials
18
 *      provided with the distribution.
19
 *
20
 *
21
 * THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
22
 * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
23
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
24
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
25
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
26
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
27
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
28
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
29
 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
30
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
31
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32
 * POSSIBILITY OF SUCH DAMAGE.
33
 *
34
 * The views and conclusions contained in the software and
35
 * documentation are those of the authors and should not be
36
 * interpreted as representing official policies, either expressed
37
 * or implied, of GRNET S.A.
38
 * </copyright>
39
 * -----------------------------------------------------------------------
40
 */
41
#endregion
42
using System.Collections.Concurrent;
43
using System.Diagnostics;
44
using System.Diagnostics.Contracts;
45
using System.IO;
46
using System.Net;
47
using System.Reflection;
48
using System.Runtime.InteropServices;
49
using System.ServiceModel;
50
using System.Threading.Tasks;
51
using System.Windows;
52
using System.Windows.Controls.Primitives;
53
using Caliburn.Micro;
54
using Hardcodet.Wpf.TaskbarNotification;
55
using Pithos.Client.WPF.Configuration;
56
using Pithos.Client.WPF.FileProperties;
57
using Pithos.Client.WPF.Preferences;
58
using Pithos.Client.WPF.SelectiveSynch;
59
using Pithos.Client.WPF.Services;
60
using Pithos.Client.WPF.Shell;
61
using Pithos.Core;
62
using Pithos.Core.Agents;
63
using Pithos.Interfaces;
64
using System;
65
using System.Collections.Generic;
66
using System.Linq;
67
using Pithos.Network;
68
using StatusService = Pithos.Client.WPF.Services.StatusService;
69

    
70
namespace Pithos.Client.WPF {
71
	using System.ComponentModel.Composition;
72

    
73
	
74
	///<summary>
75
	/// The "shell" of the Pithos application displays the taskbar  icon, menu and notifications.
76
	/// The shell also hosts the status service called by shell extensions to retrieve file info
77
	///</summary>
78
	///<remarks>
79
	/// It is a strange "shell" as its main visible element is an icon instead of a window
80
	/// The shell subscribes to the following events:
81
	/// * Notification:  Raised by components that want to notify the user. Usually displayed in a balloon
82
	/// * SelectiveSynchChanges: Notifies that the user made changes to the selective synch folders for an account. Raised by the Selective Synch dialog. Located here because the monitors are here
83
	/// * ShowFilePropertiesEvent: Raised when a shell command requests the display of the file/container properties dialog
84
	///</remarks>		
85
	//TODO: CODE SMELL Why does the shell handle the SelectiveSynchChanges?
86
	[Export(typeof(IShell))]
87
	public class ShellViewModel : Screen, IStatusNotification, IShell,
88
		IHandle<Notification>, IHandle<SelectiveSynchChanges>, IHandle<ShowFilePropertiesEvent>
89
	{
90
		//The Status Checker provides the current synch state
91
		//TODO: Could we remove the status checker and use events in its place?
92
		private readonly IStatusChecker _statusChecker;
93
		private readonly IEventAggregator _events;
94

    
95
		public PithosSettings Settings { get; private set; }
96

    
97

    
98
		private readonly ConcurrentDictionary<string, PithosMonitor> _monitors = new ConcurrentDictionary<string, PithosMonitor>();
99
		///<summary>
100
		/// Dictionary of account monitors, keyed by account
101
		///</summary>
102
		///<remarks>
103
		/// One monitor class is created for each account. The Shell needs access to the monitors to execute start/stop/pause commands,
104
		/// retrieve account and boject info		
105
		///</remarks>
106
		// TODO: Does the Shell REALLY need access to the monitors? Could we achieve the same results with a better design?
107
		// TODO: The monitors should be internal to Pithos.Core, even though exposing them makes coding of the Object and Container windows easier
108
		public ConcurrentDictionary<string, PithosMonitor> Monitors
109
		{
110
			get { return _monitors; }
111
		}
112

    
113

    
114
		///<summary>
115
		/// The status service is used by Shell extensions to retrieve file status information
116
		///</summary>
117
		//TODO: CODE SMELL! This is the shell! While hosting in the shell makes executing start/stop commands easier, it is still a smell
118
		private ServiceHost _statusService;
119

    
120
		//Logging in the Pithos client is provided by log4net
121
        private static readonly log4net.ILog Log = log4net.LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
122

    
123
		//Lazily initialized File Version info. This is done once and lazily to avoid blocking the UI
124
		private readonly Lazy<FileVersionInfo> _fileVersion;
125

    
126
	    private readonly PollAgent _pollAgent;
127

    
128
		///<summary>
129
		/// The Shell depends on MEF to provide implementations for windowManager, events, the status checker service and the settings
130
		///</summary>
131
		///<remarks>
132
		/// The PithosSettings class encapsulates the app's settings to abstract their storage mechanism (App settings, a database or registry)
133
		///</remarks>
134
		[ImportingConstructor]		
135
		public ShellViewModel(IWindowManager windowManager, IEventAggregator events, IStatusChecker statusChecker, PithosSettings settings,PollAgent pollAgent)
136
		{
137
			try
138
			{
139

    
140
				_windowManager = windowManager;
141
				//CHECK: Caliburn doesn't need explicit command construction
142
				//OpenPithosFolderCommand = new PithosCommand(OpenPithosFolder);
143
				_statusChecker = statusChecker;
144
				//The event subst
145
				_events = events;
146
				_events.Subscribe(this);
147

    
148
			    _pollAgent = pollAgent;
149
				Settings = settings;
150

    
151
				Proxy.SetFromSettings(settings);
152

    
153
				StatusMessage = "In Synch";
154

    
155
				_fileVersion=  new Lazy<FileVersionInfo>(() =>
156
				{
157
					Assembly assembly = Assembly.GetExecutingAssembly();
158
					var fileVersion = FileVersionInfo.GetVersionInfo(assembly.Location);
159
					return fileVersion;
160
				});
161
				_accounts.CollectionChanged += (sender, e) =>
162
												   {
163
													   NotifyOfPropertyChange(() => OpenFolderCaption);
164
													   NotifyOfPropertyChange(() => HasAccounts);
165
												   };
166

    
167
			}
168
			catch (Exception exc)
169
			{
170
				Log.Error("Error while starting the ShellViewModel",exc);
171
				throw;
172
			}
173
		}
174

    
175

    
176
		protected override void OnActivate()
177
		{
178
			base.OnActivate();
179

    
180
			
181

    
182
			StartMonitoring();                    
183
		}
184

    
185

    
186

    
187
		private async void StartMonitoring()
188
		{
189
			try
190
			{
191
				var accounts = Settings.Accounts.Select(MonitorAccount);
192
				await TaskEx.WhenAll(accounts);
193
				_statusService = StatusService.Start();
194

    
195
/*
196
				foreach (var account in Settings.Accounts)
197
				{
198
					await MonitorAccount(account);
199
				}
200
*/
201
				
202
			}
203
			catch (AggregateException exc)
204
			{
205
				exc.Handle(e =>
206
				{
207
					Log.Error("Error while starting monitoring", e);
208
					return true;
209
				});
210
				throw;
211
			}
212
		}
213

    
214
		protected override void OnDeactivate(bool close)
215
		{
216
			base.OnDeactivate(close);
217
			if (close)
218
			{
219
				StatusService.Stop(_statusService);
220
				_statusService = null;
221
			}
222
		}
223

    
224
		public Task MonitorAccount(AccountSettings account)
225
		{
226
			return Task.Factory.StartNew(() =>
227
			{                                                
228
				PithosMonitor monitor;
229
				var accountName = account.AccountName;
230

    
231
				if (_monitors.TryGetValue(accountName, out monitor))
232
				{
233
					//If the account is active
234
                    if (account.IsActive)
235
                    {
236
                        //The Api Key may have changed throuth the Preferences dialog
237
                        monitor.ApiKey = account.ApiKey;
238
						Debug.Assert(monitor.StatusNotification == this,"An existing monitor should already have a StatusNotification service object");
239
                        monitor.RootPath = account.RootPath;
240
                        //Start the monitor. It's OK to start an already started monitor,
241
                        //it will just ignore the call                        
242
                        StartMonitor(monitor).Wait();
243
                    }
244
                    else
245
                    {
246
                        //If the account is inactive
247
                        //Stop and remove the monitor
248
                        RemoveMonitor(accountName);
249
                    }
250
					return;
251
				}
252

    
253
				
254
				//Create a new monitor/ Can't use MEF here, it would return a single instance for all monitors
255
				monitor = new PithosMonitor
256
							  {
257
								  UserName = accountName,
258
								  ApiKey = account.ApiKey,                                  
259
								  StatusNotification = this,
260
								  RootPath = account.RootPath
261
							  };
262
				//PithosMonitor uses MEF so we need to resolve it
263
				IoC.BuildUp(monitor);
264

    
265
				monitor.AuthenticationUrl = account.ServerUrl;
266

    
267
				_monitors[accountName] = monitor;
268

    
269
				if (account.IsActive)
270
				{
271
					//Don't start a monitor if it doesn't have an account and ApiKey
272
					if (String.IsNullOrWhiteSpace(monitor.UserName) ||
273
						String.IsNullOrWhiteSpace(monitor.ApiKey))
274
						return;
275
					StartMonitor(monitor);
276
				}
277
			});
278
		}
279

    
280

    
281
		protected override void OnViewLoaded(object view)
282
		{
283
			UpdateStatus();
284
			var window = (Window)view;            
285
			TaskEx.Delay(1000).ContinueWith(t => Execute.OnUIThread(window.Hide));
286
			base.OnViewLoaded(view);
287
		}
288

    
289

    
290
		#region Status Properties
291

    
292
		private string _statusMessage;
293
		public string StatusMessage
294
		{
295
			get { return _statusMessage; }
296
			set
297
			{
298
				_statusMessage = value;
299
				NotifyOfPropertyChange(() => StatusMessage);
300
			}
301
		}
302

    
303
		private readonly ObservableConcurrentCollection<AccountInfo> _accounts = new ObservableConcurrentCollection<AccountInfo>();
304
		public ObservableConcurrentCollection<AccountInfo> Accounts
305
		{
306
			get { return _accounts; }
307
		}
308

    
309
		public bool HasAccounts
310
		{
311
			get { return _accounts.Count > 0; }
312
		}
313

    
314

    
315
		public string OpenFolderCaption
316
		{
317
			get
318
			{
319
				return (_accounts.Count == 0)
320
						? "No Accounts Defined"
321
						: "Open Pithos Folder";
322
			}
323
		}
324

    
325
		private string _pauseSyncCaption="Pause Synching";
326
		public string PauseSyncCaption
327
		{
328
			get { return _pauseSyncCaption; }
329
			set
330
			{
331
				_pauseSyncCaption = value;
332
				NotifyOfPropertyChange(() => PauseSyncCaption);
333
			}
334
		}
335

    
336
		private readonly ObservableConcurrentCollection<FileEntry> _recentFiles = new ObservableConcurrentCollection<FileEntry>();
337
		public ObservableConcurrentCollection<FileEntry> RecentFiles
338
		{
339
			get { return _recentFiles; }
340
		}
341

    
342

    
343
		private string _statusIcon="../Images/Pithos.ico";
344
		public string StatusIcon
345
		{
346
			get { return _statusIcon; }
347
			set
348
			{
349
				//TODO: Ensure all status icons use the Pithos logo
350
				_statusIcon = value;
351
				NotifyOfPropertyChange(() => StatusIcon);
352
			}
353
		}
354

    
355
		#endregion
356

    
357
		#region Commands
358

    
359
        public void ShowPreferences()
360
        {
361
            ShowPreferences(null);
362
        }
363

    
364
		public void ShowPreferences(string currentTab)
365
		{
366
			//Settings.Reload();
367
		    var preferences = new PreferencesViewModel(_windowManager, _events, this, Settings,currentTab);
368
		    _windowManager.ShowDialog(preferences);
369
			
370
		}
371

    
372
		public void AboutPithos()
373
		{
374
			var about = new AboutViewModel();
375
			_windowManager.ShowWindow(about);
376
		}
377

    
378
		public void SendFeedback()
379
		{
380
			var feedBack =  IoC.Get<FeedbackViewModel>();
381
			_windowManager.ShowWindow(feedBack);
382
		}
383

    
384
		//public PithosCommand OpenPithosFolderCommand { get; private set; }
385

    
386
		public void OpenPithosFolder()
387
		{
388
			var account = Settings.Accounts.FirstOrDefault(acc => acc.IsActive);
389
			if (account == null)
390
				return;
391
			Process.Start(account.RootPath);
392
		}
393

    
394
		public void OpenPithosFolder(AccountInfo account)
395
		{
396
			Process.Start(account.AccountPath);
397
		}
398

    
399
		
400
/*
401
		public void GoToSite()
402
		{            
403
			var site = Properties.Settings.Default.PithosSite;
404
			Process.Start(site);            
405
		}
406
*/
407

    
408
		public void GoToSite(AccountInfo account)
409
		{
410
			Process.Start(account.SiteUri);
411
		}
412

    
413
        /// <summary>
414
        /// Open an explorer window to the target path's directory
415
        /// and select the file
416
        /// </summary>
417
        /// <param name="entry"></param>
418
        public void GoToFile(FileEntry entry)
419
        {
420
            var fullPath = entry.FullPath;
421
            if (!File.Exists(fullPath) && !Directory.Exists(fullPath))
422
                return;
423
            Process.Start("explorer.exe","/select, " + fullPath);
424
        }
425

    
426
        public void OpenLogPath()
427
        {
428
            var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
429
            var pithosDataPath = Path.Combine(appDataPath, "GRNET");
430

    
431
            Process.Start(pithosDataPath);
432
        }
433
        
434
        public void ShowFileProperties()
435
		{
436
			var account = Settings.Accounts.First(acc => acc.IsActive);            
437
			var dir = new DirectoryInfo(account.RootPath + @"\pithos");
438
			var files=dir.GetFiles();
439
			var r=new Random();
440
			var idx=r.Next(0, files.Length);
441
			ShowFileProperties(files[idx].FullName);            
442
		}
443

    
444
		public void ShowFileProperties(string filePath)
445
		{
446
			if (String.IsNullOrWhiteSpace(filePath))
447
				throw new ArgumentNullException("filePath");
448
			if (!File.Exists(filePath) && !Directory.Exists(filePath))
449
				throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath");
450
			Contract.EndContractBlock();
451

    
452
			var pair=(from monitor in  Monitors
453
							   where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)
454
								   select monitor).FirstOrDefault();
455
			var accountMonitor = pair.Value;
456

    
457
			if (accountMonitor == null)
458
				return;
459

    
460
			var infoTask=Task.Factory.StartNew(()=>accountMonitor.GetObjectInfo(filePath));
461

    
462
			
463

    
464
			var fileProperties = new FilePropertiesViewModel(this, infoTask,filePath);
465
			_windowManager.ShowWindow(fileProperties);
466
		} 
467
		
468
		public void ShowContainerProperties()
469
		{
470
			var account = Settings.Accounts.First(acc => acc.IsActive);            
471
			var dir = new DirectoryInfo(account.RootPath);
472
			var fullName = (from folder in dir.EnumerateDirectories()
473
							where (folder.Attributes & FileAttributes.Hidden) == 0
474
							select folder.FullName).First();
475
			ShowContainerProperties(fullName);            
476
		}
477

    
478
		public void ShowContainerProperties(string filePath)
479
		{
480
			if (String.IsNullOrWhiteSpace(filePath))
481
				throw new ArgumentNullException("filePath");
482
			if (!Directory.Exists(filePath))
483
				throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath");
484
			Contract.EndContractBlock();
485

    
486
			var pair=(from monitor in  Monitors
487
							   where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)
488
								   select monitor).FirstOrDefault();
489
			var accountMonitor = pair.Value;            
490
			var info = accountMonitor.GetContainerInfo(filePath);
491

    
492
			
493

    
494
			var containerProperties = new ContainerPropertiesViewModel(this, info,filePath);
495
			_windowManager.ShowWindow(containerProperties);
496
		}
497

    
498
		public void SynchNow()
499
		{
500
			_pollAgent.SynchNow();
501
		}
502

    
503
		public ObjectInfo RefreshObjectInfo(ObjectInfo currentInfo)
504
		{
505
			if (currentInfo==null)
506
				throw new ArgumentNullException("currentInfo");
507
			Contract.EndContractBlock();
508

    
509
			var monitor = Monitors[currentInfo.Account];
510
			var newInfo=monitor.CloudClient.GetObjectInfo(currentInfo.Account, currentInfo.Container, currentInfo.Name);
511
			return newInfo;
512
		}
513

    
514
		public ContainerInfo RefreshContainerInfo(ContainerInfo container)
515
		{
516
			if (container == null)
517
				throw new ArgumentNullException("container");
518
			Contract.EndContractBlock();
519

    
520
			var monitor = Monitors[container.Account];
521
			var newInfo = monitor.CloudClient.GetContainerInfo(container.Account, container.Name);
522
			return newInfo;
523
		}
524

    
525

    
526
		public void ToggleSynching()
527
		{
528
			bool isPaused=false;
529
			foreach (var pair in Monitors)
530
			{
531
				var monitor = pair.Value;
532
				monitor.Pause = !monitor.Pause;
533
				isPaused = monitor.Pause;
534
			}
535

    
536
			PauseSyncCaption = isPaused ? "Resume syncing" : "Pause syncing";
537
			var iconKey = isPaused? "TraySyncPaused" : "TrayInSynch";
538
			StatusIcon = String.Format(@"../Images/{0}.ico", iconKey);
539
		}
540

    
541
		public void ExitPithos()
542
		{
543
			foreach (var pair in Monitors)
544
			{
545
				var monitor = pair.Value;
546
				monitor.Stop();
547
			}
548

    
549
			((Window)GetView()).Close();
550
		}
551
		#endregion
552

    
553

    
554
		private readonly Dictionary<PithosStatus, StatusInfo> _iconNames = new List<StatusInfo>
555
			{
556
				new StatusInfo(PithosStatus.InSynch, "All files up to date", "TrayInSynch"),
557
				new StatusInfo(PithosStatus.Syncing, "Syncing Files", "TraySynching"),
558
				new StatusInfo(PithosStatus.SyncPaused, "Sync Paused", "TraySyncPaused")
559
			}.ToDictionary(s => s.Status);
560

    
561
		readonly IWindowManager _windowManager;
562
		
563

    
564
		///<summary>
565
		/// Updates the visual status indicators of the application depending on status changes, e.g. icon, stat		
566
		///</summary>
567
		public void UpdateStatus()
568
		{
569
			var pithosStatus = _statusChecker.GetPithosStatus();
570

    
571
			if (_iconNames.ContainsKey(pithosStatus))
572
			{
573
				var info = _iconNames[pithosStatus];
574
				StatusIcon = String.Format(@"../Images/{0}.ico", info.IconName);
575

    
576

    
577

    
578
				StatusMessage = String.Format("Pithos {0}\r\n{1}", _fileVersion.Value.FileVersion,info.StatusText);
579
			}
580
			
581
			//_events.Publish(new Notification { Title = "Start", Message = "Start Monitoring", Level = TraceLevel.Info});
582
		}
583

    
584

    
585
	   
586
		private Task StartMonitor(PithosMonitor monitor,int retries=0)
587
		{
588
			return Task.Factory.StartNew(() =>
589
			{
590
				using (log4net.ThreadContext.Stacks["Monitor"].Push("Start"))
591
				{
592
					try
593
					{
594
						Log.InfoFormat("Start Monitoring {0}", monitor.UserName);
595

    
596
						monitor.Start();
597
					}
598
					catch (WebException exc)
599
					{
600
						if (AbandonRetry(monitor, retries))
601
							return;
602

    
603
						HttpStatusCode statusCode =HttpStatusCode.OK;
604
						var response = exc.Response as HttpWebResponse;
605
						if(response!=null)
606
							statusCode = response.StatusCode;
607

    
608
						switch (statusCode)
609
						{
610
							case HttpStatusCode.Unauthorized:
611
								var message = String.Format("API Key Expired for {0}. Starting Renewal",
612
															monitor.UserName);
613
								Log.Error(message, exc);
614
						        var account = Settings.Accounts.Find(acc => acc.AccountName == monitor.UserName);                                
615
						        account.IsExpired = true;
616
                                Notify(new ExpirationNotification(account));
617
								//TryAuthorize(monitor.UserName, retries).Wait();
618
								break;
619
							case HttpStatusCode.ProxyAuthenticationRequired:
620
								TryAuthenticateProxy(monitor,retries);
621
								break;
622
							default:
623
								TryLater(monitor, exc, retries);
624
								break;
625
						}
626
					}
627
					catch (Exception exc)
628
					{
629
						if (AbandonRetry(monitor, retries)) 
630
							return;
631

    
632
						TryLater(monitor,exc,retries);
633
					}
634
				}
635
			});
636
		}
637

    
638
		private void TryAuthenticateProxy(PithosMonitor monitor,int retries)
639
		{
640
			Execute.OnUIThread(() =>
641
								   {                                       
642
									   var proxyAccount = IoC.Get<ProxyAccountViewModel>();
643
										proxyAccount.Settings = Settings;
644
									   if (true != _windowManager.ShowDialog(proxyAccount)) 
645
										   return;
646
									   StartMonitor(monitor, retries);
647
									   NotifyOfPropertyChange(() => Accounts);
648
								   });
649
		}
650

    
651
		private bool AbandonRetry(PithosMonitor monitor, int retries)
652
		{
653
			if (retries > 1)
654
			{
655
				var message = String.Format("Monitoring of account {0} has failed too many times. Will not retry",
656
											monitor.UserName);
657
				_events.Publish(new Notification
658
									{Title = "Account monitoring failed", Message = message, Level = TraceLevel.Error});
659
				return true;
660
			}
661
			return false;
662
		}
663

    
664

    
665
	    private void TryLater(PithosMonitor monitor, Exception exc,int retries)
666
		{
667
			var message = String.Format("An exception occured. Can't start monitoring\nWill retry in 10 seconds");
668
			Task.Factory.StartNewDelayed(10000, () => StartMonitor(monitor,retries+1));
669
			_events.Publish(new Notification
670
								{Title = "Error", Message = message, Level = TraceLevel.Error});
671
			Log.Error(message, exc);
672
		}
673

    
674

    
675
		public void NotifyChange(string status, TraceLevel level=TraceLevel.Info)
676
		{
677
			StatusMessage = status;
678
			
679
			_events.Publish(new Notification { Title = "Pithos", Message = status, Level = level });
680
		}
681

    
682
		public void NotifyChangedFile(string filePath)
683
		{
684
            if (RecentFiles.Any(e => e.FullPath == filePath))
685
                return;
686
            
687
			IProducerConsumerCollection<FileEntry> files=RecentFiles;
688
			FileEntry popped;
689
			while (files.Count > 5)
690
				files.TryTake(out popped);
691
            var entry = new FileEntry { FullPath = filePath };
692
			files.TryAdd(entry);
693
		}
694

    
695
		public void NotifyAccount(AccountInfo account)
696
		{
697
			if (account== null)
698
				return;
699
			//TODO: What happens to an existing account whose Token has changed?
700
			account.SiteUri= String.Format("{0}/ui/?token={1}&user={2}",
701
				account.SiteUri, Uri.EscapeDataString(account.Token),
702
				Uri.EscapeDataString(account.UserName));
703

    
704
			if (Accounts.All(item => item.UserName != account.UserName))
705
				Accounts.TryAdd(account);
706

    
707
		}
708

    
709
		public void NotifyConflicts(IEnumerable<FileSystemInfo> conflictFiles, string message)
710
		{
711
			if (conflictFiles == null)
712
				return;
713
		    //Convert to list to avoid multiple iterations
714
            var files = conflictFiles.ToList();
715
			if (files.Count==0)
716
				return;
717

    
718
			UpdateStatus();
719
			//TODO: Create a more specific message. For now, just show a warning
720
			NotifyForFiles(files,message,TraceLevel.Warning);
721

    
722
		}
723

    
724
		public void NotifyForFiles(IEnumerable<FileSystemInfo> files, string message,TraceLevel level=TraceLevel.Info)
725
		{
726
			if (files == null)
727
				return;
728
			if (!files.Any())
729
				return;
730

    
731
			StatusMessage = message;
732

    
733
			_events.Publish(new Notification { Title = "Pithos", Message = message, Level = level});
734
		}
735

    
736
		public void Notify(Notification notification)
737
		{
738
			_events.Publish(notification);
739
		}
740

    
741

    
742
		public void RemoveMonitor(string accountName)
743
		{
744
			if (String.IsNullOrWhiteSpace(accountName))
745
				return;
746

    
747
			var accountInfo=_accounts.FirstOrDefault(account => account.UserName == accountName);
748
            if (accountInfo != null)
749
            {
750
                _accounts.TryRemove(accountInfo);
751
                _pollAgent.RemoveAccount(accountInfo);
752
            }
753

    
754
		    PithosMonitor monitor;
755
			if (Monitors.TryRemove(accountName, out monitor))
756
			{
757
				monitor.Stop();
758
                //TODO: Also remove any pending actions for this account
759
                //from the network queue                
760
			}
761
		}
762

    
763
		public void RefreshOverlays()
764
		{
765
			foreach (var pair in Monitors)
766
			{
767
				var monitor = pair.Value;
768

    
769
				var path = monitor.RootPath;
770

    
771
				if (String.IsNullOrWhiteSpace(path))
772
					continue;
773

    
774
				if (!Directory.Exists(path) && !File.Exists(path))
775
					continue;
776

    
777
				IntPtr pathPointer = Marshal.StringToCoTaskMemAuto(path);
778

    
779
				try
780
				{
781
					NativeMethods.SHChangeNotify(HChangeNotifyEventID.SHCNE_UPDATEITEM,
782
												 HChangeNotifyFlags.SHCNF_PATHW | HChangeNotifyFlags.SHCNF_FLUSHNOWAIT,
783
												 pathPointer, IntPtr.Zero);
784
				}
785
				finally
786
				{
787
					Marshal.FreeHGlobal(pathPointer);
788
				}
789
			}
790
		}
791

    
792
		#region Event Handlers
793
		
794
		public void Handle(SelectiveSynchChanges message)
795
		{
796
			var accountName = message.Account.AccountName;
797
			PithosMonitor monitor;
798
			if (_monitors.TryGetValue(accountName, out monitor))
799
			{
800
				monitor.SetSelectivePaths(message.Uris,message.Added,message.Removed);
801

    
802
			}
803
			
804
		}
805

    
806

    
807
		private bool _pollStarted;
808

    
809
		//SMELL: Doing so much work for notifications in the shell is wrong
810
		//The notifications should be moved to their own view/viewmodel pair
811
		//and different templates should be used for different message types
812
		//This will also allow the addition of extra functionality, eg. actions
813
		//
814
		public void Handle(Notification notification)
815
		{
816
			UpdateStatus();
817

    
818
			if (!Settings.ShowDesktopNotifications)
819
				return;
820

    
821
			if (notification is PollNotification)
822
			{
823
				_pollStarted = true;
824
				return;
825
			}
826
			if (notification is CloudNotification)
827
			{
828
				if (!_pollStarted) 
829
					return;
830
				_pollStarted= false;
831
				notification.Title = "Pithos";
832
				notification.Message = "Start Synchronisation";
833
			}
834

    
835
			if (String.IsNullOrWhiteSpace(notification.Message) && String.IsNullOrWhiteSpace(notification.Title))
836
				return;
837

    
838
			BalloonIcon icon;
839
			switch (notification.Level)
840
			{
841
				case TraceLevel.Error:
842
					icon = BalloonIcon.Error;
843
					break;
844
				case TraceLevel.Info:
845
				case TraceLevel.Verbose:
846
					icon = BalloonIcon.Info;
847
					break;
848
				case TraceLevel.Warning:
849
					icon = BalloonIcon.Warning;
850
					break;
851
				default:
852
					icon = BalloonIcon.None;
853
					break;
854
			}
855
			
856
			if (Settings.ShowDesktopNotifications)
857
			{
858
				var tv = (ShellView) GetView();
859
			    System.Action clickAction = null;
860
                if (notification is ExpirationNotification)
861
                {
862
                    clickAction = ()=>ShowPreferences("AccountTab");
863
                }
864
				var balloon=new PithosBalloon{Title=notification.Title,Message=notification.Message,Icon=icon,ClickAction=clickAction};
865
				tv.TaskbarView.ShowCustomBalloon(balloon,PopupAnimation.Fade,4000);
866
//				tv.TaskbarView.ShowBalloonTip(notification.Title, notification.Message, icon);
867
			}
868
		}
869
		#endregion
870

    
871
		public void Handle(ShowFilePropertiesEvent message)
872
		{
873
			if (message == null)
874
				throw new ArgumentNullException("message");
875
			if (String.IsNullOrWhiteSpace(message.FileName) )
876
				throw new ArgumentException("message");
877
			Contract.EndContractBlock();
878

    
879
			var fileName = message.FileName;
880
			//TODO: Display file properties for non-container folders
881
			if (File.Exists(fileName))
882
				//Retrieve the full name with exact casing. Pithos names are case sensitive				
883
				ShowFileProperties(FileInfoExtensions.GetProperFilePathCapitalization(fileName));
884
			else if (Directory.Exists(fileName))
885
				//Retrieve the full name with exact casing. Pithos names are case sensitive
886
			{
887
				var path = FileInfoExtensions.GetProperDirectoryCapitalization(fileName);
888
				if (IsContainer(path))
889
					ShowContainerProperties(path);
890
				else
891
					ShowFileProperties(path);
892
			}
893
		}
894

    
895
		private bool IsContainer(string path)
896
		{
897
			var matchingFolders = from account in _accounts
898
								  from rootFolder in Directory.GetDirectories(account.AccountPath)
899
								  where rootFolder.Equals(path, StringComparison.InvariantCultureIgnoreCase)
900
								  select rootFolder;
901
			return matchingFolders.Any();
902
		}
903

    
904
		public FileStatus GetFileStatus(string localFileName)
905
		{
906
			if (String.IsNullOrWhiteSpace(localFileName))
907
				throw new ArgumentNullException("localFileName");
908
			Contract.EndContractBlock();
909
			
910
			var statusKeeper = IoC.Get<IStatusKeeper>();
911
			var status=statusKeeper.GetFileStatus(localFileName);
912
			return status;
913
		}
914
	}
915
}