Statistics
| Branch: | Revision:

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

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

    
75

    
76
namespace Pithos.Client.WPF {
77
	using System.ComponentModel.Composition;
78

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

    
91
	    public bool CanExecute(object parameter)
92
	    {
93
	        return true;
94
	    }
95

    
96
	    public event EventHandler CanExecuteChanged;
97
	}
98

    
99

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

    
119
        
120
		public PithosSettings Settings { get; private set; }
121

    
122

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

    
138

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

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

    
148
        #pragma warning disable 649
149
        [Import]
150
	    private PollAgent _pollAgent;
151

    
152
        [Import]
153
	    private NetworkAgent _networkAgent;
154

    
155
	    [Import]
156
	    public Selectives Selectives { get; set; }
157

    
158
        #pragma warning restore 649
159

    
160
        public ToggleStatusCommand ToggleMiniStatusCommand { get; set; }
161

    
162
	    private MiniStatusViewModel _miniStatus;
163

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

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

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

    
199
/*
200
			    _pollAgent = pollAgent;
201
			    _networkAgent = networkAgent;
202
*/
203
				Settings = settings;
204

    
205
				Proxy.SetFromSettings(settings);
206

    
207
                StatusMessage = Settings.Accounts.Count==0 
208
                    ? "No Accounts added\r\nPlease add an account" 
209
                    : "Starting";
210

    
211
				_accounts.CollectionChanged += (sender, e) =>
212
												   {
213
													   NotifyOfPropertyChange(() => OpenFolderCaption);
214
													   NotifyOfPropertyChange(() => HasAccounts);
215
												   };
216

    
217
                SetVersionMessage();
218

    
219
                ToggleMiniStatusCommand=new ToggleStatusCommand(this);
220
			}
221
			catch (Exception exc)
222
			{
223
				Log.Error("Error while starting the ShellViewModel",exc);
224
				throw;
225
			}
226

    
227
		}
228

    
229
	    private void SetVersionMessage()
230
	    {
231
	        Assembly assembly = Assembly.GetExecutingAssembly();
232
	        var fileVersion = FileVersionInfo.GetVersionInfo(assembly.Location);
233
	        VersionMessage = String.Format("Pithos+ {0}", fileVersion.FileVersion);
234
	    }
235

    
236

    
237
        public void openOFM()
238
        {
239
            var fileManager = IoC.Get<FileManagerViewModel>();
240
            fileManager.Accounts = Settings.Accounts;
241
            fileManager.CurrentAccount = fileManager.Accounts.First();// Settings.Accounts.First();
242
            _windowManager.ShowWindow(fileManager);
243
            //var ofm = new OFM.GUI.OFM();
244
            //ofm.Show();
245
        }
246

    
247
        public void CurrentSyncStatus()
248
        {
249
            if (Accounts.Count == 0)
250
            {
251
                ShowPreferences("AccountTab");
252
            }
253
            else
254
            {
255
                if (!_statusVisible)
256
                {
257
                    _windowManager.ShowWindow(MiniStatus);
258
                    _statusVisible = true;
259
                }
260
                else
261
                {
262
                    if (MiniStatus.IsActive)
263
                        MiniStatus.TryClose();
264
                    _statusVisible = false;
265
                }
266

    
267
                NotifyOfPropertyChange(() => MiniStatusCaption);
268
            }
269
        }
270

    
271
	    protected override void OnActivate()
272
		{
273
			base.OnActivate();
274

    
275
            InitializeSparkle();
276

    
277
	        //Must delay opening the upgrade window
278
            //to avoid Windows Messages sent by the TaskbarIcon
279
            TaskEx.Delay(5000).ContinueWith(_=>
280
                Execute.OnUIThread(()=> _sparkle.StartLoop(true,Settings.UpdateForceCheck,Settings.UpdateCheckInterval)));
281

    
282

    
283
			StartMonitoring();                    
284
		}
285

    
286

    
287
	    private void OnCheckFinished(object sender, bool updaterequired)
288
	    {
289
            
290
            Log.InfoFormat("Upgrade check finished. Need Upgrade: {0}", updaterequired);
291
            if (_manualUpgradeCheck)
292
            {
293
                _manualUpgradeCheck = false;
294
                if (!updaterequired)
295
                    //Sparkle raises events on a background thread
296
                    Execute.OnUIThread(()=>
297
                        ShowBalloonFor(new Notification{Title="Pithos+ is up to date",Message="You have the latest Pithos+ version. No update is required"}));
298
            }
299
	    }
300

    
301
	    private void OnUpgradeDetected(object sender, UpdateDetectedEventArgs e)
302
	    {            
303
	        Log.InfoFormat("Update detected {0}",e.LatestVersion);
304
	    }
305

    
306
        public void CheckForUpgrade()
307
        {
308
            ShowBalloonFor(new Notification{Title="Checking for upgrades",Message="Contacting the server to retrieve the latest Pithos+ version."});
309
            _sparkle.StopLoop();
310
            _sparkle.updateDetected -= OnUpgradeDetected;
311
            _sparkle.checkLoopFinished -= OnCheckFinished;
312
            _sparkle.Dispose();
313

    
314
            _manualUpgradeCheck = true;
315
            InitializeSparkle();
316
            _sparkle.StartLoop(true,true,Settings.UpdateCheckInterval);
317
        }
318

    
319
        private void InitializeSparkle()
320
        {
321
            _sparkle = new Sparkle(Settings.UpdateUrl);
322
            _sparkle.updateDetected += OnUpgradeDetected;
323
            _sparkle.checkLoopFinished += OnCheckFinished;
324
            _sparkle.ShowDiagnosticWindow = Settings.UpdateDiagnostics;
325
        }
326

    
327
	    private async void StartMonitoring()
328
		{
329
			try
330
			{
331
                if (Settings.IgnoreCertificateErrors)
332
                {
333
                    ServicePointManager.ServerCertificateValidationCallback = (sender, certificate, chain, errors) => true;
334
                }
335
                    
336
				var accounts = Settings.Accounts.Select(MonitorAccount);
337
                await TaskEx.WhenAll(accounts).ConfigureAwait(false);
338
				_statusService = StatusService.Start();
339

    
340
			}
341
			catch (AggregateException exc)
342
			{
343
				exc.Handle(e =>
344
				{
345
					Log.Error("Error while starting monitoring", e);
346
					return true;
347
				});
348
				throw;
349
			}
350
		}
351

    
352
		protected override void OnDeactivate(bool close)
353
		{
354
			base.OnDeactivate(close);
355
			if (close)
356
			{
357
				StatusService.Stop(_statusService);
358
				_statusService = null;
359
			}
360
		}
361

    
362
		public Task MonitorAccount(AccountSettings account)
363
		{
364
			return Task.Factory.StartNew(() =>
365
			{                                                
366
				PithosMonitor monitor;
367
				var accountName = account.AccountName;
368

    
369
			    MigrateFolders(account);
370

    
371
			    Selectives.SetIsSelectiveEnabled(account.AccountKey, account.SelectiveSyncEnabled);
372

    
373
				if (Monitors.TryGetValue(account.AccountKey, out monitor))
374
				{
375
					//If the account is active
376
                    if (account.IsActive)
377
                    {
378
                        //The Api Key may have changed throuth the Preferences dialog
379
                        monitor.ApiKey = account.ApiKey;
380
						Debug.Assert(monitor.StatusNotification == this,"An existing monitor should already have a StatusNotification service object");
381
                        monitor.RootPath = account.RootPath;
382
                        //Start the monitor. It's OK to start an already started monitor,
383
                        //it will just ignore the call                        
384
                        StartMonitor(monitor).Wait();
385
                    }
386
                    else
387
                    {
388
                        //If the account is inactive
389
                        //Stop and remove the monitor
390
                        RemoveMonitor(account.ServerUrl,accountName);
391
                    }
392
					return;
393
				}
394

    
395
				
396
				//Create a new monitor/ Can't use MEF here, it would return a single instance for all monitors
397
				monitor = new PithosMonitor
398
							  {
399
								  UserName = accountName,
400
								  ApiKey = account.ApiKey,                                  
401
								  StatusNotification = this,
402
								  RootPath = account.RootPath
403
							  };
404
				//PithosMonitor uses MEF so we need to resolve it
405
				IoC.BuildUp(monitor);
406

    
407
				monitor.AuthenticationUrl = account.ServerUrl;
408

    
409
				Monitors[account.AccountKey] = monitor;
410

    
411
                if (!Directory.Exists(account.RootPath))
412
                {
413
                    account.IsActive = false;
414
                    Settings.Save();
415
                    Notify(new Notification
416
                    {
417
                        Level = TraceLevel.Error,
418
                        Title = "Missing account folder",
419
                        Message = String.Format("Can't find the root folder for account {0} at {1}. The account was deactivated.\r" +
420
                        "If the account's files were stored in a removable disk, please connect it and reactivate the account", account.AccountName, account.RootPath)
421
                    });
422
                }
423

    
424

    
425
				if (account.IsActive)
426
				{
427
					//Don't start a monitor if it doesn't have an account and ApiKey
428
					if (String.IsNullOrWhiteSpace(monitor.UserName) ||
429
						String.IsNullOrWhiteSpace(monitor.ApiKey))
430
						return;
431
					StartMonitor(monitor);
432
				}
433
			});
434
		}
435

    
436
	    private void MigrateFolders(AccountSettings account)
437
	    {
438
	        var oldOthersFolder=Path.Combine(account.RootPath, FolderConstants.OldOthersFolder);
439
	        var newOthersFolder = Path.Combine(account.RootPath, FolderConstants.OthersFolder);
440
	        var oldFolder = new DirectoryInfo(oldOthersFolder);
441
	        var newFolder = new DirectoryInfo(newOthersFolder);
442

    
443
            if (oldFolder.Exists && !newFolder.Exists)
444
            {
445
                oldFolder.MoveTo(newOthersFolder);
446
            }
447
	    }
448

    
449

    
450
	    protected override void OnViewLoaded(object view)
451
		{
452
			UpdateStatus();
453
			var window = (Window)view;            
454
			TaskEx.Delay(1000).ContinueWith(t => Execute.OnUIThread(window.Hide));
455
			base.OnViewLoaded(view);
456
		}
457

    
458

    
459
		#region Status Properties
460

    
461
		private string _statusMessage;
462
		public string StatusMessage
463
		{
464
			get { return _statusMessage; }
465
			set
466
			{
467
				_statusMessage = value;
468
				NotifyOfPropertyChange(() => StatusMessage);
469
                NotifyOfPropertyChange(() => TooltipMessage);
470
			}
471
		}
472

    
473
	    public int Progress
474
	    {
475
	        get { return _progress; }
476
	        set
477
	        {
478
	            _progress = value;
479
                NotifyOfPropertyChange(()=>Progress);
480
	        }
481
	    }
482

    
483
	    public string VersionMessage { get; set; }
484

    
485
	    public string TooltipMessage
486
	    {
487
	        get
488
	        {
489
	            return String.Format("{0}\r\n{1}",VersionMessage,StatusMessage);
490
	        }
491
	    }
492

    
493
        public string TooltipMiniStatus
494
        {
495
            get
496
            {
497
                return String.Format("{0}\r\n{1}", "Status Window", "Enable / Disable the status window");
498
            }
499
        }
500

    
501
        /*public string ToggleStatusWindowMessage
502
        {
503
            get
504
            {
505
                return String.Format("{0}" + Environment.NewLine + "{1} Toggle Mini Status");
506
            }
507
        }*/
508

    
509
	    private readonly ObservableConcurrentCollection<AccountInfo> _accounts = new ObservableConcurrentCollection<AccountInfo>();
510
		public ObservableConcurrentCollection<AccountInfo> Accounts
511
		{
512
			get { return _accounts; }
513
		}
514

    
515
		public bool HasAccounts
516
		{
517
			get { return _accounts.Count > 0; }
518
		}
519

    
520

    
521
		public string OpenFolderCaption
522
		{
523
			get
524
			{
525
				return (_accounts.Count == 0)
526
						? "No Accounts Defined"
527
						: "Open Pithos Folder";
528
			}
529
		}
530

    
531
		private string _pauseSyncCaption="Pause Synching";
532
		public string PauseSyncCaption
533
		{
534
			get { return _pauseSyncCaption; }
535
			set
536
			{
537
				_pauseSyncCaption = value;
538
				NotifyOfPropertyChange(() => PauseSyncCaption);
539
			}
540
		}
541

    
542
		private readonly ObservableConcurrentCollection<FileEntry> _recentFiles = new ObservableConcurrentCollection<FileEntry>();
543
		public ObservableConcurrentCollection<FileEntry> RecentFiles
544
		{
545
			get { return _recentFiles; }
546
		}
547

    
548

    
549
		private string _statusIcon="../Images/Pithos.ico";
550
		public string StatusIcon
551
		{
552
			get { return _statusIcon; }
553
			set
554
			{
555
				//TODO: Ensure all status icons use the Pithos logo
556
				_statusIcon = value;
557
				NotifyOfPropertyChange(() => StatusIcon);
558
			}
559
		}
560

    
561
		#endregion
562

    
563
		#region Commands
564

    
565
        public void CancelCurrentOperation()
566
        {
567
            _pollAgent.CancelCurrentOperation();
568
        }
569

    
570
        public void ShowPreferences()
571
        {
572
            ShowPreferences(null);
573
        }
574

    
575
		public void ShowPreferences(string currentTab)
576
		{
577
			//Settings.Reload();
578
            
579
		    var preferences = IoC.Get<PreferencesViewModel>();//??new PreferencesViewModel(_windowManager, _events, this, Settings,currentTab);
580
            if (!String.IsNullOrWhiteSpace(currentTab))
581
                preferences.SelectedTab = currentTab;
582
            if (!preferences.IsActive)
583
		        _windowManager.ShowWindow(preferences);
584
            var view = (Window)preferences.GetView();
585
            view.NullSafe(v=>v.Activate());
586
		}
587

    
588
		public void AboutPithos()
589
		{
590
			var about = IoC.Get<AboutViewModel>();
591
		    about.LatestVersion=_sparkle.LatestVersion;
592
			_windowManager.ShowWindow(about);
593
		}
594

    
595
		public void SendFeedback()
596
		{
597
			var feedBack =  IoC.Get<FeedbackViewModel>();
598
			_windowManager.ShowWindow(feedBack);
599
		}
600

    
601
		//public PithosCommand OpenPithosFolderCommand { get; private set; }
602

    
603
		public void OpenPithosFolder()
604
		{
605
			var account = Settings.Accounts.FirstOrDefault(acc => acc.IsActive);
606
			if (account == null)
607
				return;
608
			Process.Start(account.RootPath);
609
		}
610

    
611
		public void OpenPithosFolder(AccountInfo account)
612
		{
613
			Process.Start(account.AccountPath);
614
		}
615

    
616
		
617

    
618
		public void GoToSite()
619
		{            
620
			var site = Properties.Settings.Default.ProductionServer;
621
			Process.Start(site);            
622
		}
623

    
624

    
625
		public void GoToSite(AccountInfo account)
626
		{
627
		    var uri = account.SiteUri.Replace("http://","https://");            
628
		    Process.Start(uri);
629
		}
630

    
631
	    private bool _statusVisible;
632

    
633
	    public string MiniStatusCaption
634
	    {
635
	        get
636
	        {
637
	            return  _statusVisible ? "Hide Status Window" : "Show Status Window";
638
	        }
639
	    }
640

    
641
	    public bool HasConflicts
642
	    {
643
            get { return true; }
644
	    }
645
        public void ShowConflicts()
646
        {
647
            _windowManager.ShowWindow(IoC.Get<ConflictsViewModel>());            
648
        }
649

    
650
	    /// <summary>
651
        /// Open an explorer window to the target path's directory
652
        /// and select the file
653
        /// </summary>
654
        /// <param name="entry"></param>
655
        public void GoToFile(FileEntry entry)
656
        {
657
            var fullPath = entry.FullPath;
658
            if (!File.Exists(fullPath) && !Directory.Exists(fullPath))
659
                return;
660
            Process.Start("explorer.exe","/select, " + fullPath);
661
        }
662

    
663
        public void OpenLogPath()
664
        {
665
            var pithosDataPath = PithosSettings.PithosDataPath;
666

    
667
            Process.Start(pithosDataPath);
668
        }
669
        
670
        public void ShowFileProperties()
671
		{
672
			var account = Settings.Accounts.First(acc => acc.IsActive);            
673
			var dir = new DirectoryInfo(account.RootPath + @"\pithos");
674
			var files=dir.GetFiles();
675
			var r=new Random();
676
			var idx=r.Next(0, files.Length);
677
			ShowFileProperties(files[idx].FullName);            
678
		}
679

    
680
		public void ShowFileProperties(string filePath)
681
		{
682
			if (String.IsNullOrWhiteSpace(filePath))
683
				throw new ArgumentNullException("filePath");
684
			if (!File.Exists(filePath) && !Directory.Exists(filePath))
685
				throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath");
686
			Contract.EndContractBlock();
687

    
688
			var pair=(from monitor in  Monitors
689
							   where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)
690
								   select monitor).FirstOrDefault();
691
			var accountMonitor = pair.Value;
692

    
693
			if (accountMonitor == null)
694
				return;
695

    
696
			var infoTask=accountMonitor.GetObjectInfo(filePath);
697

    
698
			
699

    
700
			var fileProperties = new FilePropertiesViewModel(this, infoTask,filePath);
701
			_windowManager.ShowWindow(fileProperties);
702
		} 
703
		
704
		public void ShowContainerProperties()
705
		{
706
			var account = Settings.Accounts.First(acc => acc.IsActive);            
707
			var dir = new DirectoryInfo(account.RootPath);
708
			var fullName = (from folder in dir.EnumerateDirectories()
709
							where (folder.Attributes & FileAttributes.Hidden) == 0
710
							select folder.FullName).First();
711
			ShowContainerProperties(fullName);            
712
		}
713

    
714
		public void ShowContainerProperties(string filePath)
715
		{
716
			if (String.IsNullOrWhiteSpace(filePath))
717
				throw new ArgumentNullException("filePath");
718
			if (!Directory.Exists(filePath))
719
				throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath");
720
			Contract.EndContractBlock();
721

    
722
			var pair=(from monitor in  Monitors
723
							   where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)
724
								   select monitor).FirstOrDefault();
725
			var accountMonitor = pair.Value;            
726
			var info = accountMonitor.GetContainerInfo(filePath);
727

    
728
			
729

    
730
			var containerProperties = new ContainerPropertiesViewModel(this, info,filePath);
731
			_windowManager.ShowWindow(containerProperties);
732
		}
733

    
734
		public void SynchNow()
735
		{
736
			_pollAgent.SynchNow();
737
		}
738

    
739
		public async Task<ObjectInfo> RefreshObjectInfo(ObjectInfo currentInfo)
740
		{
741
			if (currentInfo==null)
742
				throw new ArgumentNullException("currentInfo");
743
			Contract.EndContractBlock();		    
744
            var monitor = Monitors[currentInfo.AccountKey];
745
			var newInfo=await monitor.CloudClient.GetObjectInfo(currentInfo.Account, currentInfo.Container, currentInfo.Name).ConfigureAwait(false);
746
			return newInfo;
747
		}
748

    
749
		public async Task<ContainerInfo> RefreshContainerInfo(ContainerInfo container)
750
		{
751
			if (container == null)
752
				throw new ArgumentNullException("container");
753
			Contract.EndContractBlock();
754

    
755
			var monitor = Monitors[container.AccountKey];
756
			var newInfo = await monitor.CloudClient.GetContainerInfo(container.Account, container.Name).ConfigureAwait(false);
757
			return newInfo;
758
		}
759

    
760
	    private bool _isPaused;
761
	    public bool IsPaused
762
	    {
763
	        get { return _isPaused; }
764
	        set
765
	        {
766
	            _isPaused = value;
767
                PauseSyncCaption = IsPaused ? "Resume syncing" : "Pause syncing";
768
                var iconKey = IsPaused ? "TraySyncPaused" : "TrayInSynch";
769
                StatusIcon = String.Format(@"../Images/{0}.ico", iconKey);
770

    
771
                NotifyOfPropertyChange(() => IsPaused);
772
	        }
773
	    }
774

    
775
	    public void ToggleSynching()
776
		{
777
			IsPaused=!IsPaused;
778
			foreach (var monitor in Monitors.Values)
779
			{
780
			    monitor.Pause = IsPaused ;
781
			}
782
            _pollAgent.Pause = IsPaused;
783
            _networkAgent.Pause = IsPaused;
784

    
785

    
786
		}
787

    
788
        public void ExitPithos()
789
        {
790
            try
791
            {
792

    
793
                foreach (var monitor in Monitors.Select(pair => pair.Value))
794
                {
795
                    monitor.Stop();
796
                }
797

    
798
                var view = GetView() as Window;
799
                if (view != null)
800
                    view.Close();
801
            }
802
            catch (Exception exc)
803
            {
804
                Log.Info("Exception while exiting", exc);                
805
            }
806
            finally
807
            {
808
                Application.Current.Shutdown();
809
            }
810
        }
811

    
812
	    #endregion
813

    
814

    
815
		private readonly Dictionary<PithosStatus, StatusInfo> _iconNames = new List<StatusInfo>
816
			{
817
				new StatusInfo(PithosStatus.InSynch, "All files up to date", "TrayInSynch"),
818
				new StatusInfo(PithosStatus.PollSyncing, "Polling Files", "TraySynching"),
819
                new StatusInfo(PithosStatus.LocalSyncing, "Syncing Files", "TraySynching"),
820
				new StatusInfo(PithosStatus.SyncPaused, "Sync Paused", "TraySyncPaused")
821
			}.ToDictionary(s => s.Status);
822

    
823
		readonly IWindowManager _windowManager;
824
		
825
        //private int _syncCount=0;
826

    
827

    
828
        private PithosStatus _pithosStatus = PithosStatus.Disconnected;
829

    
830
        public void SetPithosStatus(PithosStatus status)
831
        {
832
            if (_pithosStatus == PithosStatus.LocalSyncing && status == PithosStatus.PollComplete)
833
                return;
834
            if (_pithosStatus == PithosStatus.PollSyncing && status == PithosStatus.LocalComplete)
835
                return;
836
            if (status == PithosStatus.LocalComplete || status == PithosStatus.PollComplete)
837
                _pithosStatus = PithosStatus.InSynch;
838
            else
839
                _pithosStatus = status;
840
            UpdateStatus();
841
        }
842

    
843
        public void SetPithosStatus(PithosStatus status,string message)
844
        {
845
            StatusMessage = message;
846
            SetPithosStatus(status);
847
        }
848

    
849
	  /*  public Notifier GetNotifier(Notification startNotification, Notification endNotification)
850
	    {
851
	        return new Notifier(this, startNotification, endNotification);
852
	    }*/
853

    
854
	    public Notifier GetNotifier(string startMessage, string endMessage, bool isActive=true,params object[] args)
855
	    {
856
	        return isActive?new Notifier(this, 
857
                new StatusNotification(String.Format(startMessage,args)),
858
                new StatusNotification(String.Format(endMessage,args)))
859
                :new Notifier(this,(Notification) null,null);
860
	    }
861

    
862

    
863
	    ///<summary>
864
		/// Updates the visual status indicators of the application depending on status changes, e.g. icon, stat		
865
		///</summary>
866
		public void UpdateStatus()
867
		{
868

    
869
			if (_iconNames.ContainsKey(_pithosStatus))
870
			{
871
				var info = _iconNames[_pithosStatus];
872
				StatusIcon = String.Format(@"../Images/{0}.ico", info.IconName);
873
			}
874

    
875
            if (_pithosStatus == PithosStatus.InSynch)
876
                StatusMessage = "All files up to date";
877
		}
878

    
879

    
880
	   
881
		public async Task StartMonitor(PithosMonitor monitor,int retries=0)
882
		{
883
		    using (log4net.ThreadContext.Stacks["Monitor"].Push("Start"))
884
		    {
885
		        try
886
		        {
887
		            Log.InfoFormat("Start Monitoring {0}", monitor.UserName);
888

    
889
		            await monitor.Start();
890
		        }
891
                catch (HttpRequestWithStatusException exc)
892
		        {
893
		            if (AbandonRetry(monitor, retries))
894
		                return;
895

    
896
		            //HttpStatusCode statusCode = HttpStatusCode.OK;
897
		            //var response =  as HttpWebResponse;
898
		            //if (response != null)
899
		            var statusCode = exc.StatusCode;//response.StatusCode;
900

    
901
		            switch (statusCode)
902
		            {
903
		                case HttpStatusCode.Unauthorized:
904
		                    var message = String.Format("API Key Expired for {0}. Starting Renewal",
905
		                                                monitor.UserName);
906
		                    Log.Error(message, exc);
907
		                    var account =
908
		                        Settings.Accounts.Find(
909
		                            acc => acc.AccountKey == new Uri(monitor.AuthenticationUrl).Combine(monitor.UserName));
910
		                    account.IsExpired = true;
911
                            Settings.Save();
912
		                    Notify(new ExpirationNotification(account));
913
		                    //TryAuthorize(monitor.UserName, retries).Wait();
914
		                    break;
915
		                case HttpStatusCode.ProxyAuthenticationRequired:
916
		                    TryAuthenticateProxy(monitor, retries);
917
		                    break;
918
		                default:
919
		                    TryLater(monitor, exc, retries);
920
		                    break;
921
		            }
922
		        }
923
		        catch (Exception exc)
924
		        {
925
		            if (AbandonRetry(monitor, retries))
926
		                return;
927

    
928
		            TryLater(monitor, exc, retries);
929
		        }
930
		    }
931

    
932
		}
933

    
934
	    private void TryAuthenticateProxy(PithosMonitor monitor,int retries)
935
		{
936
			Execute.OnUIThread(() =>
937
								   {                                       
938
									   var proxyAccount = IoC.Get<ProxyAccountViewModel>();
939
										proxyAccount.Settings = Settings;
940
									   if (true != _windowManager.ShowDialog(proxyAccount)) 
941
										   return;
942
									   StartMonitor(monitor, retries);
943
									   NotifyOfPropertyChange(() => Accounts);
944
								   });
945
		}
946

    
947
		private bool AbandonRetry(PithosMonitor monitor, int retries)
948
		{
949
			if (retries > 3)
950
			{
951
				var message = String.Format("Monitoring of account {0} has failed too many times. Will not retry",
952
											monitor.UserName);
953
				_events.Publish(new Notification
954
									{Title = "Account monitoring failed", Message = message, Level = TraceLevel.Error});
955
				return true;
956
			}
957
			return false;
958
		}
959

    
960

    
961
	    private void TryLater(PithosMonitor monitor, Exception exc,int retries)
962
		{
963
			var message = String.Format("An exception occured. Can't start monitoring\nWill retry in 10 seconds");
964
			Task.Factory.StartNewDelayed(10000, () => StartMonitor(monitor,retries+1));
965
			_events.Publish(new Notification
966
								{Title = "Error", Message = message, Level = TraceLevel.Error});
967
			Log.Error(message, exc);
968
		}
969

    
970

    
971
		public void NotifyChange(string status, TraceLevel level=TraceLevel.Info)
972
		{
973
			StatusMessage = status;
974
			
975
			_events.Publish(new Notification { Title = "Pithos+", Message = status, Level = level });
976
		}
977

    
978
		public void NotifyChangedFile(string filePath)
979
		{
980
            if (RecentFiles.Any(e => e.FullPath == filePath))
981
                return;
982
            
983
			IProducerConsumerCollection<FileEntry> files=RecentFiles;
984
			FileEntry popped;
985
			while (files.Count > 5)
986
				files.TryTake(out popped);
987
            var entry = new FileEntry { FullPath = filePath };
988
			files.TryAdd(entry);
989
		}
990

    
991
		public void NotifyAccount(AccountInfo account)
992
		{
993
			if (account== null)
994
				return;
995
			//TODO: What happens to an existing account whose Token has changed?
996
			account.SiteUri= String.Format("{0}/ui/?token={1}&user={2}",
997
				account.SiteUri, Uri.EscapeDataString(account.Token),
998
				Uri.EscapeDataString(account.UserName));
999

    
1000
			if (!Accounts.Any(item => item.UserName == account.UserName && item.SiteUri == account.SiteUri))
1001
				Accounts.TryAdd(account);
1002

    
1003
		}
1004

    
1005
		public void NotifyConflicts(IEnumerable<FileSystemInfo> conflictFiles, string message)
1006
		{
1007
			if (conflictFiles == null)
1008
				return;
1009
		    //Convert to list to avoid multiple iterations
1010
            var files = conflictFiles.ToList();
1011
			if (files.Count==0)
1012
				return;
1013

    
1014
			UpdateStatus();
1015
			//TODO: Create a more specific message. For now, just show a warning
1016
			NotifyForFiles(files,message,TraceLevel.Warning);
1017

    
1018
		}
1019

    
1020
		public void NotifyForFiles(IEnumerable<FileSystemInfo> files, string message,TraceLevel level=TraceLevel.Info)
1021
		{
1022
			if (files == null)
1023
				return;
1024
			if (!files.Any())
1025
				return;
1026

    
1027
			StatusMessage = message;
1028

    
1029
			_events.Publish(new Notification { Title = "Pithos+", Message = message, Level = level});
1030
		}
1031

    
1032
		public void Notify(Notification notification)
1033
		{
1034
            TaskEx.Run(()=> _events.Publish(notification));
1035
		}
1036

    
1037

    
1038
		public void RemoveMonitor(string serverUrl,string accountName)
1039
		{
1040
			if (String.IsNullOrWhiteSpace(accountName))
1041
				return;
1042

    
1043
			var accountInfo=_accounts.FirstOrDefault(account => account.UserName == accountName && account.StorageUri.ToString().StartsWith(serverUrl));
1044
            if (accountInfo != null)
1045
            {
1046
                _accounts.TryRemove(accountInfo);
1047
                _pollAgent.RemoveAccount(accountInfo);
1048
            }
1049

    
1050
            var accountKey = new Uri(serverUrl).Combine(accountName);
1051
		    PithosMonitor monitor;
1052
			if (Monitors.TryRemove(accountKey, out monitor))
1053
			{
1054
				monitor.Stop();
1055
                //TODO: Also remove any pending actions for this account
1056
                //from the network queue                
1057
			}
1058
		}
1059

    
1060
		public void RefreshOverlays()
1061
		{
1062
			foreach (var pair in Monitors)
1063
			{
1064
				var monitor = pair.Value;
1065

    
1066
				var path = monitor.RootPath;
1067

    
1068
				if (String.IsNullOrWhiteSpace(path))
1069
					continue;
1070

    
1071
				if (!Directory.Exists(path) && !File.Exists(path))
1072
					continue;
1073

    
1074
				IntPtr pathPointer = Marshal.StringToCoTaskMemAuto(path);
1075

    
1076
				try
1077
				{
1078
					NativeMethods.SHChangeNotify(HChangeNotifyEventID.SHCNE_UPDATEITEM,
1079
												 HChangeNotifyFlags.SHCNF_PATHW | HChangeNotifyFlags.SHCNF_FLUSHNOWAIT,
1080
												 pathPointer, IntPtr.Zero);
1081
				}
1082
				finally
1083
				{
1084
					Marshal.FreeHGlobal(pathPointer);
1085
				}
1086
			}
1087
		}
1088

    
1089
		#region Event Handlers
1090
		
1091
		public void Handle(SelectiveSynchChanges message)
1092
		{
1093
		    TaskEx.Run(() =>
1094
		    {
1095
		        PithosMonitor monitor;
1096
		        if (Monitors.TryGetValue(message.Account.AccountKey, out monitor))
1097
		        {
1098
                    Selectives.SetIsSelectiveEnabled(message.Account.AccountKey, message.Enabled);
1099
		            monitor.SetSelectivePaths(message.Uris, message.Added, message.Removed);
1100
		        }
1101

    
1102
		        var account = Accounts.FirstOrDefault(acc => acc.AccountKey == message.Account.AccountKey);
1103
		        if (account != null)
1104
		        {
1105
		            var added=monitor.UrisToFilePaths(message.Added);
1106
                    _pollAgent.SynchNow(added);
1107
		        }
1108
		    });
1109

    
1110
		}
1111

    
1112

    
1113
		private bool _pollStarted;
1114
	    private Sparkle _sparkle;
1115
	    private bool _manualUpgradeCheck;
1116
	    private int _progress;
1117

    
1118
	    //SMELL: Doing so much work for notifications in the shell is wrong
1119
		//The notifications should be moved to their own view/viewmodel pair
1120
		//and different templates should be used for different message types
1121
		//This will also allow the addition of extra functionality, eg. actions
1122
		//
1123
		public void Handle(Notification notification)
1124
		{
1125
			UpdateStatus();
1126

    
1127
			if (!Settings.ShowDesktopNotifications)
1128
				return;
1129

    
1130
            var progress = notification as ProgressNotification;
1131

    
1132

    
1133
            if (progress != null)
1134
            {
1135
                double percentage = (progress.TotalBlocks == progress.Block) ? 1
1136
                    : (progress.Block + progress.BlockPercentage / 100.0) / (double)progress.TotalBlocks;
1137
                StatusMessage = String.Format("{0} {1:p2} of {2} - {3}",
1138
                                              progress.Action,
1139
                                              percentage,
1140
                                              progress.FileSize.ToByteSize(),
1141
                                              progress.FileName);
1142
                Progress = (int)(percentage * 100);
1143
                return;
1144
            }
1145

    
1146
            //If this is not a progress bar message, set the bar to 0
1147
            Progress = 0;
1148
            
1149
            if (notification is PollNotification)
1150
			{
1151
				_pollStarted = true;
1152
				return;
1153
			}
1154
			if (notification is CloudNotification)
1155
			{
1156
				if (!_pollStarted) 
1157
					return;
1158
				_pollStarted= false;
1159
				notification.Title = "Pithos+";
1160
				notification.Message = "Start Synchronisation";
1161
			}
1162

    
1163
		    var deleteNotification = notification as CloudDeleteNotification;
1164
            if (deleteNotification != null)
1165
            {
1166
                StatusMessage = String.Format("Deleted {0}", deleteNotification.Data.Name);
1167
                return;
1168
            }
1169

    
1170

    
1171
		    var info = notification as StatusNotification;
1172
            if (info != null)
1173
            {
1174
                StatusMessage = info.Title;
1175
                return;
1176
            }
1177
			if (String.IsNullOrWhiteSpace(notification.Message) && String.IsNullOrWhiteSpace(notification.Title))
1178
				return;
1179

    
1180
            if (notification.Level <= TraceLevel.Warning)
1181
			    ShowBalloonFor(notification);
1182
		}
1183

    
1184
	    private void ShowBalloonFor(Notification notification)
1185
	    {
1186
            Contract.Requires(notification!=null);
1187
            
1188
            if (!Settings.ShowDesktopNotifications) 
1189
                return;
1190
            
1191
            BalloonIcon icon;
1192
	        switch (notification.Level)
1193
	        {
1194
                case TraceLevel.Verbose:
1195
	                return;
1196
	            case TraceLevel.Info:	            
1197
	                icon = BalloonIcon.Info;
1198
	                break;
1199
                case TraceLevel.Error:
1200
                    icon = BalloonIcon.Error;
1201
                    break;
1202
                case TraceLevel.Warning:
1203
	                icon = BalloonIcon.Warning;
1204
	                break;
1205
	            default:
1206
	                return;
1207
	        }
1208

    
1209
	        var tv = (ShellView) GetView();
1210
	        System.Action clickAction = null;
1211
	        if (notification is ExpirationNotification)
1212
	        {
1213
	            clickAction = () => ShowPreferences("AccountTab");
1214
	        }
1215
	        var balloon = new PithosBalloon
1216
	                          {
1217
	                              Title = notification.Title,
1218
	                              Message = notification.Message,
1219
	                              Icon = icon,
1220
	                              ClickAction = clickAction
1221
	                          };
1222
	        tv.TaskbarView.ShowCustomBalloon(balloon, PopupAnimation.Fade, 4000);
1223
	    }
1224

    
1225
	    #endregion
1226

    
1227
		public void Handle(ShowFilePropertiesEvent message)
1228
		{
1229
			if (message == null)
1230
				throw new ArgumentNullException("message");
1231
			if (String.IsNullOrWhiteSpace(message.FileName) )
1232
				throw new ArgumentException("message");
1233
			Contract.EndContractBlock();
1234

    
1235
			var fileName = message.FileName;
1236
			//TODO: Display file properties for non-container folders
1237
			if (File.Exists(fileName))
1238
				//Retrieve the full name with exact casing. Pithos names are case sensitive				
1239
				ShowFileProperties(FileInfoExtensions.GetProperFilePathCapitalization(fileName));
1240
			else if (Directory.Exists(fileName))
1241
				//Retrieve the full name with exact casing. Pithos names are case sensitive
1242
			{
1243
				var path = FileInfoExtensions.GetProperDirectoryCapitalization(fileName);
1244
				if (IsContainer(path))
1245
					ShowContainerProperties(path);
1246
				else
1247
					ShowFileProperties(path);
1248
			}
1249
		}
1250

    
1251
		private bool IsContainer(string path)
1252
		{
1253
			var matchingFolders = from account in _accounts
1254
								  from rootFolder in Directory.GetDirectories(account.AccountPath)
1255
								  where rootFolder.Equals(path, StringComparison.InvariantCultureIgnoreCase)
1256
								  select rootFolder;
1257
			return matchingFolders.Any();
1258
		}
1259

    
1260
		public FileStatus GetFileStatus(string localFileName)
1261
		{
1262
			if (String.IsNullOrWhiteSpace(localFileName))
1263
				throw new ArgumentNullException("localFileName");
1264
			Contract.EndContractBlock();
1265
			
1266
			var statusKeeper = IoC.Get<IStatusKeeper>();
1267
			var status=statusKeeper.GetFileStatus(localFileName);
1268
			return status;
1269
		}
1270

    
1271
	    public void RemoveAccountFromDatabase(AccountSettings account)
1272
	    {
1273
            var statusKeeper = IoC.Get<IStatusKeeper>();
1274
            statusKeeper.ClearFolderStatus(account.RootPath);	        
1275
	    }
1276
	}
1277
}