2 /* -----------------------------------------------------------------------
\r
3 * <copyright file="ShellViewModel.cs" company="GRNet">
\r
5 * Copyright 2011-2012 GRNET S.A. All rights reserved.
\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
11 * 1. Redistributions of source code must retain the above
\r
12 * copyright notice, this list of conditions and the following
\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
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
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
39 * -----------------------------------------------------------------------
\r
42 using System.Collections.Concurrent;
\r
43 using System.Diagnostics;
\r
44 using System.Diagnostics.Contracts;
\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
65 using Pithos.Core.Agents;
\r
66 using Pithos.Interfaces;
\r
68 using System.Collections.Generic;
\r
70 using Pithos.Network;
\r
71 using StatusService = Pithos.Client.WPF.Services.StatusService;
\r
73 namespace Pithos.Client.WPF {
\r
74 using System.ComponentModel.Composition;
\r
76 public class ToggleStatusCommand:ICommand
\r
78 private readonly ShellViewModel _model;
\r
79 public ToggleStatusCommand(ShellViewModel model)
\r
83 public void Execute(object parameter)
\r
85 _model.CurrentSyncStatus();
\r
88 public bool CanExecute(object parameter)
\r
93 public event EventHandler CanExecuteChanged;
\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
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
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
114 private readonly IEventAggregator _events;
\r
116 public PithosSettings Settings { get; private set; }
\r
119 private readonly ConcurrentDictionary<Uri, PithosMonitor> _monitors = new ConcurrentDictionary<Uri, PithosMonitor>();
\r
121 /// Dictionary of account monitors, keyed by account
\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
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
131 get { return _monitors; }
\r
136 /// The status service is used by Shell extensions to retrieve file status information
\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
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
145 private PollAgent _pollAgent;
\r
148 private NetworkAgent _networkAgent;
\r
151 public Selectives Selectives { get; set; }
\r
154 public ToggleStatusCommand ToggleMiniStatusCommand { get; set; }
\r
156 private MiniStatusViewModel _miniStatus;
\r
159 public MiniStatusViewModel MiniStatus
\r
161 get { return _miniStatus; }
\r
164 _miniStatus = value;
\r
165 _miniStatus.Shell = this;
\r
166 _miniStatus.Deactivated += (sender, arg) =>
\r
168 _statusVisible = false;
\r
169 NotifyOfPropertyChange(()=>MiniStatusCaption);
\r
175 /// The Shell depends on MEF to provide implementations for windowManager, events, the status checker service and the settings
\r
178 /// The PithosSettings class encapsulates the app's settings to abstract their storage mechanism (App settings, a database or registry)
\r
180 [ImportingConstructor]
\r
181 public ShellViewModel(IWindowManager windowManager, IEventAggregator events, PithosSettings settings/*,PollAgent pollAgent,NetworkAgent networkAgent*/)
\r
186 _windowManager = windowManager;
\r
187 //CHECK: Caliburn doesn't need explicit command construction
\r
188 //CurrentSyncStatusCommand = new PithosCommand(OpenPithosFolder);
\r
191 _events.Subscribe(this);
\r
194 _pollAgent = pollAgent;
\r
195 _networkAgent = networkAgent;
\r
197 Settings = settings;
\r
199 Proxy.SetFromSettings(settings);
\r
201 StatusMessage = Settings.Accounts.Count==0
\r
202 ? "No Accounts added\r\nPlease add an account"
\r
205 _accounts.CollectionChanged += (sender, e) =>
\r
207 NotifyOfPropertyChange(() => OpenFolderCaption);
\r
208 NotifyOfPropertyChange(() => HasAccounts);
\r
211 SetVersionMessage();
\r
213 ToggleMiniStatusCommand=new ToggleStatusCommand(this);
\r
215 catch (Exception exc)
\r
217 Log.Error("Error while starting the ShellViewModel",exc);
\r
223 private void SetVersionMessage()
\r
225 Assembly assembly = Assembly.GetExecutingAssembly();
\r
226 var fileVersion = FileVersionInfo.GetVersionInfo(assembly.Location);
\r
227 VersionMessage = String.Format("Pithos+ {0}", fileVersion.FileVersion);
\r
230 public void CurrentSyncStatus()
\r
232 if (Accounts.Count == 0)
\r
234 ShowPreferences("AccountTab");
\r
238 if (!_statusVisible)
\r
240 _windowManager.ShowWindow(MiniStatus);
\r
241 _statusVisible = true;
\r
245 if (MiniStatus.IsActive)
\r
246 MiniStatus.TryClose();
\r
247 _statusVisible = false;
\r
250 NotifyOfPropertyChange(() => MiniStatusCaption);
\r
254 protected override void OnActivate()
\r
258 InitializeSparkle();
\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
266 StartMonitoring();
\r
270 private void OnCheckFinished(object sender, bool updaterequired)
\r
273 Log.InfoFormat("Upgrade check finished. Need Upgrade: {0}", updaterequired);
\r
274 if (_manualUpgradeCheck)
\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
284 private void OnUpgradeDetected(object sender, UpdateDetectedEventArgs e)
\r
286 Log.InfoFormat("Update detected {0}",e.LatestVersion);
\r
289 public void CheckForUpgrade()
\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
297 _manualUpgradeCheck = true;
\r
298 InitializeSparkle();
\r
299 _sparkle.StartLoop(true,true,Settings.UpdateCheckInterval);
\r
302 private void InitializeSparkle()
\r
304 _sparkle = new Sparkle(Settings.UpdateUrl);
\r
305 _sparkle.updateDetected += OnUpgradeDetected;
\r
306 _sparkle.checkLoopFinished += OnCheckFinished;
\r
307 _sparkle.ShowDiagnosticWindow = Settings.UpdateDiagnostics;
\r
310 private async void StartMonitoring()
\r
314 if (Settings.IgnoreCertificateErrors)
\r
316 ServicePointManager.ServerCertificateValidationCallback = (sender, certificate, chain, errors) => true;
\r
319 var accounts = Settings.Accounts.Select(MonitorAccount);
\r
320 await TaskEx.WhenAll(accounts).ConfigureAwait(false);
\r
321 _statusService = StatusService.Start();
\r
324 catch (AggregateException exc)
\r
328 Log.Error("Error while starting monitoring", e);
\r
335 protected override void OnDeactivate(bool close)
\r
337 base.OnDeactivate(close);
\r
340 StatusService.Stop(_statusService);
\r
341 _statusService = null;
\r
345 public Task MonitorAccount(AccountSettings account)
\r
347 return Task.Factory.StartNew(() =>
\r
349 PithosMonitor monitor;
\r
350 var accountName = account.AccountName;
\r
352 MigrateFolders(account);
\r
354 Selectives.SetIsSelectiveEnabled(account.AccountKey, account.SelectiveSyncEnabled);
\r
356 if (Monitors.TryGetValue(account.AccountKey, out monitor))
\r
358 //If the account is active
\r
359 if (account.IsActive)
\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
371 //If the account is inactive
\r
372 //Stop and remove the monitor
\r
373 RemoveMonitor(account.ServerUrl,accountName);
\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
382 UserName = accountName,
\r
383 ApiKey = account.ApiKey,
\r
384 StatusNotification = this,
\r
385 RootPath = account.RootPath
\r
387 //PithosMonitor uses MEF so we need to resolve it
\r
388 IoC.BuildUp(monitor);
\r
390 monitor.AuthenticationUrl = account.ServerUrl;
\r
392 Monitors[account.AccountKey] = monitor;
\r
394 if (account.IsActive)
\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
400 StartMonitor(monitor);
\r
405 private void MigrateFolders(AccountSettings account)
\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
412 if (oldFolder.Exists && !newFolder.Exists)
\r
414 oldFolder.MoveTo(newOthersFolder);
\r
419 protected override void OnViewLoaded(object view)
\r
422 var window = (Window)view;
\r
423 TaskEx.Delay(1000).ContinueWith(t => Execute.OnUIThread(window.Hide));
\r
424 base.OnViewLoaded(view);
\r
428 #region Status Properties
\r
430 private string _statusMessage;
\r
431 public string StatusMessage
\r
433 get { return _statusMessage; }
\r
436 _statusMessage = value;
\r
437 NotifyOfPropertyChange(() => StatusMessage);
\r
438 NotifyOfPropertyChange(() => TooltipMessage);
\r
442 public string VersionMessage { get; set; }
\r
444 public string TooltipMessage
\r
448 return String.Format("{0}\r\n{1}",VersionMessage,StatusMessage);
\r
452 public string TooltipMiniStatus
\r
456 return String.Format("{0}\r\n{1}", "Status Window", "Enable / Disable the status window");
\r
460 /*public string ToggleStatusWindowMessage
\r
464 return String.Format("{0}" + Environment.NewLine + "{1} Toggle Mini Status");
\r
468 private readonly ObservableConcurrentCollection<AccountInfo> _accounts = new ObservableConcurrentCollection<AccountInfo>();
\r
469 public ObservableConcurrentCollection<AccountInfo> Accounts
\r
471 get { return _accounts; }
\r
474 public bool HasAccounts
\r
476 get { return _accounts.Count > 0; }
\r
480 public string OpenFolderCaption
\r
484 return (_accounts.Count == 0)
\r
485 ? "No Accounts Defined"
\r
486 : "Open Pithos Folder";
\r
490 private string _pauseSyncCaption="Pause Synching";
\r
491 public string PauseSyncCaption
\r
493 get { return _pauseSyncCaption; }
\r
496 _pauseSyncCaption = value;
\r
497 NotifyOfPropertyChange(() => PauseSyncCaption);
\r
501 private readonly ObservableConcurrentCollection<FileEntry> _recentFiles = new ObservableConcurrentCollection<FileEntry>();
\r
502 public ObservableConcurrentCollection<FileEntry> RecentFiles
\r
504 get { return _recentFiles; }
\r
508 private string _statusIcon="../Images/Pithos.ico";
\r
509 public string StatusIcon
\r
511 get { return _statusIcon; }
\r
514 //TODO: Ensure all status icons use the Pithos logo
\r
515 _statusIcon = value;
\r
516 NotifyOfPropertyChange(() => StatusIcon);
\r
524 public void CancelCurrentOperation()
\r
526 _pollAgent.CancelCurrentOperation();
\r
529 public void ShowPreferences()
\r
531 ShowPreferences(null);
\r
534 public void ShowPreferences(string currentTab)
\r
536 //Settings.Reload();
\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
547 public void AboutPithos()
\r
549 var about = IoC.Get<AboutViewModel>();
\r
550 about.LatestVersion=_sparkle.LatestVersion;
\r
551 _windowManager.ShowWindow(about);
\r
554 public void SendFeedback()
\r
556 var feedBack = IoC.Get<FeedbackViewModel>();
\r
557 _windowManager.ShowWindow(feedBack);
\r
560 //public PithosCommand OpenPithosFolderCommand { get; private set; }
\r
562 public void OpenPithosFolder()
\r
564 var account = Settings.Accounts.FirstOrDefault(acc => acc.IsActive);
\r
565 if (account == null)
\r
567 Process.Start(account.RootPath);
\r
570 public void OpenPithosFolder(AccountInfo account)
\r
572 Process.Start(account.AccountPath);
\r
577 public void GoToSite()
\r
579 var site = Properties.Settings.Default.ProductionServer;
\r
580 Process.Start(site);
\r
584 public void GoToSite(AccountInfo account)
\r
586 var uri = account.SiteUri.Replace("http://","https://");
\r
587 Process.Start(uri);
\r
590 private bool _statusVisible;
\r
592 public string MiniStatusCaption
\r
596 return _statusVisible ? "Hide Status Window" : "Show Status Window";
\r
600 public bool HasConflicts
\r
602 get { return true; }
\r
604 public void ShowConflicts()
\r
606 _windowManager.ShowWindow(IoC.Get<ConflictsViewModel>());
\r
610 /// Open an explorer window to the target path's directory
\r
611 /// and select the file
\r
613 /// <param name="entry"></param>
\r
614 public void GoToFile(FileEntry entry)
\r
616 var fullPath = entry.FullPath;
\r
617 if (!File.Exists(fullPath) && !Directory.Exists(fullPath))
\r
619 Process.Start("explorer.exe","/select, " + fullPath);
\r
622 public void OpenLogPath()
\r
624 var pithosDataPath = PithosSettings.PithosDataPath;
\r
626 Process.Start(pithosDataPath);
\r
629 public void ShowFileProperties()
\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
639 public async void ShowFileProperties(string filePath)
\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
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
652 if (accountMonitor == null)
\r
655 var infoTask=accountMonitor.GetObjectInfo(filePath);
\r
659 var fileProperties = new FilePropertiesViewModel(this, infoTask,filePath);
\r
660 _windowManager.ShowWindow(fileProperties);
\r
663 public void ShowContainerProperties()
\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
673 public void ShowContainerProperties(string filePath)
\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
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
689 var containerProperties = new ContainerPropertiesViewModel(this, info,filePath);
\r
690 _windowManager.ShowWindow(containerProperties);
\r
693 public void SynchNow()
\r
695 _pollAgent.SynchNow();
\r
698 public async Task<ObjectInfo> RefreshObjectInfo(ObjectInfo currentInfo)
\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
708 public ContainerInfo RefreshContainerInfo(ContainerInfo container)
\r
710 if (container == null)
\r
711 throw new ArgumentNullException("container");
\r
712 Contract.EndContractBlock();
\r
714 var monitor = Monitors[container.AccountKey];
\r
715 var newInfo = monitor.CloudClient.GetContainerInfo(container.Account, container.Name);
\r
719 private bool _isPaused;
\r
720 public bool IsPaused
\r
722 get { return _isPaused; }
\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
730 NotifyOfPropertyChange(() => IsPaused);
\r
734 public void ToggleSynching()
\r
736 IsPaused=!IsPaused;
\r
737 foreach (var monitor in Monitors.Values)
\r
739 monitor.Pause = IsPaused ;
\r
741 _pollAgent.Pause = IsPaused;
\r
742 _networkAgent.Pause = IsPaused;
\r
747 public void ExitPithos()
\r
752 foreach (var monitor in Monitors.Select(pair => pair.Value))
\r
757 var view = GetView() as Window;
\r
761 catch (Exception exc)
\r
763 Log.Info("Exception while exiting", exc);
\r
767 Application.Current.Shutdown();
\r
774 private readonly Dictionary<PithosStatus, StatusInfo> _iconNames = new List<StatusInfo>
\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
782 readonly IWindowManager _windowManager;
\r
784 //private int _syncCount=0;
\r
787 private PithosStatus _pithosStatus = PithosStatus.Disconnected;
\r
789 public void SetPithosStatus(PithosStatus status)
\r
791 if (_pithosStatus == PithosStatus.LocalSyncing && status == PithosStatus.PollComplete)
\r
793 if (_pithosStatus == PithosStatus.PollSyncing && status == PithosStatus.LocalComplete)
\r
795 if (status == PithosStatus.LocalComplete || status == PithosStatus.PollComplete)
\r
796 _pithosStatus = PithosStatus.InSynch;
\r
798 _pithosStatus = status;
\r
802 public void SetPithosStatus(PithosStatus status,string message)
\r
804 StatusMessage = message;
\r
805 SetPithosStatus(status);
\r
808 /* public Notifier GetNotifier(Notification startNotification, Notification endNotification)
\r
810 return new Notifier(this, startNotification, endNotification);
\r
813 public Notifier GetNotifier(string startMessage, string endMessage, bool isActive=true,params object[] args)
\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
823 /// Updates the visual status indicators of the application depending on status changes, e.g. icon, stat
\r
825 public void UpdateStatus()
\r
828 if (_iconNames.ContainsKey(_pithosStatus))
\r
830 var info = _iconNames[_pithosStatus];
\r
831 StatusIcon = String.Format(@"../Images/{0}.ico", info.IconName);
\r
834 if (_pithosStatus == PithosStatus.InSynch)
\r
835 StatusMessage = "All files up to date";
\r
840 private Task StartMonitor(PithosMonitor monitor,int retries=0)
\r
842 return Task.Factory.StartNew(() =>
\r
844 using (log4net.ThreadContext.Stacks["Monitor"].Push("Start"))
\r
848 Log.InfoFormat("Start Monitoring {0}", monitor.UserName);
\r
852 catch (WebException exc)
\r
854 if (AbandonRetry(monitor, retries))
\r
857 HttpStatusCode statusCode =HttpStatusCode.OK;
\r
858 var response = exc.Response as HttpWebResponse;
\r
860 statusCode = response.StatusCode;
\r
862 switch (statusCode)
\r
864 case HttpStatusCode.Unauthorized:
\r
865 var message = String.Format("API Key Expired for {0}. Starting Renewal",
\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
873 case HttpStatusCode.ProxyAuthenticationRequired:
\r
874 TryAuthenticateProxy(monitor,retries);
\r
877 TryLater(monitor, exc, retries);
\r
881 catch (Exception exc)
\r
883 if (AbandonRetry(monitor, retries))
\r
886 TryLater(monitor,exc,retries);
\r
892 private void TryAuthenticateProxy(PithosMonitor monitor,int retries)
\r
894 Execute.OnUIThread(() =>
\r
896 var proxyAccount = IoC.Get<ProxyAccountViewModel>();
\r
897 proxyAccount.Settings = Settings;
\r
898 if (true != _windowManager.ShowDialog(proxyAccount))
\r
900 StartMonitor(monitor, retries);
\r
901 NotifyOfPropertyChange(() => Accounts);
\r
905 private bool AbandonRetry(PithosMonitor monitor, int retries)
\r
909 var message = String.Format("Monitoring of account {0} has failed too many times. Will not retry",
\r
911 _events.Publish(new Notification
\r
912 {Title = "Account monitoring failed", Message = message, Level = TraceLevel.Error});
\r
919 private void TryLater(PithosMonitor monitor, Exception exc,int retries)
\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
929 public void NotifyChange(string status, TraceLevel level=TraceLevel.Info)
\r
931 StatusMessage = status;
\r
933 _events.Publish(new Notification { Title = "Pithos+", Message = status, Level = level });
\r
936 public void NotifyChangedFile(string filePath)
\r
938 if (RecentFiles.Any(e => e.FullPath == filePath))
\r
941 IProducerConsumerCollection<FileEntry> files=RecentFiles;
\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
949 public void NotifyAccount(AccountInfo account)
\r
951 if (account== null)
\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
958 if (!Accounts.Any(item => item.UserName == account.UserName && item.SiteUri == account.SiteUri))
\r
959 Accounts.TryAdd(account);
\r
963 public void NotifyConflicts(IEnumerable<FileSystemInfo> conflictFiles, string message)
\r
965 if (conflictFiles == null)
\r
967 //Convert to list to avoid multiple iterations
\r
968 var files = conflictFiles.ToList();
\r
969 if (files.Count==0)
\r
973 //TODO: Create a more specific message. For now, just show a warning
\r
974 NotifyForFiles(files,message,TraceLevel.Warning);
\r
978 public void NotifyForFiles(IEnumerable<FileSystemInfo> files, string message,TraceLevel level=TraceLevel.Info)
\r
985 StatusMessage = message;
\r
987 _events.Publish(new Notification { Title = "Pithos+", Message = message, Level = level});
\r
990 public void Notify(Notification notification)
\r
992 TaskEx.Run(()=> _events.Publish(notification));
\r
996 public void RemoveMonitor(string serverUrl,string accountName)
\r
998 if (String.IsNullOrWhiteSpace(accountName))
\r
1001 var accountInfo=_accounts.FirstOrDefault(account => account.UserName == accountName && account.StorageUri.ToString().StartsWith(serverUrl));
\r
1002 if (accountInfo != null)
\r
1004 _accounts.TryRemove(accountInfo);
\r
1005 _pollAgent.RemoveAccount(accountInfo);
\r
1008 var accountKey = new Uri(serverUrl).Combine(accountName);
\r
1009 PithosMonitor monitor;
\r
1010 if (Monitors.TryRemove(accountKey, out monitor))
\r
1013 //TODO: Also remove any pending actions for this account
\r
1014 //from the network queue
\r
1018 public void RefreshOverlays()
\r
1020 foreach (var pair in Monitors)
\r
1022 var monitor = pair.Value;
\r
1024 var path = monitor.RootPath;
\r
1026 if (String.IsNullOrWhiteSpace(path))
\r
1029 if (!Directory.Exists(path) && !File.Exists(path))
\r
1032 IntPtr pathPointer = Marshal.StringToCoTaskMemAuto(path);
\r
1036 NativeMethods.SHChangeNotify(HChangeNotifyEventID.SHCNE_UPDATEITEM,
\r
1037 HChangeNotifyFlags.SHCNF_PATHW | HChangeNotifyFlags.SHCNF_FLUSHNOWAIT,
\r
1038 pathPointer, IntPtr.Zero);
\r
1042 Marshal.FreeHGlobal(pathPointer);
\r
1047 #region Event Handlers
\r
1049 public void Handle(SelectiveSynchChanges message)
\r
1053 PithosMonitor monitor;
\r
1054 if (Monitors.TryGetValue(message.Account.AccountKey, out monitor))
\r
1056 Selectives.SetIsSelectiveEnabled(message.Account.AccountKey, message.Enabled);
\r
1057 monitor.SetSelectivePaths(message.Uris, message.Added, message.Removed);
\r
1060 var account = Accounts.FirstOrDefault(acc => acc.AccountKey == message.Account.AccountKey);
\r
1061 if (account != null)
\r
1063 var added=monitor.UrisToFilePaths(message.Added);
\r
1064 _pollAgent.SynchNow(added);
\r
1071 private bool _pollStarted;
\r
1072 private Sparkle _sparkle;
\r
1073 private bool _manualUpgradeCheck;
\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
1080 public void Handle(Notification notification)
\r
1084 if (!Settings.ShowDesktopNotifications)
\r
1087 if (notification is PollNotification)
\r
1089 _pollStarted = true;
\r
1092 if (notification is CloudNotification)
\r
1094 if (!_pollStarted)
\r
1096 _pollStarted= false;
\r
1097 notification.Title = "Pithos+";
\r
1098 notification.Message = "Start Synchronisation";
\r
1101 var deleteNotification = notification as CloudDeleteNotification;
\r
1102 if (deleteNotification != null)
\r
1104 StatusMessage = String.Format("Deleted {0}", deleteNotification.Data.Name);
\r
1108 var progress = notification as ProgressNotification;
\r
1111 if (progress != null)
\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
1118 progress.FileSize.ToByteSize(),
\r
1119 progress.FileName);
\r
1123 var info = notification as StatusNotification;
\r
1126 StatusMessage = info.Title;
\r
1129 if (String.IsNullOrWhiteSpace(notification.Message) && String.IsNullOrWhiteSpace(notification.Title))
\r
1132 if (notification.Level <= TraceLevel.Warning)
\r
1133 ShowBalloonFor(notification);
\r
1136 private void ShowBalloonFor(Notification notification)
\r
1138 Contract.Requires(notification!=null);
\r
1140 if (!Settings.ShowDesktopNotifications)
\r
1144 switch (notification.Level)
\r
1146 case TraceLevel.Verbose:
\r
1148 case TraceLevel.Info:
\r
1149 icon = BalloonIcon.Info;
\r
1151 case TraceLevel.Error:
\r
1152 icon = BalloonIcon.Error;
\r
1154 case TraceLevel.Warning:
\r
1155 icon = BalloonIcon.Warning;
\r
1161 var tv = (ShellView) GetView();
\r
1162 System.Action clickAction = null;
\r
1163 if (notification is ExpirationNotification)
\r
1165 clickAction = () => ShowPreferences("AccountTab");
\r
1167 var balloon = new PithosBalloon
\r
1169 Title = notification.Title,
\r
1170 Message = notification.Message,
\r
1172 ClickAction = clickAction
\r
1174 tv.TaskbarView.ShowCustomBalloon(balloon, PopupAnimation.Fade, 4000);
\r
1179 public void Handle(ShowFilePropertiesEvent message)
\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
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
1195 var path = FileInfoExtensions.GetProperDirectoryCapitalization(fileName);
\r
1196 if (IsContainer(path))
\r
1197 ShowContainerProperties(path);
\r
1199 ShowFileProperties(path);
\r
1203 private bool IsContainer(string path)
\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
1212 public FileStatus GetFileStatus(string localFileName)
\r
1214 if (String.IsNullOrWhiteSpace(localFileName))
\r
1215 throw new ArgumentNullException("localFileName");
\r
1216 Contract.EndContractBlock();
\r
1218 var statusKeeper = IoC.Get<IStatusKeeper>();
\r
1219 var status=statusKeeper.GetFileStatus(localFileName);
\r
1223 public void RemoveAccountFromDatabase(AccountSettings account)
\r
1225 var statusKeeper = IoC.Get<IStatusKeeper>();
\r
1226 statusKeeper.ClearFolderStatus(account.RootPath);
\r