Statistics
| Branch: | Revision:

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

History | View | Annotate | Download (30.6 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 AppLimit.NetSparkle;
54
using Caliburn.Micro;
55
using Hardcodet.Wpf.TaskbarNotification;
56
using Pithos.Client.WPF.Configuration;
57
using Pithos.Client.WPF.FileProperties;
58
using Pithos.Client.WPF.Preferences;
59
using Pithos.Client.WPF.SelectiveSynch;
60
using Pithos.Client.WPF.Services;
61
using Pithos.Client.WPF.Shell;
62
using Pithos.Core;
63
using Pithos.Core.Agents;
64
using Pithos.Interfaces;
65
using System;
66
using System.Collections.Generic;
67
using System.Linq;
68
using Pithos.Network;
69
using StatusService = Pithos.Client.WPF.Services.StatusService;
70

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

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

    
92
		//The Status Checker provides the current synch state
93
		//TODO: Could we remove the status checker and use events in its place?
94
		private readonly IStatusChecker _statusChecker;
95
		private readonly IEventAggregator _events;
96

    
97
		public PithosSettings Settings { get; private set; }
98

    
99

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

    
115

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

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

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

    
128
	    private readonly PollAgent _pollAgent;
129

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

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

    
150
			    _pollAgent = pollAgent;
151
				Settings = settings;
152

    
153
				Proxy.SetFromSettings(settings);
154

    
155
                StatusMessage = Settings.Accounts.Count==0 
156
                    ? "No Accounts added. Please add an account" 
157
                    : "Starting";
158

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

    
171
			}
172
			catch (Exception exc)
173
			{
174
				Log.Error("Error while starting the ShellViewModel",exc);
175
				throw;
176
			}
177

    
178
		}
179

    
180

    
181
		protected override void OnActivate()
182
		{
183
			base.OnActivate();
184

    
185
            _sparkle = new Sparkle(Settings.UpdateUrl);
186
            _sparkle.updateDetected += OnUpgradeDetected;
187
            _sparkle.ShowDiagnosticWindow = Settings.UpdateDiagnostics;
188

    
189
            //Must delay opening the upgrade window
190
            //to avoid Windows Messages sent by the TaskbarIcon
191
            TaskEx.Delay(5000).ContinueWith(_=>
192
                Execute.OnUIThread(()=> _sparkle.StartLoop(true,Settings.UpdateForceCheck,Settings.UpdateCheckInterval)));
193

    
194

    
195
			StartMonitoring();                    
196
		}
197

    
198
	    private void OnUpgradeDetected(object sender, UpdateDetectedEventArgs e)
199
	    {
200
	        Log.InfoFormat("Update detected {0}",e.LatestVersion);
201
	    }
202

    
203
        public void CheckForUpgrade()
204
        {
205
            Log.Error("Test Error message");
206
            _sparkle.StopLoop();
207
            _sparkle.Dispose();
208
            _sparkle=new Sparkle(Settings.UpdateUrl);
209
            _sparkle.StartLoop(true,true,Settings.UpdateCheckInterval);
210
        }
211

    
212
	    private async void StartMonitoring()
213
		{
214
			try
215
			{
216
				var accounts = Settings.Accounts.Select(MonitorAccount);
217
				await TaskEx.WhenAll(accounts);
218
				_statusService = StatusService.Start();
219

    
220
/*
221
				foreach (var account in Settings.Accounts)
222
				{
223
					await MonitorAccount(account);
224
				}
225
*/
226
				
227
			}
228
			catch (AggregateException exc)
229
			{
230
				exc.Handle(e =>
231
				{
232
					Log.Error("Error while starting monitoring", e);
233
					return true;
234
				});
235
				throw;
236
			}
237
		}
238

    
239
		protected override void OnDeactivate(bool close)
240
		{
241
			base.OnDeactivate(close);
242
			if (close)
243
			{
244
				StatusService.Stop(_statusService);
245
				_statusService = null;
246
			}
247
		}
248

    
249
		public Task MonitorAccount(AccountSettings account)
250
		{
251
			return Task.Factory.StartNew(() =>
252
			{                                                
253
				PithosMonitor monitor;
254
				var accountName = account.AccountName;
255

    
256
				if (_monitors.TryGetValue(accountName, out monitor))
257
				{
258
					//If the account is active
259
                    if (account.IsActive)
260
                    {
261
                        //The Api Key may have changed throuth the Preferences dialog
262
                        monitor.ApiKey = account.ApiKey;
263
						Debug.Assert(monitor.StatusNotification == this,"An existing monitor should already have a StatusNotification service object");
264
                        monitor.RootPath = account.RootPath;
265
                        //Start the monitor. It's OK to start an already started monitor,
266
                        //it will just ignore the call                        
267
                        StartMonitor(monitor).Wait();
268
                    }
269
                    else
270
                    {
271
                        //If the account is inactive
272
                        //Stop and remove the monitor
273
                        RemoveMonitor(accountName);
274
                    }
275
					return;
276
				}
277

    
278
				
279
				//Create a new monitor/ Can't use MEF here, it would return a single instance for all monitors
280
				monitor = new PithosMonitor
281
							  {
282
								  UserName = accountName,
283
								  ApiKey = account.ApiKey,                                  
284
								  StatusNotification = this,
285
								  RootPath = account.RootPath
286
							  };
287
				//PithosMonitor uses MEF so we need to resolve it
288
				IoC.BuildUp(monitor);
289

    
290
				monitor.AuthenticationUrl = account.ServerUrl;
291

    
292
				_monitors[accountName] = monitor;
293

    
294
				if (account.IsActive)
295
				{
296
					//Don't start a monitor if it doesn't have an account and ApiKey
297
					if (String.IsNullOrWhiteSpace(monitor.UserName) ||
298
						String.IsNullOrWhiteSpace(monitor.ApiKey))
299
						return;
300
					StartMonitor(monitor);
301
				}
302
			});
303
		}
304

    
305

    
306
		protected override void OnViewLoaded(object view)
307
		{
308
			UpdateStatus();
309
			var window = (Window)view;            
310
			TaskEx.Delay(1000).ContinueWith(t => Execute.OnUIThread(window.Hide));
311
			base.OnViewLoaded(view);
312
		}
313

    
314

    
315
		#region Status Properties
316

    
317
		private string _statusMessage;
318
		public string StatusMessage
319
		{
320
			get { return _statusMessage; }
321
			set
322
			{
323
				_statusMessage = value;
324
				NotifyOfPropertyChange(() => StatusMessage);
325
			}
326
		}
327

    
328
		private readonly ObservableConcurrentCollection<AccountInfo> _accounts = new ObservableConcurrentCollection<AccountInfo>();
329
		public ObservableConcurrentCollection<AccountInfo> Accounts
330
		{
331
			get { return _accounts; }
332
		}
333

    
334
		public bool HasAccounts
335
		{
336
			get { return _accounts.Count > 0; }
337
		}
338

    
339

    
340
		public string OpenFolderCaption
341
		{
342
			get
343
			{
344
				return (_accounts.Count == 0)
345
						? "No Accounts Defined"
346
						: "Open Pithos Folder";
347
			}
348
		}
349

    
350
		private string _pauseSyncCaption="Pause Synching";
351
		public string PauseSyncCaption
352
		{
353
			get { return _pauseSyncCaption; }
354
			set
355
			{
356
				_pauseSyncCaption = value;
357
				NotifyOfPropertyChange(() => PauseSyncCaption);
358
			}
359
		}
360

    
361
		private readonly ObservableConcurrentCollection<FileEntry> _recentFiles = new ObservableConcurrentCollection<FileEntry>();
362
		public ObservableConcurrentCollection<FileEntry> RecentFiles
363
		{
364
			get { return _recentFiles; }
365
		}
366

    
367

    
368
		private string _statusIcon="../Images/Pithos.ico";
369
		public string StatusIcon
370
		{
371
			get { return _statusIcon; }
372
			set
373
			{
374
				//TODO: Ensure all status icons use the Pithos logo
375
				_statusIcon = value;
376
				NotifyOfPropertyChange(() => StatusIcon);
377
			}
378
		}
379

    
380
		#endregion
381

    
382
		#region Commands
383

    
384
        public void ShowPreferences()
385
        {
386
            ShowPreferences(null);
387
        }
388

    
389
		public void ShowPreferences(string currentTab)
390
		{
391
			//Settings.Reload();
392
		    var preferences = new PreferencesViewModel(_windowManager, _events, this, Settings,currentTab);
393
		    _windowManager.ShowDialog(preferences);
394
			
395
		}
396

    
397
		public void AboutPithos()
398
		{
399
			var about = new AboutViewModel();
400
			_windowManager.ShowWindow(about);
401
		}
402

    
403
		public void SendFeedback()
404
		{
405
			var feedBack =  IoC.Get<FeedbackViewModel>();
406
			_windowManager.ShowWindow(feedBack);
407
		}
408

    
409
		//public PithosCommand OpenPithosFolderCommand { get; private set; }
410

    
411
		public void OpenPithosFolder()
412
		{
413
			var account = Settings.Accounts.FirstOrDefault(acc => acc.IsActive);
414
			if (account == null)
415
				return;
416
			Process.Start(account.RootPath);
417
		}
418

    
419
		public void OpenPithosFolder(AccountInfo account)
420
		{
421
			Process.Start(account.AccountPath);
422
		}
423

    
424
		
425
/*
426
		public void GoToSite()
427
		{            
428
			var site = Properties.Settings.Default.PithosSite;
429
			Process.Start(site);            
430
		}
431
*/
432

    
433
		public void GoToSite(AccountInfo account)
434
		{
435
		    var uri = account.SiteUri.Replace("http://","https://");            
436
		    Process.Start(uri);
437
		}
438

    
439
        public void ShowMiniStatus()
440
        {
441
            var model=IoC.Get<MiniStatusViewModel>();
442
            model.Shell = this;
443
            _windowManager.ShowWindow(model);
444
        }
445

    
446
	    /// <summary>
447
        /// Open an explorer window to the target path's directory
448
        /// and select the file
449
        /// </summary>
450
        /// <param name="entry"></param>
451
        public void GoToFile(FileEntry entry)
452
        {
453
            var fullPath = entry.FullPath;
454
            if (!File.Exists(fullPath) && !Directory.Exists(fullPath))
455
                return;
456
            Process.Start("explorer.exe","/select, " + fullPath);
457
        }
458

    
459
        public void OpenLogPath()
460
        {
461
            var pithosDataPath = PithosSettings.PithosDataPath;
462

    
463
            Process.Start(pithosDataPath);
464
        }
465
        
466
        public void ShowFileProperties()
467
		{
468
			var account = Settings.Accounts.First(acc => acc.IsActive);            
469
			var dir = new DirectoryInfo(account.RootPath + @"\pithos");
470
			var files=dir.GetFiles();
471
			var r=new Random();
472
			var idx=r.Next(0, files.Length);
473
			ShowFileProperties(files[idx].FullName);            
474
		}
475

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

    
484
			var pair=(from monitor in  Monitors
485
							   where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)
486
								   select monitor).FirstOrDefault();
487
			var accountMonitor = pair.Value;
488

    
489
			if (accountMonitor == null)
490
				return;
491

    
492
			var infoTask=Task.Factory.StartNew(()=>accountMonitor.GetObjectInfo(filePath));
493

    
494
			
495

    
496
			var fileProperties = new FilePropertiesViewModel(this, infoTask,filePath);
497
			_windowManager.ShowWindow(fileProperties);
498
		} 
499
		
500
		public void ShowContainerProperties()
501
		{
502
			var account = Settings.Accounts.First(acc => acc.IsActive);            
503
			var dir = new DirectoryInfo(account.RootPath);
504
			var fullName = (from folder in dir.EnumerateDirectories()
505
							where (folder.Attributes & FileAttributes.Hidden) == 0
506
							select folder.FullName).First();
507
			ShowContainerProperties(fullName);            
508
		}
509

    
510
		public void ShowContainerProperties(string filePath)
511
		{
512
			if (String.IsNullOrWhiteSpace(filePath))
513
				throw new ArgumentNullException("filePath");
514
			if (!Directory.Exists(filePath))
515
				throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath");
516
			Contract.EndContractBlock();
517

    
518
			var pair=(from monitor in  Monitors
519
							   where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)
520
								   select monitor).FirstOrDefault();
521
			var accountMonitor = pair.Value;            
522
			var info = accountMonitor.GetContainerInfo(filePath);
523

    
524
			
525

    
526
			var containerProperties = new ContainerPropertiesViewModel(this, info,filePath);
527
			_windowManager.ShowWindow(containerProperties);
528
		}
529

    
530
		public void SynchNow()
531
		{
532
			_pollAgent.SynchNow();
533
		}
534

    
535
		public ObjectInfo RefreshObjectInfo(ObjectInfo currentInfo)
536
		{
537
			if (currentInfo==null)
538
				throw new ArgumentNullException("currentInfo");
539
			Contract.EndContractBlock();
540

    
541
			var monitor = Monitors[currentInfo.Account];
542
			var newInfo=monitor.CloudClient.GetObjectInfo(currentInfo.Account, currentInfo.Container, currentInfo.Name);
543
			return newInfo;
544
		}
545

    
546
		public ContainerInfo RefreshContainerInfo(ContainerInfo container)
547
		{
548
			if (container == null)
549
				throw new ArgumentNullException("container");
550
			Contract.EndContractBlock();
551

    
552
			var monitor = Monitors[container.Account];
553
			var newInfo = monitor.CloudClient.GetContainerInfo(container.Account, container.Name);
554
			return newInfo;
555
		}
556

    
557

    
558
		public void ToggleSynching()
559
		{
560
			bool isPaused=false;
561
			foreach (var pair in Monitors)
562
			{
563
				var monitor = pair.Value;
564
				monitor.Pause = !monitor.Pause;
565
				isPaused = monitor.Pause;
566
			}
567
                        
568

    
569
			PauseSyncCaption = isPaused ? "Resume syncing" : "Pause syncing";
570
			var iconKey = isPaused? "TraySyncPaused" : "TrayInSynch";
571
			StatusIcon = String.Format(@"../Images/{0}.ico", iconKey);
572
		}
573

    
574
		public void ExitPithos()
575
		{
576
			foreach (var pair in Monitors)
577
			{
578
				var monitor = pair.Value;
579
				monitor.Stop();
580
			}
581

    
582
			((Window)GetView()).Close();
583
		}
584
		#endregion
585

    
586

    
587
		private readonly Dictionary<PithosStatus, StatusInfo> _iconNames = new List<StatusInfo>
588
			{
589
				new StatusInfo(PithosStatus.InSynch, "All files up to date", "TrayInSynch"),
590
				new StatusInfo(PithosStatus.PollSyncing, "Polling Files", "TraySynching"),
591
                new StatusInfo(PithosStatus.LocalSyncing, "Syncing Files", "TraySynching"),
592
				new StatusInfo(PithosStatus.SyncPaused, "Sync Paused", "TraySyncPaused")
593
			}.ToDictionary(s => s.Status);
594

    
595
		readonly IWindowManager _windowManager;
596
		
597
        //private int _syncCount=0;
598

    
599

    
600
        private PithosStatus _pithosStatus = PithosStatus.Disconnected;
601

    
602
        public void SetPithosStatus(PithosStatus status)
603
        {
604
            if (_pithosStatus == PithosStatus.LocalSyncing && status == PithosStatus.PollComplete)
605
                return;
606
            if (_pithosStatus == PithosStatus.PollSyncing && status == PithosStatus.LocalComplete)
607
                return;
608
            if (status == PithosStatus.LocalComplete || status == PithosStatus.PollComplete)
609
                _pithosStatus = PithosStatus.InSynch;
610
            else
611
                _pithosStatus = status;
612
            UpdateStatus();
613
        }
614

    
615
        public void SetPithosStatus(PithosStatus status,string message)
616
        {
617
            StatusMessage = message;
618
            SetPithosStatus(status);
619
        }
620

    
621

    
622

    
623
		///<summary>
624
		/// Updates the visual status indicators of the application depending on status changes, e.g. icon, stat		
625
		///</summary>
626
		public void UpdateStatus()
627
		{
628

    
629
			if (_iconNames.ContainsKey(_pithosStatus))
630
			{
631
				var info = _iconNames[_pithosStatus];
632
				StatusIcon = String.Format(@"../Images/{0}.ico", info.IconName);
633
			}
634

    
635
            if (_pithosStatus == PithosStatus.InSynch)
636
                StatusMessage = "All files up to date";
637
		}
638

    
639

    
640
	   
641
		private Task StartMonitor(PithosMonitor monitor,int retries=0)
642
		{
643
			return Task.Factory.StartNew(() =>
644
			{
645
				using (log4net.ThreadContext.Stacks["Monitor"].Push("Start"))
646
				{
647
					try
648
					{
649
						Log.InfoFormat("Start Monitoring {0}", monitor.UserName);
650

    
651
						monitor.Start();
652
					}
653
					catch (WebException exc)
654
					{
655
						if (AbandonRetry(monitor, retries))
656
							return;
657

    
658
						HttpStatusCode statusCode =HttpStatusCode.OK;
659
						var response = exc.Response as HttpWebResponse;
660
						if(response!=null)
661
							statusCode = response.StatusCode;
662

    
663
						switch (statusCode)
664
						{
665
							case HttpStatusCode.Unauthorized:
666
								var message = String.Format("API Key Expired for {0}. Starting Renewal",
667
															monitor.UserName);
668
								Log.Error(message, exc);
669
						        var account = Settings.Accounts.Find(acc => acc.AccountName == monitor.UserName);                                
670
						        account.IsExpired = true;
671
                                Notify(new ExpirationNotification(account));
672
								//TryAuthorize(monitor.UserName, retries).Wait();
673
								break;
674
							case HttpStatusCode.ProxyAuthenticationRequired:
675
								TryAuthenticateProxy(monitor,retries);
676
								break;
677
							default:
678
								TryLater(monitor, exc, retries);
679
								break;
680
						}
681
					}
682
					catch (Exception exc)
683
					{
684
						if (AbandonRetry(monitor, retries)) 
685
							return;
686

    
687
						TryLater(monitor,exc,retries);
688
					}
689
				}
690
			});
691
		}
692

    
693
		private void TryAuthenticateProxy(PithosMonitor monitor,int retries)
694
		{
695
			Execute.OnUIThread(() =>
696
								   {                                       
697
									   var proxyAccount = IoC.Get<ProxyAccountViewModel>();
698
										proxyAccount.Settings = Settings;
699
									   if (true != _windowManager.ShowDialog(proxyAccount)) 
700
										   return;
701
									   StartMonitor(monitor, retries);
702
									   NotifyOfPropertyChange(() => Accounts);
703
								   });
704
		}
705

    
706
		private bool AbandonRetry(PithosMonitor monitor, int retries)
707
		{
708
			if (retries > 1)
709
			{
710
				var message = String.Format("Monitoring of account {0} has failed too many times. Will not retry",
711
											monitor.UserName);
712
				_events.Publish(new Notification
713
									{Title = "Account monitoring failed", Message = message, Level = TraceLevel.Error});
714
				return true;
715
			}
716
			return false;
717
		}
718

    
719

    
720
	    private void TryLater(PithosMonitor monitor, Exception exc,int retries)
721
		{
722
			var message = String.Format("An exception occured. Can't start monitoring\nWill retry in 10 seconds");
723
			Task.Factory.StartNewDelayed(10000, () => StartMonitor(monitor,retries+1));
724
			_events.Publish(new Notification
725
								{Title = "Error", Message = message, Level = TraceLevel.Error});
726
			Log.Error(message, exc);
727
		}
728

    
729

    
730
		public void NotifyChange(string status, TraceLevel level=TraceLevel.Info)
731
		{
732
			StatusMessage = status;
733
			
734
			_events.Publish(new Notification { Title = "Pithos", Message = status, Level = level });
735
		}
736

    
737
		public void NotifyChangedFile(string filePath)
738
		{
739
            if (RecentFiles.Any(e => e.FullPath == filePath))
740
                return;
741
            
742
			IProducerConsumerCollection<FileEntry> files=RecentFiles;
743
			FileEntry popped;
744
			while (files.Count > 5)
745
				files.TryTake(out popped);
746
            var entry = new FileEntry { FullPath = filePath };
747
			files.TryAdd(entry);
748
		}
749

    
750
		public void NotifyAccount(AccountInfo account)
751
		{
752
			if (account== null)
753
				return;
754
			//TODO: What happens to an existing account whose Token has changed?
755
			account.SiteUri= String.Format("{0}/ui/?token={1}&user={2}",
756
				account.SiteUri, Uri.EscapeDataString(account.Token),
757
				Uri.EscapeDataString(account.UserName));
758

    
759
			if (Accounts.All(item => item.UserName != account.UserName))
760
				Accounts.TryAdd(account);
761

    
762
		}
763

    
764
		public void NotifyConflicts(IEnumerable<FileSystemInfo> conflictFiles, string message)
765
		{
766
			if (conflictFiles == null)
767
				return;
768
		    //Convert to list to avoid multiple iterations
769
            var files = conflictFiles.ToList();
770
			if (files.Count==0)
771
				return;
772

    
773
			UpdateStatus();
774
			//TODO: Create a more specific message. For now, just show a warning
775
			NotifyForFiles(files,message,TraceLevel.Warning);
776

    
777
		}
778

    
779
		public void NotifyForFiles(IEnumerable<FileSystemInfo> files, string message,TraceLevel level=TraceLevel.Info)
780
		{
781
			if (files == null)
782
				return;
783
			if (!files.Any())
784
				return;
785

    
786
			StatusMessage = message;
787

    
788
			_events.Publish(new Notification { Title = "Pithos", Message = message, Level = level});
789
		}
790

    
791
		public void Notify(Notification notification)
792
		{
793
			_events.Publish(notification);
794
		}
795

    
796

    
797
		public void RemoveMonitor(string accountName)
798
		{
799
			if (String.IsNullOrWhiteSpace(accountName))
800
				return;
801

    
802
			var accountInfo=_accounts.FirstOrDefault(account => account.UserName == accountName);
803
            if (accountInfo != null)
804
            {
805
                _accounts.TryRemove(accountInfo);
806
                _pollAgent.RemoveAccount(accountInfo);
807
            }
808

    
809
		    PithosMonitor monitor;
810
			if (Monitors.TryRemove(accountName, out monitor))
811
			{
812
				monitor.Stop();
813
                //TODO: Also remove any pending actions for this account
814
                //from the network queue                
815
			}
816
		}
817

    
818
		public void RefreshOverlays()
819
		{
820
			foreach (var pair in Monitors)
821
			{
822
				var monitor = pair.Value;
823

    
824
				var path = monitor.RootPath;
825

    
826
				if (String.IsNullOrWhiteSpace(path))
827
					continue;
828

    
829
				if (!Directory.Exists(path) && !File.Exists(path))
830
					continue;
831

    
832
				IntPtr pathPointer = Marshal.StringToCoTaskMemAuto(path);
833

    
834
				try
835
				{
836
					NativeMethods.SHChangeNotify(HChangeNotifyEventID.SHCNE_UPDATEITEM,
837
												 HChangeNotifyFlags.SHCNF_PATHW | HChangeNotifyFlags.SHCNF_FLUSHNOWAIT,
838
												 pathPointer, IntPtr.Zero);
839
				}
840
				finally
841
				{
842
					Marshal.FreeHGlobal(pathPointer);
843
				}
844
			}
845
		}
846

    
847
		#region Event Handlers
848
		
849
		public void Handle(SelectiveSynchChanges message)
850
		{
851
			var accountName = message.Account.AccountName;
852
			PithosMonitor monitor;
853
			if (_monitors.TryGetValue(accountName, out monitor))
854
			{
855
				monitor.SetSelectivePaths(message.Uris,message.Added,message.Removed);
856

    
857
			}
858
			
859
		}
860

    
861

    
862
		private bool _pollStarted;
863
	    private Sparkle _sparkle;
864

    
865
	    //SMELL: Doing so much work for notifications in the shell is wrong
866
		//The notifications should be moved to their own view/viewmodel pair
867
		//and different templates should be used for different message types
868
		//This will also allow the addition of extra functionality, eg. actions
869
		//
870
		public void Handle(Notification notification)
871
		{
872
			UpdateStatus();
873

    
874
			if (!Settings.ShowDesktopNotifications)
875
				return;
876

    
877
			if (notification is PollNotification)
878
			{
879
				_pollStarted = true;
880
				return;
881
			}
882
			if (notification is CloudNotification)
883
			{
884
				if (!_pollStarted) 
885
					return;
886
				_pollStarted= false;
887
				notification.Title = "Pithos";
888
				notification.Message = "Start Synchronisation";
889
			}
890

    
891
		    var deleteNotification = notification as CloudDeleteNotification;
892
            if (deleteNotification != null)
893
            {
894
                StatusMessage = String.Format("Deleted {0}", deleteNotification.Data.Name);
895
                return;
896
            }
897

    
898
		    var progress = notification as ProgressNotification;
899
		    if (progress != null)
900
		    {
901
		        StatusMessage = String.Format("Pithos {0}\r\n{1} {2:p2} of {3} - {4}",
902
		                                      _fileVersion.Value.FileVersion, 
903
                                              progress.Action,
904
		                                      progress.Block/(double)progress.TotalBlocks,
905
		                                      progress.FileSize.ToByteSize(),
906
		                                      progress.FileName);
907
		        return;
908
		    }
909

    
910
		    var info = notification as StatusNotification;
911
            if (info != null)
912
            {
913
                StatusMessage = String.Format("Pithos {0}\r\n{1}",
914
                                              _fileVersion.Value.FileVersion,
915
                                              info.Title);
916
                return;
917
            }
918
			if (String.IsNullOrWhiteSpace(notification.Message) && String.IsNullOrWhiteSpace(notification.Title))
919
				return;
920

    
921
			ShowBalloonFor(notification);
922
		}
923

    
924
	    private void ShowBalloonFor(Notification notification)
925
	    {
926
            Contract.Requires(notification!=null);
927
            
928
            if (!Settings.ShowDesktopNotifications) 
929
                return;
930
            
931
            BalloonIcon icon;
932
	        switch (notification.Level)
933
	        {
934
	            case TraceLevel.Info:
935
	            case TraceLevel.Verbose:
936
	                return;
937
                case TraceLevel.Error:
938
                    icon = BalloonIcon.Error;
939
                    break;
940
                case TraceLevel.Warning:
941
	                icon = BalloonIcon.Warning;
942
	                break;
943
	            default:
944
	                return;
945
	        }
946

    
947
	        var tv = (ShellView) GetView();
948
	        System.Action clickAction = null;
949
	        if (notification is ExpirationNotification)
950
	        {
951
	            clickAction = () => ShowPreferences("AccountTab");
952
	        }
953
	        var balloon = new PithosBalloon
954
	                          {
955
	                              Title = notification.Title,
956
	                              Message = notification.Message,
957
	                              Icon = icon,
958
	                              ClickAction = clickAction
959
	                          };
960
	        tv.TaskbarView.ShowCustomBalloon(balloon, PopupAnimation.Fade, 4000);
961
	    }
962

    
963
	    #endregion
964

    
965
		public void Handle(ShowFilePropertiesEvent message)
966
		{
967
			if (message == null)
968
				throw new ArgumentNullException("message");
969
			if (String.IsNullOrWhiteSpace(message.FileName) )
970
				throw new ArgumentException("message");
971
			Contract.EndContractBlock();
972

    
973
			var fileName = message.FileName;
974
			//TODO: Display file properties for non-container folders
975
			if (File.Exists(fileName))
976
				//Retrieve the full name with exact casing. Pithos names are case sensitive				
977
				ShowFileProperties(FileInfoExtensions.GetProperFilePathCapitalization(fileName));
978
			else if (Directory.Exists(fileName))
979
				//Retrieve the full name with exact casing. Pithos names are case sensitive
980
			{
981
				var path = FileInfoExtensions.GetProperDirectoryCapitalization(fileName);
982
				if (IsContainer(path))
983
					ShowContainerProperties(path);
984
				else
985
					ShowFileProperties(path);
986
			}
987
		}
988

    
989
		private bool IsContainer(string path)
990
		{
991
			var matchingFolders = from account in _accounts
992
								  from rootFolder in Directory.GetDirectories(account.AccountPath)
993
								  where rootFolder.Equals(path, StringComparison.InvariantCultureIgnoreCase)
994
								  select rootFolder;
995
			return matchingFolders.Any();
996
		}
997

    
998
		public FileStatus GetFileStatus(string localFileName)
999
		{
1000
			if (String.IsNullOrWhiteSpace(localFileName))
1001
				throw new ArgumentNullException("localFileName");
1002
			Contract.EndContractBlock();
1003
			
1004
			var statusKeeper = IoC.Get<IStatusKeeper>();
1005
			var status=statusKeeper.GetFileStatus(localFileName);
1006
			return status;
1007
		}
1008

    
1009
	    public void RemoveAccountFromDatabase(AccountSettings account)
1010
	    {
1011
            var statusKeeper = IoC.Get<IStatusKeeper>();
1012
            statusKeeper.ClearFolderStatus(account.RootPath);	        
1013
	    }
1014
	}
1015
}