Statistics
| Branch: | Revision:

root / trunk / Pithos.Client.WPF / Shell / ShellViewModel.cs @ 2115e2a5

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 Task 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 async Task MonitorAccount(AccountSettings account)
363
		{
364
		    await TaskEx.Yield();                                                
365
				PithosMonitor monitor;
366
				var accountName = account.AccountName;
367

    
368
			    MigrateFolders(account);
369

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

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

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

    
406
				monitor.AuthenticationUrl = account.ServerUrl;
407

    
408
				Monitors[account.AccountKey] = monitor;
409

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

    
423

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

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

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

    
448

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

    
457

    
458
		#region Status Properties
459

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

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

    
482
	    public string VersionMessage { get; set; }
483

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

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

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

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

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

    
519

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

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

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

    
547

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

    
560
		#endregion
561

    
562
		#region Commands
563

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

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

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

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

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

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

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

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

    
615
		
616

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

    
623

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

    
630
	    private bool _statusVisible;
631

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

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

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

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

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

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

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

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

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

    
697
			
698

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

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

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

    
727
			
728

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

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

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

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

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

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

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

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

    
784

    
785
		}
786

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

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

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

    
811
	    #endregion
812

    
813

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

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

    
826

    
827
        private PithosStatus _pithosStatus = PithosStatus.Disconnected;
828

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

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

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

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

    
861

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

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

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

    
878

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

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

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

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

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

    
931
		}
932

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

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

    
959

    
960
	    private void TryLater(PithosMonitor monitor, Exception exc,int retries)
961
		{
962
			var message = String.Format("An exception occured. Can't start monitoring\nWill retry in 10 seconds");
963
            
964
			TaskEx.Delay(10000).ContinueWith(t=>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
}