Statistics
| Branch: | Revision:

root / trunk / Pithos.Client.WPF / Shell / ShellViewModel.cs @ 855fc9c9

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

    
73
namespace Pithos.Client.WPF {
74
	using System.ComponentModel.Composition;
75

    
76
	public class ToggleStatusCommand:ICommand
77
	{
78
	    private readonly ShellViewModel _model;
79
	    public ToggleStatusCommand(ShellViewModel model)
80
	    {
81
	        _model = model;
82
	    }
83
	    public void Execute(object parameter)
84
	    {
85
	        _model.CurrentSyncStatus();
86
	    }
87

    
88
	    public bool CanExecute(object parameter)
89
	    {
90
	        return true;
91
	    }
92

    
93
	    public event EventHandler CanExecuteChanged;
94
	}
95

    
96

    
97
	///<summary>
98
	/// The "shell" of the Pithos application displays the taskbar  icon, menu and notifications.
99
	/// The shell also hosts the status service called by shell extensions to retrieve file info
100
	///</summary>
101
	///<remarks>
102
	/// It is a strange "shell" as its main visible element is an icon instead of a window
103
	/// The shell subscribes to the following events:
104
	/// * Notification:  Raised by components that want to notify the user. Usually displayed in a balloon
105
	/// * SelectiveSynchChanges: Notifies that the user made changes to the selective synch folders for an account. Raised by the Selective Synch dialog. Located here because the monitors are here
106
	/// * ShowFilePropertiesEvent: Raised when a shell command requests the display of the file/container properties dialog
107
	///</remarks>		
108
	//TODO: CODE SMELL Why does the shell handle the SelectiveSynchChanges?
109
    [Export(typeof(IShell)), Export(typeof(ShellViewModel)),Export(typeof(IStatusNotification))]
110
	public class ShellViewModel : Screen, IStatusNotification, IShell,
111
		IHandle<Notification>, IHandle<SelectiveSynchChanges>, IHandle<ShowFilePropertiesEvent>
112
	{
113
		
114
		private readonly IEventAggregator _events;
115

    
116
		public PithosSettings Settings { get; private set; }
117

    
118

    
119
		private readonly ConcurrentDictionary<Uri, PithosMonitor> _monitors = new ConcurrentDictionary<Uri, PithosMonitor>();
120
		///<summary>
121
		/// Dictionary of account monitors, keyed by account
122
		///</summary>
123
		///<remarks>
124
		/// One monitor class is created for each account. The Shell needs access to the monitors to execute start/stop/pause commands,
125
		/// retrieve account and boject info		
126
		///</remarks>
127
		// TODO: Does the Shell REALLY need access to the monitors? Could we achieve the same results with a better design?
128
		// TODO: The monitors should be internal to Pithos.Core, even though exposing them makes coding of the Object and Container windows easier
129
		public ConcurrentDictionary<Uri, PithosMonitor> Monitors
130
		{
131
			get { return _monitors; }
132
		}
133

    
134

    
135
		///<summary>
136
		/// The status service is used by Shell extensions to retrieve file status information
137
		///</summary>
138
		//TODO: CODE SMELL! This is the shell! While hosting in the shell makes executing start/stop commands easier, it is still a smell
139
		private ServiceHost _statusService;
140

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

    
144
        #pragma warning disable 649
145
        [Import]
146
	    private PollAgent _pollAgent;
147

    
148
        [Import]
149
	    private NetworkAgent _networkAgent;
150

    
151
	    [Import]
152
	    public Selectives Selectives { get; set; }
153

    
154
        #pragma warning restore 649
155

    
156
        public ToggleStatusCommand ToggleMiniStatusCommand { get; set; }
157

    
158
	    private MiniStatusViewModel _miniStatus;
159

    
160
	    [Import]
161
        public MiniStatusViewModel MiniStatus
162
	    {
163
	        get { return _miniStatus; }
164
	        set
165
	        {
166
	            _miniStatus = value;
167
	            _miniStatus.Shell = this;
168
	            _miniStatus.Deactivated += (sender, arg) =>
169
	                                           {
170
	                                               _statusVisible = false;
171
                                                   NotifyOfPropertyChange(()=>MiniStatusCaption);
172
	                                           };
173
	        }
174
	    }
175

    
176
	    ///<summary>
177
		/// The Shell depends on MEF to provide implementations for windowManager, events, the status checker service and the settings
178
		///</summary>
179
		///<remarks>
180
		/// The PithosSettings class encapsulates the app's settings to abstract their storage mechanism (App settings, a database or registry)
181
		///</remarks>
182
		[ImportingConstructor]		
183
		public ShellViewModel(IWindowManager windowManager, IEventAggregator events, PithosSettings settings/*,PollAgent pollAgent,NetworkAgent networkAgent*/)
184
		{
185
			try
186
			{
187

    
188
				_windowManager = windowManager;
189
				//CHECK: Caliburn doesn't need explicit command construction
190
				//CurrentSyncStatusCommand = new PithosCommand(OpenPithosFolder);
191
				//The event subst
192
				_events = events;
193
				_events.Subscribe(this);
194

    
195
/*
196
			    _pollAgent = pollAgent;
197
			    _networkAgent = networkAgent;
198
*/
199
				Settings = settings;
200

    
201
				Proxy.SetFromSettings(settings);
202

    
203
                StatusMessage = Settings.Accounts.Count==0 
204
                    ? "No Accounts added\r\nPlease add an account" 
205
                    : "Starting";
206

    
207
				_accounts.CollectionChanged += (sender, e) =>
208
												   {
209
													   NotifyOfPropertyChange(() => OpenFolderCaption);
210
													   NotifyOfPropertyChange(() => HasAccounts);
211
												   };
212

    
213
                SetVersionMessage();
214

    
215
                ToggleMiniStatusCommand=new ToggleStatusCommand(this);
216
			}
217
			catch (Exception exc)
218
			{
219
				Log.Error("Error while starting the ShellViewModel",exc);
220
				throw;
221
			}
222

    
223
		}
224

    
225
	    private void SetVersionMessage()
226
	    {
227
	        Assembly assembly = Assembly.GetExecutingAssembly();
228
	        var fileVersion = FileVersionInfo.GetVersionInfo(assembly.Location);
229
	        VersionMessage = String.Format("Pithos+ {0}", fileVersion.FileVersion);
230
	    }
231

    
232
        public void CurrentSyncStatus()
233
        {
234
            if (Accounts.Count == 0)
235
            {
236
                ShowPreferences("AccountTab");
237
            }
238
            else
239
            {
240
                if (!_statusVisible)
241
                {
242
                    _windowManager.ShowWindow(MiniStatus);
243
                    _statusVisible = true;
244
                }
245
                else
246
                {
247
                    if (MiniStatus.IsActive)
248
                        MiniStatus.TryClose();
249
                    _statusVisible = false;
250
                }
251

    
252
                NotifyOfPropertyChange(() => MiniStatusCaption);
253
            }
254
        }
255

    
256
	    protected override void OnActivate()
257
		{
258
			base.OnActivate();
259

    
260
            InitializeSparkle();
261

    
262
	        //Must delay opening the upgrade window
263
            //to avoid Windows Messages sent by the TaskbarIcon
264
            TaskEx.Delay(5000).ContinueWith(_=>
265
                Execute.OnUIThread(()=> _sparkle.StartLoop(true,Settings.UpdateForceCheck,Settings.UpdateCheckInterval)));
266

    
267

    
268
			StartMonitoring();                    
269
		}
270

    
271

    
272
	    private void OnCheckFinished(object sender, bool updaterequired)
273
	    {
274
            
275
            Log.InfoFormat("Upgrade check finished. Need Upgrade: {0}", updaterequired);
276
            if (_manualUpgradeCheck)
277
            {
278
                _manualUpgradeCheck = false;
279
                if (!updaterequired)
280
                    //Sparkle raises events on a background thread
281
                    Execute.OnUIThread(()=>
282
                        ShowBalloonFor(new Notification{Title="Pithos+ is up to date",Message="You have the latest Pithos+ version. No update is required"}));
283
            }
284
	    }
285

    
286
	    private void OnUpgradeDetected(object sender, UpdateDetectedEventArgs e)
287
	    {            
288
	        Log.InfoFormat("Update detected {0}",e.LatestVersion);
289
	    }
290

    
291
        public void CheckForUpgrade()
292
        {
293
            ShowBalloonFor(new Notification{Title="Checking for upgrades",Message="Contacting the server to retrieve the latest Pithos+ version."});
294
            _sparkle.StopLoop();
295
            _sparkle.updateDetected -= OnUpgradeDetected;
296
            _sparkle.checkLoopFinished -= OnCheckFinished;
297
            _sparkle.Dispose();
298

    
299
            _manualUpgradeCheck = true;
300
            InitializeSparkle();
301
            _sparkle.StartLoop(true,true,Settings.UpdateCheckInterval);
302
        }
303

    
304
        private void InitializeSparkle()
305
        {
306
            _sparkle = new Sparkle(Settings.UpdateUrl);
307
            _sparkle.updateDetected += OnUpgradeDetected;
308
            _sparkle.checkLoopFinished += OnCheckFinished;
309
            _sparkle.ShowDiagnosticWindow = Settings.UpdateDiagnostics;
310
        }
311

    
312
	    private async void StartMonitoring()
313
		{
314
			try
315
			{
316
                if (Settings.IgnoreCertificateErrors)
317
                {
318
                    ServicePointManager.ServerCertificateValidationCallback = (sender, certificate, chain, errors) => true;
319
                }
320
                    
321
				var accounts = Settings.Accounts.Select(MonitorAccount);
322
                await TaskEx.WhenAll(accounts).ConfigureAwait(false);
323
				_statusService = StatusService.Start();
324

    
325
			}
326
			catch (AggregateException exc)
327
			{
328
				exc.Handle(e =>
329
				{
330
					Log.Error("Error while starting monitoring", e);
331
					return true;
332
				});
333
				throw;
334
			}
335
		}
336

    
337
		protected override void OnDeactivate(bool close)
338
		{
339
			base.OnDeactivate(close);
340
			if (close)
341
			{
342
				StatusService.Stop(_statusService);
343
				_statusService = null;
344
			}
345
		}
346

    
347
		public Task MonitorAccount(AccountSettings account)
348
		{
349
			return Task.Factory.StartNew(() =>
350
			{                                                
351
				PithosMonitor monitor;
352
				var accountName = account.AccountName;
353

    
354
			    MigrateFolders(account);
355

    
356
			    Selectives.SetIsSelectiveEnabled(account.AccountKey, account.SelectiveSyncEnabled);
357

    
358
				if (Monitors.TryGetValue(account.AccountKey, out monitor))
359
				{
360
					//If the account is active
361
                    if (account.IsActive)
362
                    {
363
                        //The Api Key may have changed throuth the Preferences dialog
364
                        monitor.ApiKey = account.ApiKey;
365
						Debug.Assert(monitor.StatusNotification == this,"An existing monitor should already have a StatusNotification service object");
366
                        monitor.RootPath = account.RootPath;
367
                        //Start the monitor. It's OK to start an already started monitor,
368
                        //it will just ignore the call                        
369
                        StartMonitor(monitor).Wait();
370
                    }
371
                    else
372
                    {
373
                        //If the account is inactive
374
                        //Stop and remove the monitor
375
                        RemoveMonitor(account.ServerUrl,accountName);
376
                    }
377
					return;
378
				}
379

    
380
				
381
				//Create a new monitor/ Can't use MEF here, it would return a single instance for all monitors
382
				monitor = new PithosMonitor
383
							  {
384
								  UserName = accountName,
385
								  ApiKey = account.ApiKey,                                  
386
								  StatusNotification = this,
387
								  RootPath = account.RootPath
388
							  };
389
				//PithosMonitor uses MEF so we need to resolve it
390
				IoC.BuildUp(monitor);
391

    
392
				monitor.AuthenticationUrl = account.ServerUrl;
393

    
394
				Monitors[account.AccountKey] = monitor;
395

    
396
                if (!Directory.Exists(account.RootPath))
397
                {
398
                    account.IsActive = false;
399
                    Settings.Save();
400
                    Notify(new Notification
401
                    {
402
                        Level = TraceLevel.Error,
403
                        Title = "Missing account folder",
404
                        Message = String.Format("Can't find the root folder for account {0} at {1}. The account was deactivated.\r" +
405
                        "If the account's files were stored in a removable disk, please connect it and reactivate the account", account.AccountName, account.RootPath)
406
                    });
407
                }
408

    
409

    
410
				if (account.IsActive)
411
				{
412
					//Don't start a monitor if it doesn't have an account and ApiKey
413
					if (String.IsNullOrWhiteSpace(monitor.UserName) ||
414
						String.IsNullOrWhiteSpace(monitor.ApiKey))
415
						return;
416
					StartMonitor(monitor);
417
				}
418
			});
419
		}
420

    
421
	    private void MigrateFolders(AccountSettings account)
422
	    {
423
	        var oldOthersFolder=Path.Combine(account.RootPath, FolderConstants.OldOthersFolder);
424
	        var newOthersFolder = Path.Combine(account.RootPath, FolderConstants.OthersFolder);
425
	        var oldFolder = new DirectoryInfo(oldOthersFolder);
426
	        var newFolder = new DirectoryInfo(newOthersFolder);
427

    
428
            if (oldFolder.Exists && !newFolder.Exists)
429
            {
430
                oldFolder.MoveTo(newOthersFolder);
431
            }
432
	    }
433

    
434

    
435
	    protected override void OnViewLoaded(object view)
436
		{
437
			UpdateStatus();
438
			var window = (Window)view;            
439
			TaskEx.Delay(1000).ContinueWith(t => Execute.OnUIThread(window.Hide));
440
			base.OnViewLoaded(view);
441
		}
442

    
443

    
444
		#region Status Properties
445

    
446
		private string _statusMessage;
447
		public string StatusMessage
448
		{
449
			get { return _statusMessage; }
450
			set
451
			{
452
				_statusMessage = value;
453
				NotifyOfPropertyChange(() => StatusMessage);
454
                NotifyOfPropertyChange(() => TooltipMessage);
455
			}
456
		}
457

    
458
        public string VersionMessage { get; set; }
459

    
460
	    public string TooltipMessage
461
	    {
462
	        get
463
	        {
464
	            return String.Format("{0}\r\n{1}",VersionMessage,StatusMessage);
465
	        }
466
	    }
467

    
468
        public string TooltipMiniStatus
469
        {
470
            get
471
            {
472
                return String.Format("{0}\r\n{1}", "Status Window", "Enable / Disable the status window");
473
            }
474
        }
475

    
476
        /*public string ToggleStatusWindowMessage
477
        {
478
            get
479
            {
480
                return String.Format("{0}" + Environment.NewLine + "{1} Toggle Mini Status");
481
            }
482
        }*/
483

    
484
	    private readonly ObservableConcurrentCollection<AccountInfo> _accounts = new ObservableConcurrentCollection<AccountInfo>();
485
		public ObservableConcurrentCollection<AccountInfo> Accounts
486
		{
487
			get { return _accounts; }
488
		}
489

    
490
		public bool HasAccounts
491
		{
492
			get { return _accounts.Count > 0; }
493
		}
494

    
495

    
496
		public string OpenFolderCaption
497
		{
498
			get
499
			{
500
				return (_accounts.Count == 0)
501
						? "No Accounts Defined"
502
						: "Open Pithos Folder";
503
			}
504
		}
505

    
506
		private string _pauseSyncCaption="Pause Synching";
507
		public string PauseSyncCaption
508
		{
509
			get { return _pauseSyncCaption; }
510
			set
511
			{
512
				_pauseSyncCaption = value;
513
				NotifyOfPropertyChange(() => PauseSyncCaption);
514
			}
515
		}
516

    
517
		private readonly ObservableConcurrentCollection<FileEntry> _recentFiles = new ObservableConcurrentCollection<FileEntry>();
518
		public ObservableConcurrentCollection<FileEntry> RecentFiles
519
		{
520
			get { return _recentFiles; }
521
		}
522

    
523

    
524
		private string _statusIcon="../Images/Pithos.ico";
525
		public string StatusIcon
526
		{
527
			get { return _statusIcon; }
528
			set
529
			{
530
				//TODO: Ensure all status icons use the Pithos logo
531
				_statusIcon = value;
532
				NotifyOfPropertyChange(() => StatusIcon);
533
			}
534
		}
535

    
536
		#endregion
537

    
538
		#region Commands
539

    
540
        public void CancelCurrentOperation()
541
        {
542
            _pollAgent.CancelCurrentOperation();
543
        }
544

    
545
        public void ShowPreferences()
546
        {
547
            ShowPreferences(null);
548
        }
549

    
550
		public void ShowPreferences(string currentTab)
551
		{
552
			//Settings.Reload();
553
            
554
		    var preferences = IoC.Get<PreferencesViewModel>();//??new PreferencesViewModel(_windowManager, _events, this, Settings,currentTab);
555
            if (!String.IsNullOrWhiteSpace(currentTab))
556
                preferences.SelectedTab = currentTab;
557
            if (!preferences.IsActive)
558
		        _windowManager.ShowWindow(preferences);
559
            var view = (Window)preferences.GetView();
560
            view.NullSafe(v=>v.Activate());
561
		}
562

    
563
		public void AboutPithos()
564
		{
565
			var about = IoC.Get<AboutViewModel>();
566
		    about.LatestVersion=_sparkle.LatestVersion;
567
			_windowManager.ShowWindow(about);
568
		}
569

    
570
		public void SendFeedback()
571
		{
572
			var feedBack =  IoC.Get<FeedbackViewModel>();
573
			_windowManager.ShowWindow(feedBack);
574
		}
575

    
576
		//public PithosCommand OpenPithosFolderCommand { get; private set; }
577

    
578
		public void OpenPithosFolder()
579
		{
580
			var account = Settings.Accounts.FirstOrDefault(acc => acc.IsActive);
581
			if (account == null)
582
				return;
583
			Process.Start(account.RootPath);
584
		}
585

    
586
		public void OpenPithosFolder(AccountInfo account)
587
		{
588
			Process.Start(account.AccountPath);
589
		}
590

    
591
		
592

    
593
		public void GoToSite()
594
		{            
595
			var site = Properties.Settings.Default.ProductionServer;
596
			Process.Start(site);            
597
		}
598

    
599

    
600
		public void GoToSite(AccountInfo account)
601
		{
602
		    var uri = account.SiteUri.Replace("http://","https://");            
603
		    Process.Start(uri);
604
		}
605

    
606
	    private bool _statusVisible;
607

    
608
	    public string MiniStatusCaption
609
	    {
610
	        get
611
	        {
612
	            return  _statusVisible ? "Hide Status Window" : "Show Status Window";
613
	        }
614
	    }
615

    
616
	    public bool HasConflicts
617
	    {
618
            get { return true; }
619
	    }
620
        public void ShowConflicts()
621
        {
622
            _windowManager.ShowWindow(IoC.Get<ConflictsViewModel>());            
623
        }
624

    
625
	    /// <summary>
626
        /// Open an explorer window to the target path's directory
627
        /// and select the file
628
        /// </summary>
629
        /// <param name="entry"></param>
630
        public void GoToFile(FileEntry entry)
631
        {
632
            var fullPath = entry.FullPath;
633
            if (!File.Exists(fullPath) && !Directory.Exists(fullPath))
634
                return;
635
            Process.Start("explorer.exe","/select, " + fullPath);
636
        }
637

    
638
        public void OpenLogPath()
639
        {
640
            var pithosDataPath = PithosSettings.PithosDataPath;
641

    
642
            Process.Start(pithosDataPath);
643
        }
644
        
645
        public void ShowFileProperties()
646
		{
647
			var account = Settings.Accounts.First(acc => acc.IsActive);            
648
			var dir = new DirectoryInfo(account.RootPath + @"\pithos");
649
			var files=dir.GetFiles();
650
			var r=new Random();
651
			var idx=r.Next(0, files.Length);
652
			ShowFileProperties(files[idx].FullName);            
653
		}
654

    
655
		public void ShowFileProperties(string filePath)
656
		{
657
			if (String.IsNullOrWhiteSpace(filePath))
658
				throw new ArgumentNullException("filePath");
659
			if (!File.Exists(filePath) && !Directory.Exists(filePath))
660
				throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath");
661
			Contract.EndContractBlock();
662

    
663
			var pair=(from monitor in  Monitors
664
							   where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)
665
								   select monitor).FirstOrDefault();
666
			var accountMonitor = pair.Value;
667

    
668
			if (accountMonitor == null)
669
				return;
670

    
671
			var infoTask=accountMonitor.GetObjectInfo(filePath);
672

    
673
			
674

    
675
			var fileProperties = new FilePropertiesViewModel(this, infoTask,filePath);
676
			_windowManager.ShowWindow(fileProperties);
677
		} 
678
		
679
		public void ShowContainerProperties()
680
		{
681
			var account = Settings.Accounts.First(acc => acc.IsActive);            
682
			var dir = new DirectoryInfo(account.RootPath);
683
			var fullName = (from folder in dir.EnumerateDirectories()
684
							where (folder.Attributes & FileAttributes.Hidden) == 0
685
							select folder.FullName).First();
686
			ShowContainerProperties(fullName);            
687
		}
688

    
689
		public void ShowContainerProperties(string filePath)
690
		{
691
			if (String.IsNullOrWhiteSpace(filePath))
692
				throw new ArgumentNullException("filePath");
693
			if (!Directory.Exists(filePath))
694
				throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath");
695
			Contract.EndContractBlock();
696

    
697
			var pair=(from monitor in  Monitors
698
							   where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)
699
								   select monitor).FirstOrDefault();
700
			var accountMonitor = pair.Value;            
701
			var info = accountMonitor.GetContainerInfo(filePath);
702

    
703
			
704

    
705
			var containerProperties = new ContainerPropertiesViewModel(this, info,filePath);
706
			_windowManager.ShowWindow(containerProperties);
707
		}
708

    
709
		public void SynchNow()
710
		{
711
			_pollAgent.SynchNow();
712
		}
713

    
714
		public async Task<ObjectInfo> RefreshObjectInfo(ObjectInfo currentInfo)
715
		{
716
			if (currentInfo==null)
717
				throw new ArgumentNullException("currentInfo");
718
			Contract.EndContractBlock();		    
719
            var monitor = Monitors[currentInfo.AccountKey];
720
			var newInfo=await monitor.CloudClient.GetObjectInfo(currentInfo.Account, currentInfo.Container, currentInfo.Name).ConfigureAwait(false);
721
			return newInfo;
722
		}
723

    
724
		public async Task<ContainerInfo> RefreshContainerInfo(ContainerInfo container)
725
		{
726
			if (container == null)
727
				throw new ArgumentNullException("container");
728
			Contract.EndContractBlock();
729

    
730
			var monitor = Monitors[container.AccountKey];
731
			var newInfo = await monitor.CloudClient.GetContainerInfo(container.Account, container.Name).ConfigureAwait(false);
732
			return newInfo;
733
		}
734

    
735
	    private bool _isPaused;
736
	    public bool IsPaused
737
	    {
738
	        get { return _isPaused; }
739
	        set
740
	        {
741
	            _isPaused = value;
742
                PauseSyncCaption = IsPaused ? "Resume syncing" : "Pause syncing";
743
                var iconKey = IsPaused ? "TraySyncPaused" : "TrayInSynch";
744
                StatusIcon = String.Format(@"../Images/{0}.ico", iconKey);
745

    
746
                NotifyOfPropertyChange(() => IsPaused);
747
	        }
748
	    }
749

    
750
	    public void ToggleSynching()
751
		{
752
			IsPaused=!IsPaused;
753
			foreach (var monitor in Monitors.Values)
754
			{
755
			    monitor.Pause = IsPaused ;
756
			}
757
            _pollAgent.Pause = IsPaused;
758
            _networkAgent.Pause = IsPaused;
759

    
760

    
761
		}
762

    
763
        public void ExitPithos()
764
        {
765
            try
766
            {
767

    
768
                foreach (var monitor in Monitors.Select(pair => pair.Value))
769
                {
770
                    monitor.Stop();
771
                }
772

    
773
                var view = GetView() as Window;
774
                if (view != null)
775
                    view.Close();
776
            }
777
            catch (Exception exc)
778
            {
779
                Log.Info("Exception while exiting", exc);                
780
            }
781
            finally
782
            {
783
                Application.Current.Shutdown();
784
            }
785
        }
786

    
787
	    #endregion
788

    
789

    
790
		private readonly Dictionary<PithosStatus, StatusInfo> _iconNames = new List<StatusInfo>
791
			{
792
				new StatusInfo(PithosStatus.InSynch, "All files up to date", "TrayInSynch"),
793
				new StatusInfo(PithosStatus.PollSyncing, "Polling Files", "TraySynching"),
794
                new StatusInfo(PithosStatus.LocalSyncing, "Syncing Files", "TraySynching"),
795
				new StatusInfo(PithosStatus.SyncPaused, "Sync Paused", "TraySyncPaused")
796
			}.ToDictionary(s => s.Status);
797

    
798
		readonly IWindowManager _windowManager;
799
		
800
        //private int _syncCount=0;
801

    
802

    
803
        private PithosStatus _pithosStatus = PithosStatus.Disconnected;
804

    
805
        public void SetPithosStatus(PithosStatus status)
806
        {
807
            if (_pithosStatus == PithosStatus.LocalSyncing && status == PithosStatus.PollComplete)
808
                return;
809
            if (_pithosStatus == PithosStatus.PollSyncing && status == PithosStatus.LocalComplete)
810
                return;
811
            if (status == PithosStatus.LocalComplete || status == PithosStatus.PollComplete)
812
                _pithosStatus = PithosStatus.InSynch;
813
            else
814
                _pithosStatus = status;
815
            UpdateStatus();
816
        }
817

    
818
        public void SetPithosStatus(PithosStatus status,string message)
819
        {
820
            StatusMessage = message;
821
            SetPithosStatus(status);
822
        }
823

    
824
	  /*  public Notifier GetNotifier(Notification startNotification, Notification endNotification)
825
	    {
826
	        return new Notifier(this, startNotification, endNotification);
827
	    }*/
828

    
829
	    public Notifier GetNotifier(string startMessage, string endMessage, bool isActive=true,params object[] args)
830
	    {
831
	        return isActive?new Notifier(this, 
832
                new StatusNotification(String.Format(startMessage,args)), 
833
                new StatusNotification(String.Format(endMessage,args)))
834
                :new Notifier(this,(Notification) null,null);
835
	    }
836

    
837

    
838
	    ///<summary>
839
		/// Updates the visual status indicators of the application depending on status changes, e.g. icon, stat		
840
		///</summary>
841
		public void UpdateStatus()
842
		{
843

    
844
			if (_iconNames.ContainsKey(_pithosStatus))
845
			{
846
				var info = _iconNames[_pithosStatus];
847
				StatusIcon = String.Format(@"../Images/{0}.ico", info.IconName);
848
			}
849

    
850
            if (_pithosStatus == PithosStatus.InSynch)
851
                StatusMessage = "All files up to date";
852
		}
853

    
854

    
855
	   
856
		private Task StartMonitor(PithosMonitor monitor,int retries=0)
857
		{
858
			return Task.Factory.StartNew(() =>
859
			{
860
				using (log4net.ThreadContext.Stacks["Monitor"].Push("Start"))
861
				{
862
					try
863
					{
864
						Log.InfoFormat("Start Monitoring {0}", monitor.UserName);
865

    
866
						monitor.Start();
867
					}
868
					catch (WebException exc)
869
					{
870
						if (AbandonRetry(monitor, retries))
871
							return;
872

    
873
						HttpStatusCode statusCode =HttpStatusCode.OK;
874
						var response = exc.Response as HttpWebResponse;
875
						if(response!=null)
876
							statusCode = response.StatusCode;
877

    
878
						switch (statusCode)
879
						{
880
							case HttpStatusCode.Unauthorized:
881
								var message = String.Format("API Key Expired for {0}. Starting Renewal",
882
															monitor.UserName);
883
								Log.Error(message, exc);
884
                                var account = Settings.Accounts.Find(acc => acc.AccountKey == new Uri(monitor.AuthenticationUrl).Combine(monitor.UserName));                                
885
						        account.IsExpired = true;
886
                                Notify(new ExpirationNotification(account));
887
								//TryAuthorize(monitor.UserName, retries).Wait();
888
								break;
889
							case HttpStatusCode.ProxyAuthenticationRequired:
890
								TryAuthenticateProxy(monitor,retries);
891
								break;
892
							default:
893
								TryLater(monitor, exc, retries);
894
								break;
895
						}
896
					}
897
					catch (Exception exc)
898
					{
899
						if (AbandonRetry(monitor, retries)) 
900
							return;
901

    
902
						TryLater(monitor,exc,retries);
903
					}
904
				}
905
			});
906
		}
907

    
908
		private void TryAuthenticateProxy(PithosMonitor monitor,int retries)
909
		{
910
			Execute.OnUIThread(() =>
911
								   {                                       
912
									   var proxyAccount = IoC.Get<ProxyAccountViewModel>();
913
										proxyAccount.Settings = Settings;
914
									   if (true != _windowManager.ShowDialog(proxyAccount)) 
915
										   return;
916
									   StartMonitor(monitor, retries);
917
									   NotifyOfPropertyChange(() => Accounts);
918
								   });
919
		}
920

    
921
		private bool AbandonRetry(PithosMonitor monitor, int retries)
922
		{
923
			if (retries > 3)
924
			{
925
				var message = String.Format("Monitoring of account {0} has failed too many times. Will not retry",
926
											monitor.UserName);
927
				_events.Publish(new Notification
928
									{Title = "Account monitoring failed", Message = message, Level = TraceLevel.Error});
929
				return true;
930
			}
931
			return false;
932
		}
933

    
934

    
935
	    private void TryLater(PithosMonitor monitor, Exception exc,int retries)
936
		{
937
			var message = String.Format("An exception occured. Can't start monitoring\nWill retry in 10 seconds");
938
			Task.Factory.StartNewDelayed(10000, () => StartMonitor(monitor,retries+1));
939
			_events.Publish(new Notification
940
								{Title = "Error", Message = message, Level = TraceLevel.Error});
941
			Log.Error(message, exc);
942
		}
943

    
944

    
945
		public void NotifyChange(string status, TraceLevel level=TraceLevel.Info)
946
		{
947
			StatusMessage = status;
948
			
949
			_events.Publish(new Notification { Title = "Pithos+", Message = status, Level = level });
950
		}
951

    
952
		public void NotifyChangedFile(string filePath)
953
		{
954
            if (RecentFiles.Any(e => e.FullPath == filePath))
955
                return;
956
            
957
			IProducerConsumerCollection<FileEntry> files=RecentFiles;
958
			FileEntry popped;
959
			while (files.Count > 5)
960
				files.TryTake(out popped);
961
            var entry = new FileEntry { FullPath = filePath };
962
			files.TryAdd(entry);
963
		}
964

    
965
		public void NotifyAccount(AccountInfo account)
966
		{
967
			if (account== null)
968
				return;
969
			//TODO: What happens to an existing account whose Token has changed?
970
			account.SiteUri= String.Format("{0}/ui/?token={1}&user={2}",
971
				account.SiteUri, Uri.EscapeDataString(account.Token),
972
				Uri.EscapeDataString(account.UserName));
973

    
974
			if (!Accounts.Any(item => item.UserName == account.UserName && item.SiteUri == account.SiteUri))
975
				Accounts.TryAdd(account);
976

    
977
		}
978

    
979
		public void NotifyConflicts(IEnumerable<FileSystemInfo> conflictFiles, string message)
980
		{
981
			if (conflictFiles == null)
982
				return;
983
		    //Convert to list to avoid multiple iterations
984
            var files = conflictFiles.ToList();
985
			if (files.Count==0)
986
				return;
987

    
988
			UpdateStatus();
989
			//TODO: Create a more specific message. For now, just show a warning
990
			NotifyForFiles(files,message,TraceLevel.Warning);
991

    
992
		}
993

    
994
		public void NotifyForFiles(IEnumerable<FileSystemInfo> files, string message,TraceLevel level=TraceLevel.Info)
995
		{
996
			if (files == null)
997
				return;
998
			if (!files.Any())
999
				return;
1000

    
1001
			StatusMessage = message;
1002

    
1003
			_events.Publish(new Notification { Title = "Pithos+", Message = message, Level = level});
1004
		}
1005

    
1006
		public void Notify(Notification notification)
1007
		{
1008
            TaskEx.Run(()=> _events.Publish(notification));
1009
		}
1010

    
1011

    
1012
		public void RemoveMonitor(string serverUrl,string accountName)
1013
		{
1014
			if (String.IsNullOrWhiteSpace(accountName))
1015
				return;
1016

    
1017
			var accountInfo=_accounts.FirstOrDefault(account => account.UserName == accountName && account.StorageUri.ToString().StartsWith(serverUrl));
1018
            if (accountInfo != null)
1019
            {
1020
                _accounts.TryRemove(accountInfo);
1021
                _pollAgent.RemoveAccount(accountInfo);
1022
            }
1023

    
1024
            var accountKey = new Uri(serverUrl).Combine(accountName);
1025
		    PithosMonitor monitor;
1026
			if (Monitors.TryRemove(accountKey, out monitor))
1027
			{
1028
				monitor.Stop();
1029
                //TODO: Also remove any pending actions for this account
1030
                //from the network queue                
1031
			}
1032
		}
1033

    
1034
		public void RefreshOverlays()
1035
		{
1036
			foreach (var pair in Monitors)
1037
			{
1038
				var monitor = pair.Value;
1039

    
1040
				var path = monitor.RootPath;
1041

    
1042
				if (String.IsNullOrWhiteSpace(path))
1043
					continue;
1044

    
1045
				if (!Directory.Exists(path) && !File.Exists(path))
1046
					continue;
1047

    
1048
				IntPtr pathPointer = Marshal.StringToCoTaskMemAuto(path);
1049

    
1050
				try
1051
				{
1052
					NativeMethods.SHChangeNotify(HChangeNotifyEventID.SHCNE_UPDATEITEM,
1053
												 HChangeNotifyFlags.SHCNF_PATHW | HChangeNotifyFlags.SHCNF_FLUSHNOWAIT,
1054
												 pathPointer, IntPtr.Zero);
1055
				}
1056
				finally
1057
				{
1058
					Marshal.FreeHGlobal(pathPointer);
1059
				}
1060
			}
1061
		}
1062

    
1063
		#region Event Handlers
1064
		
1065
		public void Handle(SelectiveSynchChanges message)
1066
		{
1067
		    TaskEx.Run(() =>
1068
		    {
1069
		        PithosMonitor monitor;
1070
		        if (Monitors.TryGetValue(message.Account.AccountKey, out monitor))
1071
		        {
1072
                    Selectives.SetIsSelectiveEnabled(message.Account.AccountKey, message.Enabled);
1073
		            monitor.SetSelectivePaths(message.Uris, message.Added, message.Removed);
1074
		        }
1075

    
1076
		        var account = Accounts.FirstOrDefault(acc => acc.AccountKey == message.Account.AccountKey);
1077
		        if (account != null)
1078
		        {
1079
		            var added=monitor.UrisToFilePaths(message.Added);
1080
                    _pollAgent.SynchNow(added);
1081
		        }
1082
		    });
1083

    
1084
		}
1085

    
1086

    
1087
		private bool _pollStarted;
1088
	    private Sparkle _sparkle;
1089
	    private bool _manualUpgradeCheck;
1090

    
1091
	    //SMELL: Doing so much work for notifications in the shell is wrong
1092
		//The notifications should be moved to their own view/viewmodel pair
1093
		//and different templates should be used for different message types
1094
		//This will also allow the addition of extra functionality, eg. actions
1095
		//
1096
		public void Handle(Notification notification)
1097
		{
1098
			UpdateStatus();
1099

    
1100
			if (!Settings.ShowDesktopNotifications)
1101
				return;
1102

    
1103
			if (notification is PollNotification)
1104
			{
1105
				_pollStarted = true;
1106
				return;
1107
			}
1108
			if (notification is CloudNotification)
1109
			{
1110
				if (!_pollStarted) 
1111
					return;
1112
				_pollStarted= false;
1113
				notification.Title = "Pithos+";
1114
				notification.Message = "Start Synchronisation";
1115
			}
1116

    
1117
		    var deleteNotification = notification as CloudDeleteNotification;
1118
            if (deleteNotification != null)
1119
            {
1120
                StatusMessage = String.Format("Deleted {0}", deleteNotification.Data.Name);
1121
                return;
1122
            }
1123

    
1124
		    var progress = notification as ProgressNotification;
1125
		    
1126
		    
1127
            if (progress != null)
1128
            {
1129
                double percentage = (progress.TotalBlocks == progress.Block) ? 1
1130
                    :(progress.Block + progress.BlockPercentage / 100.0) / (double)progress.TotalBlocks;
1131
		        StatusMessage = String.Format("{0} {1:p2} of {2} - {3}",		                                      
1132
                                              progress.Action,
1133
		                                      percentage,
1134
		                                      progress.FileSize.ToByteSize(),
1135
		                                      progress.FileName);
1136
		        return;
1137
		    }
1138

    
1139
		    var info = notification as StatusNotification;
1140
            if (info != null)
1141
            {
1142
                StatusMessage = info.Title;
1143
                return;
1144
            }
1145
			if (String.IsNullOrWhiteSpace(notification.Message) && String.IsNullOrWhiteSpace(notification.Title))
1146
				return;
1147

    
1148
            if (notification.Level <= TraceLevel.Warning)
1149
			    ShowBalloonFor(notification);
1150
		}
1151

    
1152
	    private void ShowBalloonFor(Notification notification)
1153
	    {
1154
            Contract.Requires(notification!=null);
1155
            
1156
            if (!Settings.ShowDesktopNotifications) 
1157
                return;
1158
            
1159
            BalloonIcon icon;
1160
	        switch (notification.Level)
1161
	        {
1162
                case TraceLevel.Verbose:
1163
	                return;
1164
	            case TraceLevel.Info:	            
1165
	                icon = BalloonIcon.Info;
1166
	                break;
1167
                case TraceLevel.Error:
1168
                    icon = BalloonIcon.Error;
1169
                    break;
1170
                case TraceLevel.Warning:
1171
	                icon = BalloonIcon.Warning;
1172
	                break;
1173
	            default:
1174
	                return;
1175
	        }
1176

    
1177
	        var tv = (ShellView) GetView();
1178
	        System.Action clickAction = null;
1179
	        if (notification is ExpirationNotification)
1180
	        {
1181
	            clickAction = () => ShowPreferences("AccountTab");
1182
	        }
1183
	        var balloon = new PithosBalloon
1184
	                          {
1185
	                              Title = notification.Title,
1186
	                              Message = notification.Message,
1187
	                              Icon = icon,
1188
	                              ClickAction = clickAction
1189
	                          };
1190
	        tv.TaskbarView.ShowCustomBalloon(balloon, PopupAnimation.Fade, 4000);
1191
	    }
1192

    
1193
	    #endregion
1194

    
1195
		public void Handle(ShowFilePropertiesEvent message)
1196
		{
1197
			if (message == null)
1198
				throw new ArgumentNullException("message");
1199
			if (String.IsNullOrWhiteSpace(message.FileName) )
1200
				throw new ArgumentException("message");
1201
			Contract.EndContractBlock();
1202

    
1203
			var fileName = message.FileName;
1204
			//TODO: Display file properties for non-container folders
1205
			if (File.Exists(fileName))
1206
				//Retrieve the full name with exact casing. Pithos names are case sensitive				
1207
				ShowFileProperties(FileInfoExtensions.GetProperFilePathCapitalization(fileName));
1208
			else if (Directory.Exists(fileName))
1209
				//Retrieve the full name with exact casing. Pithos names are case sensitive
1210
			{
1211
				var path = FileInfoExtensions.GetProperDirectoryCapitalization(fileName);
1212
				if (IsContainer(path))
1213
					ShowContainerProperties(path);
1214
				else
1215
					ShowFileProperties(path);
1216
			}
1217
		}
1218

    
1219
		private bool IsContainer(string path)
1220
		{
1221
			var matchingFolders = from account in _accounts
1222
								  from rootFolder in Directory.GetDirectories(account.AccountPath)
1223
								  where rootFolder.Equals(path, StringComparison.InvariantCultureIgnoreCase)
1224
								  select rootFolder;
1225
			return matchingFolders.Any();
1226
		}
1227

    
1228
		public FileStatus GetFileStatus(string localFileName)
1229
		{
1230
			if (String.IsNullOrWhiteSpace(localFileName))
1231
				throw new ArgumentNullException("localFileName");
1232
			Contract.EndContractBlock();
1233
			
1234
			var statusKeeper = IoC.Get<IStatusKeeper>();
1235
			var status=statusKeeper.GetFileStatus(localFileName);
1236
			return status;
1237
		}
1238

    
1239
	    public void RemoveAccountFromDatabase(AccountSettings account)
1240
	    {
1241
            var statusKeeper = IoC.Get<IStatusKeeper>();
1242
            statusKeeper.ClearFolderStatus(account.RootPath);	        
1243
	    }
1244
	}
1245
}