Some timeout issues
[pithos-ms-client] / trunk / Pithos.Client.WPF / Shell / ShellViewModel.cs
1 #region\r
2 /* -----------------------------------------------------------------------\r
3  * <copyright file="ShellViewModel.cs" company="GRNet">\r
4  * \r
5  * Copyright 2011-2012 GRNET S.A. All rights reserved.\r
6  *\r
7  * Redistribution and use in source and binary forms, with or\r
8  * without modification, are permitted provided that the following\r
9  * conditions are met:\r
10  *\r
11  *   1. Redistributions of source code must retain the above\r
12  *      copyright notice, this list of conditions and the following\r
13  *      disclaimer.\r
14  *\r
15  *   2. Redistributions in binary form must reproduce the above\r
16  *      copyright notice, this list of conditions and the following\r
17  *      disclaimer in the documentation and/or other materials\r
18  *      provided with the distribution.\r
19  *\r
20  *\r
21  * THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS\r
22  * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\r
23  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\r
24  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR\r
25  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\r
26  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\r
27  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF\r
28  * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED\r
29  * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT\r
30  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN\r
31  * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\r
32  * POSSIBILITY OF SUCH DAMAGE.\r
33  *\r
34  * The views and conclusions contained in the software and\r
35  * documentation are those of the authors and should not be\r
36  * interpreted as representing official policies, either expressed\r
37  * or implied, of GRNET S.A.\r
38  * </copyright>\r
39  * -----------------------------------------------------------------------\r
40  */\r
41 #endregion\r
42 using System.Collections.Concurrent;\r
43 using System.Diagnostics;\r
44 using System.Diagnostics.Contracts;\r
45 using System.IO;\r
46 using System.Net;\r
47 using System.Reflection;\r
48 using System.Runtime.InteropServices;\r
49 using System.ServiceModel;\r
50 using System.Threading;\r
51 using System.Threading.Tasks;\r
52 using System.Windows;\r
53 using System.Windows.Controls.Primitives;\r
54 using System.Windows.Input;\r
55 using AppLimit.NetSparkle;\r
56 using Caliburn.Micro;\r
57 using Hardcodet.Wpf.TaskbarNotification;\r
58 using Pithos.Client.WPF.Configuration;\r
59 using Pithos.Client.WPF.FileProperties;\r
60 using Pithos.Client.WPF.Preferences;\r
61 using Pithos.Client.WPF.SelectiveSynch;\r
62 using Pithos.Client.WPF.Services;\r
63 using Pithos.Client.WPF.Shell;\r
64 using Pithos.Core;\r
65 using Pithos.Core.Agents;\r
66 using Pithos.Interfaces;\r
67 using System;\r
68 using System.Collections.Generic;\r
69 using System.Linq;\r
70 using Pithos.Network;\r
71 using StatusService = Pithos.Client.WPF.Services.StatusService;\r
72 \r
73 namespace Pithos.Client.WPF {\r
74         using System.ComponentModel.Composition;\r
75 \r
76         public class ToggleStatusCommand:ICommand\r
77         {\r
78             private readonly ShellViewModel _model;\r
79             public ToggleStatusCommand(ShellViewModel model)\r
80             {\r
81                 _model = model;\r
82             }\r
83             public void Execute(object parameter)\r
84             {\r
85                 _model.CurrentSyncStatus();\r
86             }\r
87 \r
88             public bool CanExecute(object parameter)\r
89             {\r
90                 return true;\r
91             }\r
92 \r
93             public event EventHandler CanExecuteChanged;\r
94         }\r
95 \r
96 \r
97         ///<summary>\r
98         /// The "shell" of the Pithos application displays the taskbar  icon, menu and notifications.\r
99         /// The shell also hosts the status service called by shell extensions to retrieve file info\r
100         ///</summary>\r
101         ///<remarks>\r
102         /// It is a strange "shell" as its main visible element is an icon instead of a window\r
103         /// The shell subscribes to the following events:\r
104         /// * Notification:  Raised by components that want to notify the user. Usually displayed in a balloon\r
105         /// * SelectiveSynchChanges: Notifies that the user made changes to the selective synch folders for an account. Raised by the Selective Synch dialog. Located here because the monitors are here\r
106         /// * ShowFilePropertiesEvent: Raised when a shell command requests the display of the file/container properties dialog\r
107         ///</remarks>           \r
108         //TODO: CODE SMELL Why does the shell handle the SelectiveSynchChanges?\r
109     [Export(typeof(IShell)), Export(typeof(ShellViewModel)),Export(typeof(IStatusNotification))]\r
110         public class ShellViewModel : Screen, IStatusNotification, IShell,\r
111                 IHandle<Notification>, IHandle<SelectiveSynchChanges>, IHandle<ShowFilePropertiesEvent>\r
112         {\r
113                 \r
114                 private readonly IEventAggregator _events;\r
115 \r
116                 public PithosSettings Settings { get; private set; }\r
117 \r
118 \r
119                 private readonly ConcurrentDictionary<Uri, PithosMonitor> _monitors = new ConcurrentDictionary<Uri, PithosMonitor>();\r
120                 ///<summary>\r
121                 /// Dictionary of account monitors, keyed by account\r
122                 ///</summary>\r
123                 ///<remarks>\r
124                 /// One monitor class is created for each account. The Shell needs access to the monitors to execute start/stop/pause commands,\r
125                 /// retrieve account and boject info            \r
126                 ///</remarks>\r
127                 // TODO: Does the Shell REALLY need access to the monitors? Could we achieve the same results with a better design?\r
128                 // TODO: The monitors should be internal to Pithos.Core, even though exposing them makes coding of the Object and Container windows easier\r
129                 public ConcurrentDictionary<Uri, PithosMonitor> Monitors\r
130                 {\r
131                         get { return _monitors; }\r
132                 }\r
133 \r
134 \r
135                 ///<summary>\r
136                 /// The status service is used by Shell extensions to retrieve file status information\r
137                 ///</summary>\r
138                 //TODO: CODE SMELL! This is the shell! While hosting in the shell makes executing start/stop commands easier, it is still a smell\r
139                 private ServiceHost _statusService;\r
140 \r
141                 //Logging in the Pithos client is provided by log4net\r
142         private static readonly log4net.ILog Log = log4net.LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);\r
143 \r
144         [Import]\r
145             private PollAgent _pollAgent;\r
146 \r
147         [Import]\r
148             private NetworkAgent _networkAgent;\r
149 \r
150             [Import]\r
151             public Selectives Selectives { get; set; }\r
152 \r
153 \r
154             public ToggleStatusCommand ToggleMiniStatusCommand { get; set; }\r
155 \r
156             private MiniStatusViewModel _miniStatus;\r
157 \r
158             [Import]\r
159         public MiniStatusViewModel MiniStatus\r
160             {\r
161                 get { return _miniStatus; }\r
162                 set\r
163                 {\r
164                     _miniStatus = value;\r
165                     _miniStatus.Shell = this;\r
166                     _miniStatus.Deactivated += (sender, arg) =>\r
167                                                    {\r
168                                                        _statusVisible = false;\r
169                                                    NotifyOfPropertyChange(()=>MiniStatusCaption);\r
170                                                    };\r
171                 }\r
172             }\r
173 \r
174             ///<summary>\r
175                 /// The Shell depends on MEF to provide implementations for windowManager, events, the status checker service and the settings\r
176                 ///</summary>\r
177                 ///<remarks>\r
178                 /// The PithosSettings class encapsulates the app's settings to abstract their storage mechanism (App settings, a database or registry)\r
179                 ///</remarks>\r
180                 [ImportingConstructor]          \r
181                 public ShellViewModel(IWindowManager windowManager, IEventAggregator events, PithosSettings settings/*,PollAgent pollAgent,NetworkAgent networkAgent*/)\r
182                 {\r
183                         try\r
184                         {\r
185 \r
186                                 _windowManager = windowManager;\r
187                                 //CHECK: Caliburn doesn't need explicit command construction\r
188                                 //CurrentSyncStatusCommand = new PithosCommand(OpenPithosFolder);\r
189                                 //The event subst\r
190                                 _events = events;\r
191                                 _events.Subscribe(this);\r
192 \r
193 /*\r
194                             _pollAgent = pollAgent;\r
195                             _networkAgent = networkAgent;\r
196 */\r
197                                 Settings = settings;\r
198 \r
199                                 Proxy.SetFromSettings(settings);\r
200 \r
201                 StatusMessage = Settings.Accounts.Count==0 \r
202                     ? "No Accounts added\r\nPlease add an account" \r
203                     : "Starting";\r
204 \r
205                                 _accounts.CollectionChanged += (sender, e) =>\r
206                                                                                                    {\r
207                                                                                                            NotifyOfPropertyChange(() => OpenFolderCaption);\r
208                                                                                                            NotifyOfPropertyChange(() => HasAccounts);\r
209                                                                                                    };\r
210 \r
211                 SetVersionMessage();\r
212 \r
213                 ToggleMiniStatusCommand=new ToggleStatusCommand(this);\r
214                         }\r
215                         catch (Exception exc)\r
216                         {\r
217                                 Log.Error("Error while starting the ShellViewModel",exc);\r
218                                 throw;\r
219                         }\r
220 \r
221                 }\r
222 \r
223             private void SetVersionMessage()\r
224             {\r
225                 Assembly assembly = Assembly.GetExecutingAssembly();\r
226                 var fileVersion = FileVersionInfo.GetVersionInfo(assembly.Location);\r
227                 VersionMessage = String.Format("Pithos+ {0}", fileVersion.FileVersion);\r
228             }\r
229 \r
230         public void CurrentSyncStatus()\r
231         {\r
232             if (Accounts.Count == 0)\r
233             {\r
234                 ShowPreferences("AccountTab");\r
235             }\r
236             else\r
237             {\r
238                 if (!_statusVisible)\r
239                 {\r
240                     _windowManager.ShowWindow(MiniStatus);\r
241                     _statusVisible = true;\r
242                 }\r
243                 else\r
244                 {\r
245                     if (MiniStatus.IsActive)\r
246                         MiniStatus.TryClose();\r
247                     _statusVisible = false;\r
248                 }\r
249 \r
250                 NotifyOfPropertyChange(() => MiniStatusCaption);\r
251             }\r
252         }\r
253 \r
254             protected override void OnActivate()\r
255                 {\r
256                         base.OnActivate();\r
257 \r
258             InitializeSparkle();\r
259 \r
260                 //Must delay opening the upgrade window\r
261             //to avoid Windows Messages sent by the TaskbarIcon\r
262             TaskEx.Delay(5000).ContinueWith(_=>\r
263                 Execute.OnUIThread(()=> _sparkle.StartLoop(true,Settings.UpdateForceCheck,Settings.UpdateCheckInterval)));\r
264 \r
265 \r
266                         StartMonitoring();                    \r
267                 }\r
268 \r
269 \r
270             private void OnCheckFinished(object sender, bool updaterequired)\r
271             {\r
272             \r
273             Log.InfoFormat("Upgrade check finished. Need Upgrade: {0}", updaterequired);\r
274             if (_manualUpgradeCheck)\r
275             {\r
276                 _manualUpgradeCheck = false;\r
277                 if (!updaterequired)\r
278                     //Sparkle raises events on a background thread\r
279                     Execute.OnUIThread(()=>\r
280                         ShowBalloonFor(new Notification{Title="Pithos+ is up to date",Message="You have the latest Pithos+ version. No update is required"}));\r
281             }\r
282             }\r
283 \r
284             private void OnUpgradeDetected(object sender, UpdateDetectedEventArgs e)\r
285             {            \r
286                 Log.InfoFormat("Update detected {0}",e.LatestVersion);\r
287             }\r
288 \r
289         public void CheckForUpgrade()\r
290         {\r
291             ShowBalloonFor(new Notification{Title="Checking for upgrades",Message="Contacting the server to retrieve the latest Pithos+ version."});\r
292             _sparkle.StopLoop();\r
293             _sparkle.updateDetected -= OnUpgradeDetected;\r
294             _sparkle.checkLoopFinished -= OnCheckFinished;\r
295             _sparkle.Dispose();\r
296 \r
297             _manualUpgradeCheck = true;\r
298             InitializeSparkle();\r
299             _sparkle.StartLoop(true,true,Settings.UpdateCheckInterval);\r
300         }\r
301 \r
302         private void InitializeSparkle()\r
303         {\r
304             _sparkle = new Sparkle(Settings.UpdateUrl);\r
305             _sparkle.updateDetected += OnUpgradeDetected;\r
306             _sparkle.checkLoopFinished += OnCheckFinished;\r
307             _sparkle.ShowDiagnosticWindow = Settings.UpdateDiagnostics;\r
308         }\r
309 \r
310             private async void StartMonitoring()\r
311                 {\r
312                         try\r
313                         {\r
314                 if (Settings.IgnoreCertificateErrors)\r
315                 {\r
316                     ServicePointManager.ServerCertificateValidationCallback = (sender, certificate, chain, errors) => true;\r
317                 }\r
318                     \r
319                                 var accounts = Settings.Accounts.Select(MonitorAccount);\r
320                 await TaskEx.WhenAll(accounts).ConfigureAwait(false);\r
321                                 _statusService = StatusService.Start();\r
322 \r
323                         }\r
324                         catch (AggregateException exc)\r
325                         {\r
326                                 exc.Handle(e =>\r
327                                 {\r
328                                         Log.Error("Error while starting monitoring", e);\r
329                                         return true;\r
330                                 });\r
331                                 throw;\r
332                         }\r
333                 }\r
334 \r
335                 protected override void OnDeactivate(bool close)\r
336                 {\r
337                         base.OnDeactivate(close);\r
338                         if (close)\r
339                         {\r
340                                 StatusService.Stop(_statusService);\r
341                                 _statusService = null;\r
342                         }\r
343                 }\r
344 \r
345                 public Task MonitorAccount(AccountSettings account)\r
346                 {\r
347                         return Task.Factory.StartNew(() =>\r
348                         {                                                \r
349                                 PithosMonitor monitor;\r
350                                 var accountName = account.AccountName;\r
351 \r
352                             MigrateFolders(account);\r
353 \r
354                             Selectives.SetIsSelectiveEnabled(account.AccountKey, account.SelectiveSyncEnabled);\r
355 \r
356                                 if (Monitors.TryGetValue(account.AccountKey, out monitor))\r
357                                 {\r
358                                         //If the account is active\r
359                     if (account.IsActive)\r
360                     {\r
361                         //The Api Key may have changed throuth the Preferences dialog\r
362                         monitor.ApiKey = account.ApiKey;\r
363                                                 Debug.Assert(monitor.StatusNotification == this,"An existing monitor should already have a StatusNotification service object");\r
364                         monitor.RootPath = account.RootPath;\r
365                         //Start the monitor. It's OK to start an already started monitor,\r
366                         //it will just ignore the call                        \r
367                         StartMonitor(monitor).Wait();\r
368                     }\r
369                     else\r
370                     {\r
371                         //If the account is inactive\r
372                         //Stop and remove the monitor\r
373                         RemoveMonitor(account.ServerUrl,accountName);\r
374                     }\r
375                                         return;\r
376                                 }\r
377 \r
378                                 \r
379                                 //Create a new monitor/ Can't use MEF here, it would return a single instance for all monitors\r
380                                 monitor = new PithosMonitor\r
381                                                           {\r
382                                                                   UserName = accountName,\r
383                                                                   ApiKey = account.ApiKey,                                  \r
384                                                                   StatusNotification = this,\r
385                                                                   RootPath = account.RootPath\r
386                                                           };\r
387                                 //PithosMonitor uses MEF so we need to resolve it\r
388                                 IoC.BuildUp(monitor);\r
389 \r
390                                 monitor.AuthenticationUrl = account.ServerUrl;\r
391 \r
392                                 Monitors[account.AccountKey] = monitor;\r
393 \r
394                                 if (account.IsActive)\r
395                                 {\r
396                                         //Don't start a monitor if it doesn't have an account and ApiKey\r
397                                         if (String.IsNullOrWhiteSpace(monitor.UserName) ||\r
398                                                 String.IsNullOrWhiteSpace(monitor.ApiKey))\r
399                                                 return;\r
400                                         StartMonitor(monitor);\r
401                                 }\r
402                         });\r
403                 }\r
404 \r
405             private void MigrateFolders(AccountSettings account)\r
406             {\r
407                 var oldOthersFolder=Path.Combine(account.RootPath, FolderConstants.OldOthersFolder);\r
408                 var newOthersFolder = Path.Combine(account.RootPath, FolderConstants.OthersFolder);\r
409                 var oldFolder = new DirectoryInfo(oldOthersFolder);\r
410                 var newFolder = new DirectoryInfo(newOthersFolder);\r
411 \r
412             if (oldFolder.Exists && !newFolder.Exists)\r
413             {\r
414                 oldFolder.MoveTo(newOthersFolder);\r
415             }\r
416             }\r
417 \r
418 \r
419             protected override void OnViewLoaded(object view)\r
420                 {\r
421                         UpdateStatus();\r
422                         var window = (Window)view;            \r
423                         TaskEx.Delay(1000).ContinueWith(t => Execute.OnUIThread(window.Hide));\r
424                         base.OnViewLoaded(view);\r
425                 }\r
426 \r
427 \r
428                 #region Status Properties\r
429 \r
430                 private string _statusMessage;\r
431                 public string StatusMessage\r
432                 {\r
433                         get { return _statusMessage; }\r
434                         set\r
435                         {\r
436                                 _statusMessage = value;\r
437                                 NotifyOfPropertyChange(() => StatusMessage);\r
438                 NotifyOfPropertyChange(() => TooltipMessage);\r
439                         }\r
440                 }\r
441 \r
442         public string VersionMessage { get; set; }\r
443 \r
444             public string TooltipMessage\r
445             {\r
446                 get\r
447                 {\r
448                     return String.Format("{0}\r\n{1}",VersionMessage,StatusMessage);\r
449                 }\r
450             }\r
451 \r
452         public string TooltipMiniStatus\r
453         {\r
454             get\r
455             {\r
456                 return String.Format("{0}\r\n{1}", "Status Window", "Enable / Disable the status window");\r
457             }\r
458         }\r
459 \r
460         /*public string ToggleStatusWindowMessage\r
461         {\r
462             get\r
463             {\r
464                 return String.Format("{0}" + Environment.NewLine + "{1} Toggle Mini Status");\r
465             }\r
466         }*/\r
467 \r
468             private readonly ObservableConcurrentCollection<AccountInfo> _accounts = new ObservableConcurrentCollection<AccountInfo>();\r
469                 public ObservableConcurrentCollection<AccountInfo> Accounts\r
470                 {\r
471                         get { return _accounts; }\r
472                 }\r
473 \r
474                 public bool HasAccounts\r
475                 {\r
476                         get { return _accounts.Count > 0; }\r
477                 }\r
478 \r
479 \r
480                 public string OpenFolderCaption\r
481                 {\r
482                         get\r
483                         {\r
484                                 return (_accounts.Count == 0)\r
485                                                 ? "No Accounts Defined"\r
486                                                 : "Open Pithos Folder";\r
487                         }\r
488                 }\r
489 \r
490                 private string _pauseSyncCaption="Pause Synching";\r
491                 public string PauseSyncCaption\r
492                 {\r
493                         get { return _pauseSyncCaption; }\r
494                         set\r
495                         {\r
496                                 _pauseSyncCaption = value;\r
497                                 NotifyOfPropertyChange(() => PauseSyncCaption);\r
498                         }\r
499                 }\r
500 \r
501                 private readonly ObservableConcurrentCollection<FileEntry> _recentFiles = new ObservableConcurrentCollection<FileEntry>();\r
502                 public ObservableConcurrentCollection<FileEntry> RecentFiles\r
503                 {\r
504                         get { return _recentFiles; }\r
505                 }\r
506 \r
507 \r
508                 private string _statusIcon="../Images/Pithos.ico";\r
509                 public string StatusIcon\r
510                 {\r
511                         get { return _statusIcon; }\r
512                         set\r
513                         {\r
514                                 //TODO: Ensure all status icons use the Pithos logo\r
515                                 _statusIcon = value;\r
516                                 NotifyOfPropertyChange(() => StatusIcon);\r
517                         }\r
518                 }\r
519 \r
520                 #endregion\r
521 \r
522                 #region Commands\r
523 \r
524         public void CancelCurrentOperation()\r
525         {\r
526             _pollAgent.CancelCurrentOperation();\r
527         }\r
528 \r
529         public void ShowPreferences()\r
530         {\r
531             ShowPreferences(null);\r
532         }\r
533 \r
534                 public void ShowPreferences(string currentTab)\r
535                 {\r
536                         //Settings.Reload();\r
537             \r
538                     var preferences = IoC.Get<PreferencesViewModel>();//??new PreferencesViewModel(_windowManager, _events, this, Settings,currentTab);\r
539             if (!String.IsNullOrWhiteSpace(currentTab))\r
540                 preferences.SelectedTab = currentTab;\r
541             if (!preferences.IsActive)\r
542                         _windowManager.ShowWindow(preferences);\r
543             var view = (Window)preferences.GetView();\r
544             view.NullSafe(v=>v.Activate());\r
545                 }\r
546 \r
547                 public void AboutPithos()\r
548                 {\r
549                         var about = IoC.Get<AboutViewModel>();\r
550                     about.LatestVersion=_sparkle.LatestVersion;\r
551                         _windowManager.ShowWindow(about);\r
552                 }\r
553 \r
554                 public void SendFeedback()\r
555                 {\r
556                         var feedBack =  IoC.Get<FeedbackViewModel>();\r
557                         _windowManager.ShowWindow(feedBack);\r
558                 }\r
559 \r
560                 //public PithosCommand OpenPithosFolderCommand { get; private set; }\r
561 \r
562                 public void OpenPithosFolder()\r
563                 {\r
564                         var account = Settings.Accounts.FirstOrDefault(acc => acc.IsActive);\r
565                         if (account == null)\r
566                                 return;\r
567                         Process.Start(account.RootPath);\r
568                 }\r
569 \r
570                 public void OpenPithosFolder(AccountInfo account)\r
571                 {\r
572                         Process.Start(account.AccountPath);\r
573                 }\r
574 \r
575                 \r
576 \r
577                 public void GoToSite()\r
578                 {            \r
579                         var site = Properties.Settings.Default.ProductionServer;\r
580                         Process.Start(site);            \r
581                 }\r
582 \r
583 \r
584                 public void GoToSite(AccountInfo account)\r
585                 {\r
586                     var uri = account.SiteUri.Replace("http://","https://");            \r
587                     Process.Start(uri);\r
588                 }\r
589 \r
590             private bool _statusVisible;\r
591 \r
592             public string MiniStatusCaption\r
593             {\r
594                 get\r
595                 {\r
596                     return  _statusVisible ? "Hide Status Window" : "Show Status Window";\r
597                 }\r
598             }\r
599 \r
600             public bool HasConflicts\r
601             {\r
602             get { return true; }\r
603             }\r
604         public void ShowConflicts()\r
605         {\r
606             _windowManager.ShowWindow(IoC.Get<ConflictsViewModel>());            \r
607         }\r
608 \r
609             /// <summary>\r
610         /// Open an explorer window to the target path's directory\r
611         /// and select the file\r
612         /// </summary>\r
613         /// <param name="entry"></param>\r
614         public void GoToFile(FileEntry entry)\r
615         {\r
616             var fullPath = entry.FullPath;\r
617             if (!File.Exists(fullPath) && !Directory.Exists(fullPath))\r
618                 return;\r
619             Process.Start("explorer.exe","/select, " + fullPath);\r
620         }\r
621 \r
622         public void OpenLogPath()\r
623         {\r
624             var pithosDataPath = PithosSettings.PithosDataPath;\r
625 \r
626             Process.Start(pithosDataPath);\r
627         }\r
628         \r
629         public void ShowFileProperties()\r
630                 {\r
631                         var account = Settings.Accounts.First(acc => acc.IsActive);            \r
632                         var dir = new DirectoryInfo(account.RootPath + @"\pithos");\r
633                         var files=dir.GetFiles();\r
634                         var r=new Random();\r
635                         var idx=r.Next(0, files.Length);\r
636                         ShowFileProperties(files[idx].FullName);            \r
637                 }\r
638 \r
639                 public async void ShowFileProperties(string filePath)\r
640                 {\r
641                         if (String.IsNullOrWhiteSpace(filePath))\r
642                                 throw new ArgumentNullException("filePath");\r
643                         if (!File.Exists(filePath) && !Directory.Exists(filePath))\r
644                                 throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath");\r
645                         Contract.EndContractBlock();\r
646 \r
647                         var pair=(from monitor in  Monitors\r
648                                                            where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)\r
649                                                                    select monitor).FirstOrDefault();\r
650                         var accountMonitor = pair.Value;\r
651 \r
652                         if (accountMonitor == null)\r
653                                 return;\r
654 \r
655                         var infoTask=accountMonitor.GetObjectInfo(filePath);\r
656 \r
657                         \r
658 \r
659                         var fileProperties = new FilePropertiesViewModel(this, infoTask,filePath);\r
660                         _windowManager.ShowWindow(fileProperties);\r
661                 } \r
662                 \r
663                 public void ShowContainerProperties()\r
664                 {\r
665                         var account = Settings.Accounts.First(acc => acc.IsActive);            \r
666                         var dir = new DirectoryInfo(account.RootPath);\r
667                         var fullName = (from folder in dir.EnumerateDirectories()\r
668                                                         where (folder.Attributes & FileAttributes.Hidden) == 0\r
669                                                         select folder.FullName).First();\r
670                         ShowContainerProperties(fullName);            \r
671                 }\r
672 \r
673                 public void ShowContainerProperties(string filePath)\r
674                 {\r
675                         if (String.IsNullOrWhiteSpace(filePath))\r
676                                 throw new ArgumentNullException("filePath");\r
677                         if (!Directory.Exists(filePath))\r
678                                 throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath");\r
679                         Contract.EndContractBlock();\r
680 \r
681                         var pair=(from monitor in  Monitors\r
682                                                            where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)\r
683                                                                    select monitor).FirstOrDefault();\r
684                         var accountMonitor = pair.Value;            \r
685                         var info = accountMonitor.GetContainerInfo(filePath);\r
686 \r
687                         \r
688 \r
689                         var containerProperties = new ContainerPropertiesViewModel(this, info,filePath);\r
690                         _windowManager.ShowWindow(containerProperties);\r
691                 }\r
692 \r
693                 public void SynchNow()\r
694                 {\r
695                         _pollAgent.SynchNow();\r
696                 }\r
697 \r
698                 public async Task<ObjectInfo> RefreshObjectInfo(ObjectInfo currentInfo)\r
699                 {\r
700                         if (currentInfo==null)\r
701                                 throw new ArgumentNullException("currentInfo");\r
702                         Contract.EndContractBlock();                \r
703             var monitor = Monitors[currentInfo.AccountKey];\r
704                         var newInfo=await monitor.CloudClient.GetObjectInfo(currentInfo.Account, currentInfo.Container, currentInfo.Name).ConfigureAwait(false);\r
705                         return newInfo;\r
706                 }\r
707 \r
708                 public ContainerInfo RefreshContainerInfo(ContainerInfo container)\r
709                 {\r
710                         if (container == null)\r
711                                 throw new ArgumentNullException("container");\r
712                         Contract.EndContractBlock();\r
713 \r
714                         var monitor = Monitors[container.AccountKey];\r
715                         var newInfo = monitor.CloudClient.GetContainerInfo(container.Account, container.Name);\r
716                         return newInfo;\r
717                 }\r
718 \r
719             private bool _isPaused;\r
720             public bool IsPaused\r
721             {\r
722                 get { return _isPaused; }\r
723                 set\r
724                 {\r
725                     _isPaused = value;\r
726                 PauseSyncCaption = IsPaused ? "Resume syncing" : "Pause syncing";\r
727                 var iconKey = IsPaused ? "TraySyncPaused" : "TrayInSynch";\r
728                 StatusIcon = String.Format(@"../Images/{0}.ico", iconKey);\r
729 \r
730                 NotifyOfPropertyChange(() => IsPaused);\r
731                 }\r
732             }\r
733 \r
734             public void ToggleSynching()\r
735                 {\r
736                         IsPaused=!IsPaused;\r
737                         foreach (var monitor in Monitors.Values)\r
738                         {\r
739                             monitor.Pause = IsPaused ;\r
740                         }\r
741             _pollAgent.Pause = IsPaused;\r
742             _networkAgent.Pause = IsPaused;\r
743 \r
744 \r
745                 }\r
746 \r
747         public void ExitPithos()\r
748         {\r
749             try\r
750             {\r
751 \r
752                 foreach (var monitor in Monitors.Select(pair => pair.Value))\r
753                 {\r
754                     monitor.Stop();\r
755                 }\r
756 \r
757                 var view = GetView() as Window;\r
758                 if (view != null)\r
759                     view.Close();\r
760             }\r
761             catch (Exception exc)\r
762             {\r
763                 Log.Info("Exception while exiting", exc);                \r
764             }\r
765             finally\r
766             {\r
767                 Application.Current.Shutdown();\r
768             }\r
769         }\r
770 \r
771             #endregion\r
772 \r
773 \r
774                 private readonly Dictionary<PithosStatus, StatusInfo> _iconNames = new List<StatusInfo>\r
775                         {\r
776                                 new StatusInfo(PithosStatus.InSynch, "All files up to date", "TrayInSynch"),\r
777                                 new StatusInfo(PithosStatus.PollSyncing, "Polling Files", "TraySynching"),\r
778                 new StatusInfo(PithosStatus.LocalSyncing, "Syncing Files", "TraySynching"),\r
779                                 new StatusInfo(PithosStatus.SyncPaused, "Sync Paused", "TraySyncPaused")\r
780                         }.ToDictionary(s => s.Status);\r
781 \r
782                 readonly IWindowManager _windowManager;\r
783                 \r
784         //private int _syncCount=0;\r
785 \r
786 \r
787         private PithosStatus _pithosStatus = PithosStatus.Disconnected;\r
788 \r
789         public void SetPithosStatus(PithosStatus status)\r
790         {\r
791             if (_pithosStatus == PithosStatus.LocalSyncing && status == PithosStatus.PollComplete)\r
792                 return;\r
793             if (_pithosStatus == PithosStatus.PollSyncing && status == PithosStatus.LocalComplete)\r
794                 return;\r
795             if (status == PithosStatus.LocalComplete || status == PithosStatus.PollComplete)\r
796                 _pithosStatus = PithosStatus.InSynch;\r
797             else\r
798                 _pithosStatus = status;\r
799             UpdateStatus();\r
800         }\r
801 \r
802         public void SetPithosStatus(PithosStatus status,string message)\r
803         {\r
804             StatusMessage = message;\r
805             SetPithosStatus(status);\r
806         }\r
807 \r
808           /*  public Notifier GetNotifier(Notification startNotification, Notification endNotification)\r
809             {\r
810                 return new Notifier(this, startNotification, endNotification);\r
811             }*/\r
812 \r
813             public Notifier GetNotifier(string startMessage, string endMessage, bool isActive=true,params object[] args)\r
814             {\r
815                 return isActive?new Notifier(this, \r
816                 new StatusNotification(String.Format(startMessage,args)), \r
817                 new StatusNotification(String.Format(endMessage,args)))\r
818                 :new Notifier(this,(Notification) null,null);\r
819             }\r
820 \r
821 \r
822             ///<summary>\r
823                 /// Updates the visual status indicators of the application depending on status changes, e.g. icon, stat                \r
824                 ///</summary>\r
825                 public void UpdateStatus()\r
826                 {\r
827 \r
828                         if (_iconNames.ContainsKey(_pithosStatus))\r
829                         {\r
830                                 var info = _iconNames[_pithosStatus];\r
831                                 StatusIcon = String.Format(@"../Images/{0}.ico", info.IconName);\r
832                         }\r
833 \r
834             if (_pithosStatus == PithosStatus.InSynch)\r
835                 StatusMessage = "All files up to date";\r
836                 }\r
837 \r
838 \r
839            \r
840                 private Task StartMonitor(PithosMonitor monitor,int retries=0)\r
841                 {\r
842                         return Task.Factory.StartNew(() =>\r
843                         {\r
844                                 using (log4net.ThreadContext.Stacks["Monitor"].Push("Start"))\r
845                                 {\r
846                                         try\r
847                                         {\r
848                                                 Log.InfoFormat("Start Monitoring {0}", monitor.UserName);\r
849 \r
850                                                 monitor.Start();\r
851                                         }\r
852                                         catch (WebException exc)\r
853                                         {\r
854                                                 if (AbandonRetry(monitor, retries))\r
855                                                         return;\r
856 \r
857                                                 HttpStatusCode statusCode =HttpStatusCode.OK;\r
858                                                 var response = exc.Response as HttpWebResponse;\r
859                                                 if(response!=null)\r
860                                                         statusCode = response.StatusCode;\r
861 \r
862                                                 switch (statusCode)\r
863                                                 {\r
864                                                         case HttpStatusCode.Unauthorized:\r
865                                                                 var message = String.Format("API Key Expired for {0}. Starting Renewal",\r
866                                                                                                                         monitor.UserName);\r
867                                                                 Log.Error(message, exc);\r
868                                 var account = Settings.Accounts.Find(acc => acc.AccountKey == new Uri(monitor.AuthenticationUrl).Combine(monitor.UserName));                                \r
869                                                         account.IsExpired = true;\r
870                                 Notify(new ExpirationNotification(account));\r
871                                                                 //TryAuthorize(monitor.UserName, retries).Wait();\r
872                                                                 break;\r
873                                                         case HttpStatusCode.ProxyAuthenticationRequired:\r
874                                                                 TryAuthenticateProxy(monitor,retries);\r
875                                                                 break;\r
876                                                         default:\r
877                                                                 TryLater(monitor, exc, retries);\r
878                                                                 break;\r
879                                                 }\r
880                                         }\r
881                                         catch (Exception exc)\r
882                                         {\r
883                                                 if (AbandonRetry(monitor, retries)) \r
884                                                         return;\r
885 \r
886                                                 TryLater(monitor,exc,retries);\r
887                                         }\r
888                                 }\r
889                         });\r
890                 }\r
891 \r
892                 private void TryAuthenticateProxy(PithosMonitor monitor,int retries)\r
893                 {\r
894                         Execute.OnUIThread(() =>\r
895                                                                    {                                       \r
896                                                                            var proxyAccount = IoC.Get<ProxyAccountViewModel>();\r
897                                                                                 proxyAccount.Settings = Settings;\r
898                                                                            if (true != _windowManager.ShowDialog(proxyAccount)) \r
899                                                                                    return;\r
900                                                                            StartMonitor(monitor, retries);\r
901                                                                            NotifyOfPropertyChange(() => Accounts);\r
902                                                                    });\r
903                 }\r
904 \r
905                 private bool AbandonRetry(PithosMonitor monitor, int retries)\r
906                 {\r
907                         if (retries > 3)\r
908                         {\r
909                                 var message = String.Format("Monitoring of account {0} has failed too many times. Will not retry",\r
910                                                                                         monitor.UserName);\r
911                                 _events.Publish(new Notification\r
912                                                                         {Title = "Account monitoring failed", Message = message, Level = TraceLevel.Error});\r
913                                 return true;\r
914                         }\r
915                         return false;\r
916                 }\r
917 \r
918 \r
919             private void TryLater(PithosMonitor monitor, Exception exc,int retries)\r
920                 {\r
921                         var message = String.Format("An exception occured. Can't start monitoring\nWill retry in 10 seconds");\r
922                         Task.Factory.StartNewDelayed(10000, () => StartMonitor(monitor,retries+1));\r
923                         _events.Publish(new Notification\r
924                                                                 {Title = "Error", Message = message, Level = TraceLevel.Error});\r
925                         Log.Error(message, exc);\r
926                 }\r
927 \r
928 \r
929                 public void NotifyChange(string status, TraceLevel level=TraceLevel.Info)\r
930                 {\r
931                         StatusMessage = status;\r
932                         \r
933                         _events.Publish(new Notification { Title = "Pithos+", Message = status, Level = level });\r
934                 }\r
935 \r
936                 public void NotifyChangedFile(string filePath)\r
937                 {\r
938             if (RecentFiles.Any(e => e.FullPath == filePath))\r
939                 return;\r
940             \r
941                         IProducerConsumerCollection<FileEntry> files=RecentFiles;\r
942                         FileEntry popped;\r
943                         while (files.Count > 5)\r
944                                 files.TryTake(out popped);\r
945             var entry = new FileEntry { FullPath = filePath };\r
946                         files.TryAdd(entry);\r
947                 }\r
948 \r
949                 public void NotifyAccount(AccountInfo account)\r
950                 {\r
951                         if (account== null)\r
952                                 return;\r
953                         //TODO: What happens to an existing account whose Token has changed?\r
954                         account.SiteUri= String.Format("{0}/ui/?token={1}&user={2}",\r
955                                 account.SiteUri, Uri.EscapeDataString(account.Token),\r
956                                 Uri.EscapeDataString(account.UserName));\r
957 \r
958                         if (!Accounts.Any(item => item.UserName == account.UserName && item.SiteUri == account.SiteUri))\r
959                                 Accounts.TryAdd(account);\r
960 \r
961                 }\r
962 \r
963                 public void NotifyConflicts(IEnumerable<FileSystemInfo> conflictFiles, string message)\r
964                 {\r
965                         if (conflictFiles == null)\r
966                                 return;\r
967                     //Convert to list to avoid multiple iterations\r
968             var files = conflictFiles.ToList();\r
969                         if (files.Count==0)\r
970                                 return;\r
971 \r
972                         UpdateStatus();\r
973                         //TODO: Create a more specific message. For now, just show a warning\r
974                         NotifyForFiles(files,message,TraceLevel.Warning);\r
975 \r
976                 }\r
977 \r
978                 public void NotifyForFiles(IEnumerable<FileSystemInfo> files, string message,TraceLevel level=TraceLevel.Info)\r
979                 {\r
980                         if (files == null)\r
981                                 return;\r
982                         if (!files.Any())\r
983                                 return;\r
984 \r
985                         StatusMessage = message;\r
986 \r
987                         _events.Publish(new Notification { Title = "Pithos+", Message = message, Level = level});\r
988                 }\r
989 \r
990                 public void Notify(Notification notification)\r
991                 {\r
992             TaskEx.Run(()=> _events.Publish(notification));\r
993                 }\r
994 \r
995 \r
996                 public void RemoveMonitor(string serverUrl,string accountName)\r
997                 {\r
998                         if (String.IsNullOrWhiteSpace(accountName))\r
999                                 return;\r
1000 \r
1001                         var accountInfo=_accounts.FirstOrDefault(account => account.UserName == accountName && account.StorageUri.ToString().StartsWith(serverUrl));\r
1002             if (accountInfo != null)\r
1003             {\r
1004                 _accounts.TryRemove(accountInfo);\r
1005                 _pollAgent.RemoveAccount(accountInfo);\r
1006             }\r
1007 \r
1008             var accountKey = new Uri(serverUrl).Combine(accountName);\r
1009                     PithosMonitor monitor;\r
1010                         if (Monitors.TryRemove(accountKey, out monitor))\r
1011                         {\r
1012                                 monitor.Stop();\r
1013                 //TODO: Also remove any pending actions for this account\r
1014                 //from the network queue                \r
1015                         }\r
1016                 }\r
1017 \r
1018                 public void RefreshOverlays()\r
1019                 {\r
1020                         foreach (var pair in Monitors)\r
1021                         {\r
1022                                 var monitor = pair.Value;\r
1023 \r
1024                                 var path = monitor.RootPath;\r
1025 \r
1026                                 if (String.IsNullOrWhiteSpace(path))\r
1027                                         continue;\r
1028 \r
1029                                 if (!Directory.Exists(path) && !File.Exists(path))\r
1030                                         continue;\r
1031 \r
1032                                 IntPtr pathPointer = Marshal.StringToCoTaskMemAuto(path);\r
1033 \r
1034                                 try\r
1035                                 {\r
1036                                         NativeMethods.SHChangeNotify(HChangeNotifyEventID.SHCNE_UPDATEITEM,\r
1037                                                                                                  HChangeNotifyFlags.SHCNF_PATHW | HChangeNotifyFlags.SHCNF_FLUSHNOWAIT,\r
1038                                                                                                  pathPointer, IntPtr.Zero);\r
1039                                 }\r
1040                                 finally\r
1041                                 {\r
1042                                         Marshal.FreeHGlobal(pathPointer);\r
1043                                 }\r
1044                         }\r
1045                 }\r
1046 \r
1047                 #region Event Handlers\r
1048                 \r
1049                 public void Handle(SelectiveSynchChanges message)\r
1050                 {\r
1051                     TaskEx.Run(() =>\r
1052                     {\r
1053                         PithosMonitor monitor;\r
1054                         if (Monitors.TryGetValue(message.Account.AccountKey, out monitor))\r
1055                         {\r
1056                     Selectives.SetIsSelectiveEnabled(message.Account.AccountKey, message.Enabled);\r
1057                             monitor.SetSelectivePaths(message.Uris, message.Added, message.Removed);\r
1058                         }\r
1059 \r
1060                         var account = Accounts.FirstOrDefault(acc => acc.AccountKey == message.Account.AccountKey);\r
1061                         if (account != null)\r
1062                         {\r
1063                             var added=monitor.UrisToFilePaths(message.Added);\r
1064                     _pollAgent.SynchNow(added);\r
1065                         }\r
1066                     });\r
1067 \r
1068                 }\r
1069 \r
1070 \r
1071                 private bool _pollStarted;\r
1072             private Sparkle _sparkle;\r
1073             private bool _manualUpgradeCheck;\r
1074 \r
1075             //SMELL: Doing so much work for notifications in the shell is wrong\r
1076                 //The notifications should be moved to their own view/viewmodel pair\r
1077                 //and different templates should be used for different message types\r
1078                 //This will also allow the addition of extra functionality, eg. actions\r
1079                 //\r
1080                 public void Handle(Notification notification)\r
1081                 {\r
1082                         UpdateStatus();\r
1083 \r
1084                         if (!Settings.ShowDesktopNotifications)\r
1085                                 return;\r
1086 \r
1087                         if (notification is PollNotification)\r
1088                         {\r
1089                                 _pollStarted = true;\r
1090                                 return;\r
1091                         }\r
1092                         if (notification is CloudNotification)\r
1093                         {\r
1094                                 if (!_pollStarted) \r
1095                                         return;\r
1096                                 _pollStarted= false;\r
1097                                 notification.Title = "Pithos+";\r
1098                                 notification.Message = "Start Synchronisation";\r
1099                         }\r
1100 \r
1101                     var deleteNotification = notification as CloudDeleteNotification;\r
1102             if (deleteNotification != null)\r
1103             {\r
1104                 StatusMessage = String.Format("Deleted {0}", deleteNotification.Data.Name);\r
1105                 return;\r
1106             }\r
1107 \r
1108                     var progress = notification as ProgressNotification;\r
1109                     \r
1110                     \r
1111             if (progress != null)\r
1112             {\r
1113                 double percentage = (progress.TotalBlocks == progress.Block) ? 1\r
1114                     :(progress.Block + progress.BlockPercentage / 100.0) / (double)progress.TotalBlocks;\r
1115                         StatusMessage = String.Format("{0} {1:p2} of {2} - {3}",                                                      \r
1116                                               progress.Action,\r
1117                                                       percentage,\r
1118                                                       progress.FileSize.ToByteSize(),\r
1119                                                       progress.FileName);\r
1120                         return;\r
1121                     }\r
1122 \r
1123                     var info = notification as StatusNotification;\r
1124             if (info != null)\r
1125             {\r
1126                 StatusMessage = info.Title;\r
1127                 return;\r
1128             }\r
1129                         if (String.IsNullOrWhiteSpace(notification.Message) && String.IsNullOrWhiteSpace(notification.Title))\r
1130                                 return;\r
1131 \r
1132             if (notification.Level <= TraceLevel.Warning)\r
1133                             ShowBalloonFor(notification);\r
1134                 }\r
1135 \r
1136             private void ShowBalloonFor(Notification notification)\r
1137             {\r
1138             Contract.Requires(notification!=null);\r
1139             \r
1140             if (!Settings.ShowDesktopNotifications) \r
1141                 return;\r
1142             \r
1143             BalloonIcon icon;\r
1144                 switch (notification.Level)\r
1145                 {\r
1146                 case TraceLevel.Verbose:\r
1147                         return;\r
1148                     case TraceLevel.Info:                   \r
1149                         icon = BalloonIcon.Info;\r
1150                         break;\r
1151                 case TraceLevel.Error:\r
1152                     icon = BalloonIcon.Error;\r
1153                     break;\r
1154                 case TraceLevel.Warning:\r
1155                         icon = BalloonIcon.Warning;\r
1156                         break;\r
1157                     default:\r
1158                         return;\r
1159                 }\r
1160 \r
1161                 var tv = (ShellView) GetView();\r
1162                 System.Action clickAction = null;\r
1163                 if (notification is ExpirationNotification)\r
1164                 {\r
1165                     clickAction = () => ShowPreferences("AccountTab");\r
1166                 }\r
1167                 var balloon = new PithosBalloon\r
1168                                   {\r
1169                                       Title = notification.Title,\r
1170                                       Message = notification.Message,\r
1171                                       Icon = icon,\r
1172                                       ClickAction = clickAction\r
1173                                   };\r
1174                 tv.TaskbarView.ShowCustomBalloon(balloon, PopupAnimation.Fade, 4000);\r
1175             }\r
1176 \r
1177             #endregion\r
1178 \r
1179                 public void Handle(ShowFilePropertiesEvent message)\r
1180                 {\r
1181                         if (message == null)\r
1182                                 throw new ArgumentNullException("message");\r
1183                         if (String.IsNullOrWhiteSpace(message.FileName) )\r
1184                                 throw new ArgumentException("message");\r
1185                         Contract.EndContractBlock();\r
1186 \r
1187                         var fileName = message.FileName;\r
1188                         //TODO: Display file properties for non-container folders\r
1189                         if (File.Exists(fileName))\r
1190                                 //Retrieve the full name with exact casing. Pithos names are case sensitive                             \r
1191                                 ShowFileProperties(FileInfoExtensions.GetProperFilePathCapitalization(fileName));\r
1192                         else if (Directory.Exists(fileName))\r
1193                                 //Retrieve the full name with exact casing. Pithos names are case sensitive\r
1194                         {\r
1195                                 var path = FileInfoExtensions.GetProperDirectoryCapitalization(fileName);\r
1196                                 if (IsContainer(path))\r
1197                                         ShowContainerProperties(path);\r
1198                                 else\r
1199                                         ShowFileProperties(path);\r
1200                         }\r
1201                 }\r
1202 \r
1203                 private bool IsContainer(string path)\r
1204                 {\r
1205                         var matchingFolders = from account in _accounts\r
1206                                                                   from rootFolder in Directory.GetDirectories(account.AccountPath)\r
1207                                                                   where rootFolder.Equals(path, StringComparison.InvariantCultureIgnoreCase)\r
1208                                                                   select rootFolder;\r
1209                         return matchingFolders.Any();\r
1210                 }\r
1211 \r
1212                 public FileStatus GetFileStatus(string localFileName)\r
1213                 {\r
1214                         if (String.IsNullOrWhiteSpace(localFileName))\r
1215                                 throw new ArgumentNullException("localFileName");\r
1216                         Contract.EndContractBlock();\r
1217                         \r
1218                         var statusKeeper = IoC.Get<IStatusKeeper>();\r
1219                         var status=statusKeeper.GetFileStatus(localFileName);\r
1220                         return status;\r
1221                 }\r
1222 \r
1223             public void RemoveAccountFromDatabase(AccountSettings account)\r
1224             {\r
1225             var statusKeeper = IoC.Get<IStatusKeeper>();\r
1226             statusKeeper.ClearFolderStatus(account.RootPath);           \r
1227             }\r
1228         }\r
1229 }\r