diff --git a/src/AppVNext.Notifier.ConsoleUwp.Setup/AppVNext.Notifier.ConsoleUwp.Setup.wixproj b/src/AppVNext.Notifier.ConsoleUwp.Setup/AppVNext.Notifier.ConsoleUwp.Setup.wixproj new file mode 100644 index 0000000..0a66339 --- /dev/null +++ b/src/AppVNext.Notifier.ConsoleUwp.Setup/AppVNext.Notifier.ConsoleUwp.Setup.wixproj @@ -0,0 +1,50 @@ + + + + Debug + x86 + 3.10 + 6592e67d-a611-4643-b73f-3f1a7c1c5f50 + 2.0 + Notifier + Package + AppVNext.Notifier.ConsoleUwp.Setup + + + bin\$(Configuration)\ + obj\$(Configuration)\ + Debug + True + + + bin\$(Configuration)\ + obj\$(Configuration)\ + True + + + + + + + AppVNext.Notifier.ConsoleUwp + {9ebf043a-3ff8-46cb-a204-3e3bdf35ed49} + True + True + Binaries;Content;Satellites + INSTALLFOLDER + + + + + + + + + \ No newline at end of file diff --git a/src/AppVNext.Notifier.ConsoleUwp.Setup/Product.wxs b/src/AppVNext.Notifier.ConsoleUwp.Setup/Product.wxs new file mode 100644 index 0000000..50fb2e2 --- /dev/null +++ b/src/AppVNext.Notifier.ConsoleUwp.Setup/Product.wxs @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/AppVNext.Notifier.ConsoleUwp/App.config b/src/AppVNext.Notifier.ConsoleUwp/App.config new file mode 100644 index 0000000..ac79024 --- /dev/null +++ b/src/AppVNext.Notifier.ConsoleUwp/App.config @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/AppVNext.Notifier.ConsoleUwp/AppVNext.Notifier.ConsoleUwp.csproj b/src/AppVNext.Notifier.ConsoleUwp/AppVNext.Notifier.ConsoleUwp.csproj new file mode 100644 index 0000000..afa789b --- /dev/null +++ b/src/AppVNext.Notifier.ConsoleUwp/AppVNext.Notifier.ConsoleUwp.csproj @@ -0,0 +1,176 @@ + + + + + Debug + AnyCPU + {9EBF043A-3FF8-46CB-A204-3E3BDF35ED49} + Exe + AppVNext.Notifier.ConsoleUwp + notifier + v4.6.1 + 10.0.10240.0 + 512 + true + + + + + + x64 + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + Icon.ico + + + + ..\packages\Microsoft.Toolkit.Uwp.Notifications.4.0.0\lib\netstandard1.4\Microsoft.Toolkit.Uwp.Notifications.dll + + + ..\packages\Microsoft.Win32.Primitives.4.3.0\lib\net46\Microsoft.Win32.Primitives.dll + + + ..\packages\Microsoft.Win32.Registry.4.5.0\lib\net461\Microsoft.Win32.Registry.dll + + + ..\packages\Newtonsoft.Json.11.0.2\lib\net45\Newtonsoft.Json.dll + + + ..\packages\QueryString.NET.1.0.0\lib\dotnet\QueryString.NETCore.dll + + + + ..\packages\System.AppContext.4.3.0\lib\net46\System.AppContext.dll + + + + ..\packages\System.Console.4.3.1\lib\net46\System.Console.dll + + + + ..\packages\System.Diagnostics.DiagnosticSource.4.5.0\lib\net46\System.Diagnostics.DiagnosticSource.dll + + + ..\packages\System.Globalization.Calendars.4.3.0\lib\net46\System.Globalization.Calendars.dll + + + ..\packages\System.IO.Compression.4.3.0\lib\net46\System.IO.Compression.dll + + + + ..\packages\System.IO.Compression.ZipFile.4.3.0\lib\net46\System.IO.Compression.ZipFile.dll + + + ..\packages\System.IO.FileSystem.4.3.0\lib\net46\System.IO.FileSystem.dll + + + ..\packages\System.IO.FileSystem.Primitives.4.3.0\lib\net46\System.IO.FileSystem.Primitives.dll + + + ..\packages\System.Net.Http.4.3.3\lib\net46\System.Net.Http.dll + + + ..\packages\System.Net.Sockets.4.3.0\lib\net46\System.Net.Sockets.dll + + + + ..\packages\System.Runtime.InteropServices.RuntimeInformation.4.3.0\lib\net45\System.Runtime.InteropServices.RuntimeInformation.dll + + + ..\packages\System.Security.AccessControl.4.5.0\lib\net461\System.Security.AccessControl.dll + + + ..\packages\System.Security.Cryptography.Algorithms.4.3.1\lib\net461\System.Security.Cryptography.Algorithms.dll + + + ..\packages\System.Security.Cryptography.Encoding.4.3.0\lib\net46\System.Security.Cryptography.Encoding.dll + + + ..\packages\System.Security.Cryptography.Primitives.4.3.0\lib\net46\System.Security.Cryptography.Primitives.dll + + + ..\packages\System.Security.Cryptography.X509Certificates.4.3.2\lib\net461\System.Security.Cryptography.X509Certificates.dll + + + ..\packages\System.Security.Principal.Windows.4.5.0\lib\net461\System.Security.Principal.Windows.dll + + + + + + + + 4.0 + + + ..\packages\System.Xml.ReaderWriter.4.3.1\lib\net46\System.Xml.ReaderWriter.dll + + + + + + + + + + + + + + + + + + + + + + Designer + + + Designer + + + + + PreserveNewest + + + PreserveNewest + + + + + {71bc5b4c-e5e3-4c46-933d-a772ca3c0499} + AppVNext.Notifier.Common + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + \ No newline at end of file diff --git a/src/AppVNext.Notifier.ConsoleUwp/DesktopNotificationManagerCompat.cs b/src/AppVNext.Notifier.ConsoleUwp/DesktopNotificationManagerCompat.cs new file mode 100644 index 0000000..656d776 --- /dev/null +++ b/src/AppVNext.Notifier.ConsoleUwp/DesktopNotificationManagerCompat.cs @@ -0,0 +1,398 @@ +// ****************************************************************** +// Copyright (c) Microsoft. All rights reserved. +// This code is licensed under the MIT License (MIT). +// THE CODE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH +// THE CODE OR THE USE OR OTHER DEALINGS IN THE CODE. +// ****************************************************************** + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using Windows.UI.Notifications; + +namespace DesktopNotifications +{ + public class DesktopNotificationManagerCompat + { + public const string TOAST_ACTIVATED_LAUNCH_ARG = "-ToastActivated"; + + private static bool _registeredAumidAndComServer; + private static string _aumid; + private static bool _registeredActivator; + + /// + /// If not running under the Desktop Bridge, you must call this method to register your AUMID with the Compat library and to + /// register your COM CLSID and EXE in LocalServer32 registry. Feel free to call this regardless, and we will no-op if running + /// under Desktop Bridge. Call this upon application startup, before calling any other APIs. + /// + /// An AUMID that uniquely identifies your application. + public static void RegisterAumidAndComServer(string aumid) + where T : NotificationActivator + { + if (string.IsNullOrWhiteSpace(aumid)) + { + throw new ArgumentException("You must provide an AUMID.", nameof(aumid)); + } + + // If running as Desktop Bridge + if (DesktopBridgeHelpers.IsRunningAsUwp()) + { + // Clear the AUMID since Desktop Bridge doesn't use it, and then we're done. + // Desktop Bridge apps are registered with platform through their manifest. + // Their LocalServer32 key is also registered through their manifest. + _aumid = null; + _registeredAumidAndComServer = true; + return; + } + + _aumid = aumid; + + String exePath = Process.GetCurrentProcess().MainModule.FileName; + RegisterComServer(exePath); + + _registeredAumidAndComServer = true; + } + + private static void RegisterComServer(String exePath) + where T : NotificationActivator + { + // We register the EXE to start up when the notification is activated + string regString = String.Format("SOFTWARE\\Classes\\CLSID\\{{{0}}}\\LocalServer32", typeof(T).GUID); + var key = Microsoft.Win32.Registry.CurrentUser.CreateSubKey(regString); + + // Include a flag so we know this was a toast activation and should wait for COM to process + // We also wrap EXE path in quotes for extra security + key.SetValue(null, '"' + exePath + '"' + " " + TOAST_ACTIVATED_LAUNCH_ARG); + } + + /// + /// Registers the activator type as a COM server client so that Windows can launch your activator. + /// + /// Your implementation of NotificationActivator. Must have GUID and ComVisible attributes on class. + public static void RegisterActivator() + where T : NotificationActivator + { + // Register type + var regService = new RegistrationServices(); + + regService.RegisterTypeForComClients( + typeof(T), + RegistrationClassContext.LocalServer, + RegistrationConnectionType.MultipleUse); + + _registeredActivator = true; + } + + /// + /// Creates a toast notifier. You must have called first (and also if you're a classic Win32 app), or this will throw an exception. + /// + /// + public static ToastNotifier CreateToastNotifier() + { + EnsureRegistered(); + + if (_aumid != null) + { + // Non-Desktop Bridge + return ToastNotificationManager.CreateToastNotifier(_aumid); + } + else + { + // Desktop Bridge + return ToastNotificationManager.CreateToastNotifier(); + } + } + + /// + /// Gets the object. You must have called first (and also if you're a classic Win32 app), or this will throw an exception. + /// + public static DesktopNotificationHistoryCompat History + { + get + { + EnsureRegistered(); + + return new DesktopNotificationHistoryCompat(_aumid); + } + } + + private static void EnsureRegistered() + { + // If not registered AUMID yet + if (!_registeredAumidAndComServer) + { + // Check if Desktop Bridge + if (DesktopBridgeHelpers.IsRunningAsUwp()) + { + // Implicitly registered, all good! + _registeredAumidAndComServer = true; + } + + else + { + // Otherwise, incorrect usage + throw new Exception("You must call RegisterAumidAndComServer first."); + } + } + + // If not registered activator yet + if (!_registeredActivator) + { + // Incorrect usage + throw new Exception("You must call RegisterActivator first."); + } + } + + /// + /// Gets a boolean representing whether http images can be used within toasts. This is true if running under Desktop Bridge. + /// + public static bool CanUseHttpImages { get { return DesktopBridgeHelpers.IsRunningAsUwp(); } } + + /// + /// Code from https://github.com/qmatteoq/DesktopBridgeHelpers/edit/master/DesktopBridge.Helpers/Helpers.cs + /// + private class DesktopBridgeHelpers + { + const long APPMODEL_ERROR_NO_PACKAGE = 15700L; + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + static extern int GetCurrentPackageFullName(ref int packageFullNameLength, StringBuilder packageFullName); + + private static bool? _isRunningAsUwp; + public static bool IsRunningAsUwp() + { + if (_isRunningAsUwp == null) + { + if (IsWindows7OrLower) + { + _isRunningAsUwp = false; + } + else + { + int length = 0; + StringBuilder sb = new StringBuilder(0); + int result = GetCurrentPackageFullName(ref length, sb); + + sb = new StringBuilder(length); + result = GetCurrentPackageFullName(ref length, sb); + + _isRunningAsUwp = result != APPMODEL_ERROR_NO_PACKAGE; + } + } + + return _isRunningAsUwp.Value; + } + + private static bool IsWindows7OrLower + { + get + { + int versionMajor = Environment.OSVersion.Version.Major; + int versionMinor = Environment.OSVersion.Version.Minor; + double version = versionMajor + (double)versionMinor / 10; + return version <= 6.1; + } + } + } + } + + /// + /// Manages the toast notifications for an app including the ability the clear all toast history and removing individual toasts. + /// + public sealed class DesktopNotificationHistoryCompat + { + private string _aumid; + private ToastNotificationHistory _history; + + /// + /// Do not call this. Instead, call to obtain an instance. + /// + /// + internal DesktopNotificationHistoryCompat(string aumid) + { + _aumid = aumid; + _history = ToastNotificationManager.History; + } + + /// + /// Removes all notifications sent by this app from action center. + /// + public void Clear() + { + if (_aumid != null) + { + _history.Clear(_aumid); + } + else + { + _history.Clear(); + } + } + + /// + /// Gets all notifications sent by this app that are currently still in Action Center. + /// + /// A collection of toasts. + public IReadOnlyList GetHistory() + { + return _aumid != null ? _history.GetHistory(_aumid) : _history.GetHistory(); + } + + /// + /// Removes an individual toast, with the specified tag label, from action center. + /// + /// The tag label of the toast notification to be removed. + public void Remove(string tag) + { + if (_aumid != null) + { + _history.Remove(tag, string.Empty, _aumid); + } + else + { + _history.Remove(tag); + } + } + + /// + /// Removes a toast notification from the action using the notification's tag and group labels. + /// + /// The tag label of the toast notification to be removed. + /// The group label of the toast notification to be removed. + public void Remove(string tag, string group) + { + if (_aumid != null) + { + _history.Remove(tag, group, _aumid); + } + else + { + _history.Remove(tag, group); + } + } + + /// + /// Removes a group of toast notifications, identified by the specified group label, from action center. + /// + /// The group label of the toast notifications to be removed. + public void RemoveGroup(string group) + { + if (_aumid != null) + { + _history.RemoveGroup(group, _aumid); + } + else + { + _history.RemoveGroup(group); + } + } + } + + /// + /// Apps must implement this activator to handle notification activation. + /// + public abstract class NotificationActivator : NotificationActivator.INotificationActivationCallback + { + public void Activate(string appUserModelId, string invokedArgs, NOTIFICATION_USER_INPUT_DATA[] data, uint dataCount) + { + OnActivated(invokedArgs, new NotificationUserInput(data), appUserModelId); + } + + /// + /// This method will be called when the user clicks on a foreground or background activation on a toast. Parent app must implement this method. + /// + /// The arguments from the original notification. This is either the launch argument if the user clicked the body of your toast, or the arguments from a button on your toast. + /// Text and selection values that the user entered in your toast. + /// Your AUMID. + public abstract void OnActivated(string arguments, NotificationUserInput userInput, string appUserModelId); + + // These are the new APIs for Windows 10 + #region NewAPIs + [StructLayout(LayoutKind.Sequential), Serializable] + public struct NOTIFICATION_USER_INPUT_DATA + { + [MarshalAs(UnmanagedType.LPWStr)] + public string Key; + + [MarshalAs(UnmanagedType.LPWStr)] + public string Value; + } + + [ComImport, + Guid("53E31837-6600-4A81-9395-75CFFE746F94"), ComVisible(true), + InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface INotificationActivationCallback + { + void Activate( + [In, MarshalAs(UnmanagedType.LPWStr)] + string appUserModelId, + [In, MarshalAs(UnmanagedType.LPWStr)] + string invokedArgs, + [In, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 3)] + NOTIFICATION_USER_INPUT_DATA[] data, + [In, MarshalAs(UnmanagedType.U4)] + uint dataCount); + } + #endregion + } + + /// + /// Text and selection values that the user entered on your notification. The Key is the ID of the input, and the Value is what the user entered. + /// + public class NotificationUserInput : IReadOnlyDictionary + { + public NotificationActivator.NOTIFICATION_USER_INPUT_DATA[] Data { get; } + + internal NotificationUserInput(NotificationActivator.NOTIFICATION_USER_INPUT_DATA[] data) + { + Data = data; + } + + public string this[string key] => Data.First(i => i.Key == key).Value; + + public IEnumerable Keys => Data.Select(i => i.Key); + + public IEnumerable Values => Data.Select(i => i.Value); + + public int Count => Data.Length; + + public bool ContainsKey(string key) + { + return Data.Any(i => i.Key == key); + } + + public IEnumerator> GetEnumerator() + { + return Data?.Select(i => new KeyValuePair(i.Key, i.Value))?.GetEnumerator(); + } + + public bool TryGetValue(string key, out string value) + { + foreach (var item in Data) + { + if (item.Key == key) + { + value = item.Value; + return true; + } + } + + value = null; + return false; + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} diff --git a/src/AppVNext.Notifier.ConsoleUwp/Icon.ico b/src/AppVNext.Notifier.ConsoleUwp/Icon.ico new file mode 100644 index 0000000..daa2985 Binary files /dev/null and b/src/AppVNext.Notifier.ConsoleUwp/Icon.ico differ diff --git a/src/AppVNext.Notifier.ConsoleUwp/NotificationEvents.cs b/src/AppVNext.Notifier.ConsoleUwp/NotificationEvents.cs new file mode 100644 index 0000000..35a620f --- /dev/null +++ b/src/AppVNext.Notifier.ConsoleUwp/NotificationEvents.cs @@ -0,0 +1,80 @@ +using System; +using Windows.UI.Notifications; +using static System.Environment; +using static System.Console; +using System.Collections.Generic; +using System.Reflection; +using AppVNext.Notifier.Common; +using Microsoft.QueryStringDotNET; + +namespace AppVNext.Notifier +{ + /// + /// Used when the Notification has been activated, dismissed or failed. + /// + class NotificationEvents + { + /// + /// Triggered when the notification has been activated or clicked on. + /// + /// ToastNotification sender object. + /// Used to get the properties when the notifications has been activated or clicked on. + internal void Activated(ToastNotification sender, object e) + { + var type = e.GetType(); + var properties = new List(type.GetProperties()); + + var results = string.Empty; + + foreach (var property in properties) + { + if (!string.IsNullOrEmpty(results)) + { + results += $"{results}{Globals.NewLine}"; + } + if (property.GetValue(e, null) is string value && !string.IsNullOrWhiteSpace(value)) + { + results += $"{property.Name}: {value}"; + } + } + + WriteLine($"The user clicked on the toast. {results}"); + Exit(0); + } + + /// + /// Triggered when the notification has been dismissed. + /// + /// ToastNotification sender object. + /// Toast dismissed event arguments. + internal void Dismissed(ToastNotification sender, ToastDismissedEventArgs e) + { + switch (e.Reason) + { + case ToastDismissalReason.ApplicationHidden: + WriteLine("The notification has been closed."); + Exit((int)DismissalActions.Hidden); + break; + case ToastDismissalReason.UserCanceled: + WriteLine("The user dismissed this toast."); + Exit((int)DismissalActions.Dismissed); + break; + case ToastDismissalReason.TimedOut: + WriteLine("The toast has timed out."); + Exit((int)DismissalActions.Timeout); + break; + } + } + + /// + /// Triggered when the notification failed. + /// + /// ToastNotification sender object. + /// Toast failed event arguments. + internal void Failed(ToastNotification sender, ToastFailedEventArgs e) + { + WriteLine($"An error has occurred. {e.ErrorCode}"); + Exit(-1); + } + } +} diff --git a/src/AppVNext.Notifier.ConsoleUwp/Notifier.cs b/src/AppVNext.Notifier.ConsoleUwp/Notifier.cs new file mode 100644 index 0000000..59ceda3 --- /dev/null +++ b/src/AppVNext.Notifier.ConsoleUwp/Notifier.cs @@ -0,0 +1,221 @@ +using AppVNext.Notifier.Common; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Windows.UI.Notifications; +using Microsoft.Toolkit.Uwp.Notifications; +using Microsoft.QueryStringDotNET; +using Windows.Data.Xml.Dom; +using DesktopNotifications; +using System.IO; + +namespace AppVNext.Notifier +{ + /// + /// Notifier class. + /// + static class Notifier + { + /// + /// Show toast notification. + /// + /// Notification arguments object. + /// Toast notification object. + public static async Task ShowToast(NotificationArguments arguments) + { + //Set the toast visual + var visual = new ToastVisual() + { + BindingGeneric = new ToastBindingGeneric() + { + Children = + { + new AdaptiveText() + { + Text = arguments.Title + }, + new AdaptiveText() + { + Text = arguments.Message + } + } + } + }; + + //Set the logo override + var imagePath = Globals.GetImageOrDefault(arguments.PicturePath); + var isInternetImage = IsInternetImage(imagePath); + var imageSource = isInternetImage ? await DownloadImageToDisk(imagePath) : imagePath; + + visual.BindingGeneric.AppLogoOverride = new ToastGenericAppLogo() + { + Source = imageSource, + HintCrop = ToastGenericAppLogoCrop.Circle + }; + + //Set a background image + if (!string.IsNullOrWhiteSpace(arguments.Image)) + { + isInternetImage = IsInternetImage(arguments.Image); + imageSource = isInternetImage ? await DownloadImageToDisk(arguments.Image) : arguments.Image; + + visual.BindingGeneric.Children.Add(new AdaptiveImage() + { + Source = imageSource + }); + } + + // Construct the actions for the toast (inputs and buttons) + var actions = new ToastActionsCustom(); + + // Add any inputs + if (arguments.Inputs != null) + { + foreach (var input in arguments.Inputs) + { + var textBox = new ToastTextBox(input.Id) + { + PlaceholderContent = input.PlaceHolderText + }; + + if (!string.IsNullOrWhiteSpace(input.Title)) + { + textBox.Title = input.Title; + } + actions.Inputs.Add(textBox); + } + } + + // Add any buttons + if (arguments.Buttons != null) + { + foreach (var button in arguments.Buttons) + { + actions.Buttons.Add(new ToastButton(button.Text, button.Arguments)); + + //Background activation is not needed the COM activator decides whether + //to process in background or launch foreground window + //actions.Buttons.Add(new ToastButton(button.Text, button.Arguments) + //{ + // ActivationType = ToastActivationType.Background + //}); + } + } + + //Set the audio + ToastAudio audio = null; + if (!string.IsNullOrWhiteSpace(arguments.WindowsSound) || !string.IsNullOrWhiteSpace(arguments.SoundPath)) + { + string sound; + if (string.IsNullOrWhiteSpace(arguments.WindowsSound)) + { + sound = "file:///" + arguments.SoundPath; + } + else + { + sound = $"ms-winsoundevent:{arguments.WindowsSound}"; + } + + audio = new ToastAudio() + { + Src = new Uri(sound), + Loop = bool.Parse(arguments.Loop), + Silent = arguments.Silent + }; + } + + // Construct the toast content + var toastContent = new ToastContent() + { + Visual = visual, + Actions = actions, + Audio = audio + }; + + // Create notification + var xmlDocument = new XmlDocument(); + xmlDocument.LoadXml(toastContent.GetContent()); + + var toast = new ToastNotification(xmlDocument); + + // Set the expiration time + if (!string.IsNullOrWhiteSpace(arguments.Duration)) + { + switch (arguments.Duration) + { + case "short": + toast.ExpirationTime = DateTime.Now.AddSeconds(5); + break; + case "long": + toast.ExpirationTime = DateTime.Now.AddSeconds(25); + break; + } + } + + //Add event handlers + var events = new NotificationEvents(); + toast.Activated += events.Activated; + toast.Dismissed += events.Dismissed; + toast.Failed += events.Failed; + + //Show notification + DesktopNotificationManagerCompat.CreateToastNotifier().Show(toast); + + return toast; + } + + private static bool IsInternetImage(string image) + { + return image.ToLower().StartsWith("http") || image.ToLower().StartsWith("https"); + } + + private static bool _hasPerformedCleanup; + private static async Task DownloadImageToDisk(string httpImage) + { + try + { + if (DesktopNotificationManagerCompat.CanUseHttpImages) + { + return httpImage; + } + + var directory = Directory.CreateDirectory(Path.GetTempPath() + "AppVNextNotifierImages"); + + if (!_hasPerformedCleanup) + { + _hasPerformedCleanup = true; + + foreach (var d in directory.EnumerateDirectories()) + { + if (d.CreationTimeUtc.Date < DateTime.UtcNow.Date.AddDays(-3)) + { + d.Delete(true); + } + } + } + + var dayDirectory = directory.CreateSubdirectory(DateTime.UtcNow.Day.ToString()); + string imagePath = dayDirectory.FullName + "\\" + (uint)httpImage.GetHashCode(); + + if (File.Exists(imagePath)) + { + return imagePath; + } + + System.Net.Http.HttpClient c = new System.Net.Http.HttpClient(); + using (var stream = await c.GetStreamAsync(httpImage)) + { + using (var fileStream = File.OpenWrite(imagePath)) + { + stream.CopyTo(fileStream); + } + } + + return imagePath; + } + catch { return ""; } + } + } +} \ No newline at end of file diff --git a/src/AppVNext.Notifier.ConsoleUwp/NotifierActivator.cs b/src/AppVNext.Notifier.ConsoleUwp/NotifierActivator.cs new file mode 100644 index 0000000..0ee5709 --- /dev/null +++ b/src/AppVNext.Notifier.ConsoleUwp/NotifierActivator.cs @@ -0,0 +1,59 @@ +// ****************************************************************** +// Copyright (c) Microsoft. All rights reserved. +// This code is licensed under the MIT License (MIT). +// THE CODE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH +// THE CODE OR THE USE OR OTHER DEALINGS IN THE CODE. +// ****************************************************************** + +using DesktopNotifications; +using System; +using System.Runtime.InteropServices; +using static System.Environment; +using static System.Console; + +namespace AppVNext.Notifier +{ + // This GUID should be unique and match the System.AppUserModel.ToastActivatorCLSID in the installer. + [ClassInterface(ClassInterfaceType.None)] + [ComSourceInterfaces(typeof(INotificationActivationCallback))] + [Guid("69684589-9DC2-46C6-B023-F29BF6B4FA5F"), ComVisible(true)] + public class NotifierActivator : NotificationActivator + { + public override void OnActivated(string arguments, NotificationUserInput userInput, string appUserModelId) + { + if (arguments?.Length == 0) + { + WriteLine($"The user clicked on the toast."); + } + else + { + WriteLine($"The user clicked on the toast. {arguments}"); + } + + var inputs = string.Empty; + if (userInput?.Data != null) + { + foreach (var input in userInput) + { + if (!string.IsNullOrEmpty(inputs)) + { + inputs += "&"; + } + inputs += $"{input.Key}={input.Value}"; + } + } + + if (inputs != string.Empty) + { + WriteLine($"The user entered the following input values: {inputs}"); + } + + Exit(0); + } + } +} \ No newline at end of file diff --git a/src/AppVNext.Notifier.ConsoleUwp/Program.cs b/src/AppVNext.Notifier.ConsoleUwp/Program.cs new file mode 100644 index 0000000..d9b22a8 --- /dev/null +++ b/src/AppVNext.Notifier.ConsoleUwp/Program.cs @@ -0,0 +1,97 @@ +using AppVNext.Notifier.Common; +using DesktopNotifications; +using Microsoft.QueryStringDotNET; +using Microsoft.Toolkit.Uwp.Notifications; +using System; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using System.Windows; +using Windows.Data.Xml.Dom; +using Windows.UI.Notifications; +using static System.Console; + +namespace AppVNext.Notifier +{ + /// + /// Notifier main program. + /// + class Program + { + /// + /// Main method. + /// + /// Arguments for the notification. + static void Main(string[] args) + { + // Register AUMID, COM server, and activator + DesktopNotificationManagerCompat.RegisterAumidAndComServer("AppVNextNotifier"); + DesktopNotificationManagerCompat.RegisterActivator(); + + // If launched from a toast notification + if (args.Contains(DesktopNotificationManagerCompat.TOAST_ACTIVATED_LAUNCH_ARG)) + { + //TODO: Handle if launched from a toast notification + } + else + { + //Initialize application type. TODO: Replace this with dependency injection. + Globals.ApplicationType = ApplicationTypes.UwpConsole; + + var arguments = ArgumentManager.ProcessArguments(args); + + if (arguments == null) + { + WriteLine($"{Globals.HelpForNullMessage}{Globals.HelpForErrors}"); + ArgumentManager.DisplayHelp(); + } + else + { + if (arguments.NotificationsCheck) + { + WriteLine(RegistryHelper.AreNotificationsEnabled(arguments.NotificationCheckAppId)); + } + + if (arguments.PushNotificationCheck) + { + WriteLine(RegistryHelper.ArePushNotificationsEnabled()); + } + + if (arguments.VersionInformation) + { + WriteLine(Globals.GetVersionInformation()); + } + + if (arguments.ClearNotifications) + { + DesktopNotificationManagerCompat.History.Clear(); + } + + if (string.IsNullOrEmpty(arguments.Errors) && !string.IsNullOrEmpty(arguments.Message)) + { + SendNotification(arguments); + while (arguments.Wait) { System.Threading.Thread.Sleep(500); } + } + else + { + WriteLine($"{(arguments.Errors ?? string.Empty)}"); + } + } + } + } + + /// + /// Send notification. + /// + /// Notification arguments object. + private static void SendNotification(NotificationArguments arguments) + { + //if (arguments.ApplicationId == Globals.DefaultApplicationId) + //{ + // ShortcutHelper.CreateShortcutIfNeeded(arguments.ApplicationId, arguments.ApplicationName); + //} + var toast = Notifier.ShowToast(arguments); + } + } +} diff --git a/src/AppVNext.Notifier.ConsoleUwp/Properties/AssemblyInfo.cs b/src/AppVNext.Notifier.ConsoleUwp/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..98042fc --- /dev/null +++ b/src/AppVNext.Notifier.ConsoleUwp/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("App vNext UWP Notifier")] +[assembly: AssemblyDescription("Command line tool to send Windows Toast Notifications.")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("App vNext")] +[assembly: AssemblyProduct("UWP Notifier")] +[assembly: AssemblyCopyright("Copyright © 2018")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("9ebf043a-3ff8-46cb-a204-3e3bdf35ed49")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/AppVNext.Notifier.ConsoleUwp/packages.config b/src/AppVNext.Notifier.ConsoleUwp/packages.config new file mode 100644 index 0000000..b0dcc2a --- /dev/null +++ b/src/AppVNext.Notifier.ConsoleUwp/packages.config @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/AppVNext.Notifier.ConsoleUwp/sound.mp3 b/src/AppVNext.Notifier.ConsoleUwp/sound.mp3 new file mode 100644 index 0000000..800c655 Binary files /dev/null and b/src/AppVNext.Notifier.ConsoleUwp/sound.mp3 differ diff --git a/src/AppVNext.Notifier.sln b/src/AppVNext.Notifier.sln index d10490e..f69c08a 100644 --- a/src/AppVNext.Notifier.sln +++ b/src/AppVNext.Notifier.sln @@ -9,12 +9,16 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AppVNext.Notifier.Common", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AppVNext.Notifier.Uwp", "AppVNext.Notifier.Uwp\AppVNext.Notifier.Uwp.csproj", "{0D1A6417-F76C-4562-A12B-B3E0AB90F294}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AppVNext.Notifier.Uwp.Tasks", "AppVNext.Notifier.Uwp.Tasks\AppVNext.Notifier.Uwp.Tasks.csproj", "{796A3452-8359-494E-8AB8-946CE61FED18}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AppVNext.Notifier.ConsoleUwp", "AppVNext.Notifier.ConsoleUwp\AppVNext.Notifier.ConsoleUwp.csproj", "{9EBF043A-3FF8-46CB-A204-3E3BDF35ED49}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleApp1", "ConsoleApp1\ConsoleApp1.csproj", "{8AD9E6C1-54B5-4548-AC66-19510A6C6FED}" +Project("{930C7802-8A8C-48F9-8165-68863BCCD9DD}") = "AppVNext.Notifier.ConsoleUwp.Setup", "AppVNext.Notifier.ConsoleUwp.Setup\AppVNext.Notifier.ConsoleUwp.Setup.wixproj", "{6592E67D-A611-4643-B73F-3F1A7C1C5F50}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution + All|Any CPU = All|Any CPU + All|ARM = All|ARM + All|x64 = All|x64 + All|x86 = All|x86 Debug|Any CPU = Debug|Any CPU Debug|ARM = Debug|ARM Debug|x64 = Debug|x64 @@ -25,6 +29,14 @@ Global Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A4C223A8-6D90-4015-B417-64D01F9F1C9B}.All|Any CPU.ActiveCfg = Release|Any CPU + {A4C223A8-6D90-4015-B417-64D01F9F1C9B}.All|Any CPU.Build.0 = Release|Any CPU + {A4C223A8-6D90-4015-B417-64D01F9F1C9B}.All|ARM.ActiveCfg = Release|Any CPU + {A4C223A8-6D90-4015-B417-64D01F9F1C9B}.All|ARM.Build.0 = Release|Any CPU + {A4C223A8-6D90-4015-B417-64D01F9F1C9B}.All|x64.ActiveCfg = Release|Any CPU + {A4C223A8-6D90-4015-B417-64D01F9F1C9B}.All|x64.Build.0 = Release|Any CPU + {A4C223A8-6D90-4015-B417-64D01F9F1C9B}.All|x86.ActiveCfg = Release|Any CPU + {A4C223A8-6D90-4015-B417-64D01F9F1C9B}.All|x86.Build.0 = Release|Any CPU {A4C223A8-6D90-4015-B417-64D01F9F1C9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A4C223A8-6D90-4015-B417-64D01F9F1C9B}.Debug|Any CPU.Build.0 = Debug|Any CPU {A4C223A8-6D90-4015-B417-64D01F9F1C9B}.Debug|ARM.ActiveCfg = Debug|Any CPU @@ -41,6 +53,14 @@ Global {A4C223A8-6D90-4015-B417-64D01F9F1C9B}.Release|x64.Build.0 = Release|Any CPU {A4C223A8-6D90-4015-B417-64D01F9F1C9B}.Release|x86.ActiveCfg = Release|Any CPU {A4C223A8-6D90-4015-B417-64D01F9F1C9B}.Release|x86.Build.0 = Release|Any CPU + {71BC5B4C-E5E3-4C46-933D-A772CA3C0499}.All|Any CPU.ActiveCfg = Release|Any CPU + {71BC5B4C-E5E3-4C46-933D-A772CA3C0499}.All|Any CPU.Build.0 = Release|Any CPU + {71BC5B4C-E5E3-4C46-933D-A772CA3C0499}.All|ARM.ActiveCfg = Release|Any CPU + {71BC5B4C-E5E3-4C46-933D-A772CA3C0499}.All|ARM.Build.0 = Release|Any CPU + {71BC5B4C-E5E3-4C46-933D-A772CA3C0499}.All|x64.ActiveCfg = Release|Any CPU + {71BC5B4C-E5E3-4C46-933D-A772CA3C0499}.All|x64.Build.0 = Release|Any CPU + {71BC5B4C-E5E3-4C46-933D-A772CA3C0499}.All|x86.ActiveCfg = Release|Any CPU + {71BC5B4C-E5E3-4C46-933D-A772CA3C0499}.All|x86.Build.0 = Release|Any CPU {71BC5B4C-E5E3-4C46-933D-A772CA3C0499}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {71BC5B4C-E5E3-4C46-933D-A772CA3C0499}.Debug|Any CPU.Build.0 = Debug|Any CPU {71BC5B4C-E5E3-4C46-933D-A772CA3C0499}.Debug|ARM.ActiveCfg = Debug|Any CPU @@ -57,6 +77,18 @@ Global {71BC5B4C-E5E3-4C46-933D-A772CA3C0499}.Release|x64.Build.0 = Release|Any CPU {71BC5B4C-E5E3-4C46-933D-A772CA3C0499}.Release|x86.ActiveCfg = Release|Any CPU {71BC5B4C-E5E3-4C46-933D-A772CA3C0499}.Release|x86.Build.0 = Release|Any CPU + {0D1A6417-F76C-4562-A12B-B3E0AB90F294}.All|Any CPU.ActiveCfg = Release|x64 + {0D1A6417-F76C-4562-A12B-B3E0AB90F294}.All|Any CPU.Build.0 = Release|x64 + {0D1A6417-F76C-4562-A12B-B3E0AB90F294}.All|Any CPU.Deploy.0 = Release|x64 + {0D1A6417-F76C-4562-A12B-B3E0AB90F294}.All|ARM.ActiveCfg = Release|ARM + {0D1A6417-F76C-4562-A12B-B3E0AB90F294}.All|ARM.Build.0 = Release|ARM + {0D1A6417-F76C-4562-A12B-B3E0AB90F294}.All|ARM.Deploy.0 = Release|ARM + {0D1A6417-F76C-4562-A12B-B3E0AB90F294}.All|x64.ActiveCfg = Release|x64 + {0D1A6417-F76C-4562-A12B-B3E0AB90F294}.All|x64.Build.0 = Release|x64 + {0D1A6417-F76C-4562-A12B-B3E0AB90F294}.All|x64.Deploy.0 = Release|x64 + {0D1A6417-F76C-4562-A12B-B3E0AB90F294}.All|x86.ActiveCfg = Release|x86 + {0D1A6417-F76C-4562-A12B-B3E0AB90F294}.All|x86.Build.0 = Release|x86 + {0D1A6417-F76C-4562-A12B-B3E0AB90F294}.All|x86.Deploy.0 = Release|x86 {0D1A6417-F76C-4562-A12B-B3E0AB90F294}.Debug|Any CPU.ActiveCfg = Debug|x86 {0D1A6417-F76C-4562-A12B-B3E0AB90F294}.Debug|ARM.ActiveCfg = Debug|ARM {0D1A6417-F76C-4562-A12B-B3E0AB90F294}.Debug|ARM.Build.0 = Debug|ARM @@ -77,38 +109,48 @@ Global {0D1A6417-F76C-4562-A12B-B3E0AB90F294}.Release|x86.ActiveCfg = Release|x86 {0D1A6417-F76C-4562-A12B-B3E0AB90F294}.Release|x86.Build.0 = Release|x86 {0D1A6417-F76C-4562-A12B-B3E0AB90F294}.Release|x86.Deploy.0 = Release|x86 - {796A3452-8359-494E-8AB8-946CE61FED18}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {796A3452-8359-494E-8AB8-946CE61FED18}.Debug|Any CPU.Build.0 = Debug|Any CPU - {796A3452-8359-494E-8AB8-946CE61FED18}.Debug|ARM.ActiveCfg = Debug|ARM - {796A3452-8359-494E-8AB8-946CE61FED18}.Debug|ARM.Build.0 = Debug|ARM - {796A3452-8359-494E-8AB8-946CE61FED18}.Debug|x64.ActiveCfg = Debug|x64 - {796A3452-8359-494E-8AB8-946CE61FED18}.Debug|x64.Build.0 = Debug|x64 - {796A3452-8359-494E-8AB8-946CE61FED18}.Debug|x86.ActiveCfg = Debug|x86 - {796A3452-8359-494E-8AB8-946CE61FED18}.Debug|x86.Build.0 = Debug|x86 - {796A3452-8359-494E-8AB8-946CE61FED18}.Release|Any CPU.ActiveCfg = Release|Any CPU - {796A3452-8359-494E-8AB8-946CE61FED18}.Release|Any CPU.Build.0 = Release|Any CPU - {796A3452-8359-494E-8AB8-946CE61FED18}.Release|ARM.ActiveCfg = Release|ARM - {796A3452-8359-494E-8AB8-946CE61FED18}.Release|ARM.Build.0 = Release|ARM - {796A3452-8359-494E-8AB8-946CE61FED18}.Release|x64.ActiveCfg = Release|x64 - {796A3452-8359-494E-8AB8-946CE61FED18}.Release|x64.Build.0 = Release|x64 - {796A3452-8359-494E-8AB8-946CE61FED18}.Release|x86.ActiveCfg = Release|x86 - {796A3452-8359-494E-8AB8-946CE61FED18}.Release|x86.Build.0 = Release|x86 - {8AD9E6C1-54B5-4548-AC66-19510A6C6FED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8AD9E6C1-54B5-4548-AC66-19510A6C6FED}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8AD9E6C1-54B5-4548-AC66-19510A6C6FED}.Debug|ARM.ActiveCfg = Debug|Any CPU - {8AD9E6C1-54B5-4548-AC66-19510A6C6FED}.Debug|ARM.Build.0 = Debug|Any CPU - {8AD9E6C1-54B5-4548-AC66-19510A6C6FED}.Debug|x64.ActiveCfg = Debug|Any CPU - {8AD9E6C1-54B5-4548-AC66-19510A6C6FED}.Debug|x64.Build.0 = Debug|Any CPU - {8AD9E6C1-54B5-4548-AC66-19510A6C6FED}.Debug|x86.ActiveCfg = Debug|Any CPU - {8AD9E6C1-54B5-4548-AC66-19510A6C6FED}.Debug|x86.Build.0 = Debug|Any CPU - {8AD9E6C1-54B5-4548-AC66-19510A6C6FED}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8AD9E6C1-54B5-4548-AC66-19510A6C6FED}.Release|Any CPU.Build.0 = Release|Any CPU - {8AD9E6C1-54B5-4548-AC66-19510A6C6FED}.Release|ARM.ActiveCfg = Release|Any CPU - {8AD9E6C1-54B5-4548-AC66-19510A6C6FED}.Release|ARM.Build.0 = Release|Any CPU - {8AD9E6C1-54B5-4548-AC66-19510A6C6FED}.Release|x64.ActiveCfg = Release|Any CPU - {8AD9E6C1-54B5-4548-AC66-19510A6C6FED}.Release|x64.Build.0 = Release|Any CPU - {8AD9E6C1-54B5-4548-AC66-19510A6C6FED}.Release|x86.ActiveCfg = Release|Any CPU - {8AD9E6C1-54B5-4548-AC66-19510A6C6FED}.Release|x86.Build.0 = Release|Any CPU + {9EBF043A-3FF8-46CB-A204-3E3BDF35ED49}.All|Any CPU.ActiveCfg = Release|Any CPU + {9EBF043A-3FF8-46CB-A204-3E3BDF35ED49}.All|Any CPU.Build.0 = Release|Any CPU + {9EBF043A-3FF8-46CB-A204-3E3BDF35ED49}.All|ARM.ActiveCfg = Release|Any CPU + {9EBF043A-3FF8-46CB-A204-3E3BDF35ED49}.All|ARM.Build.0 = Release|Any CPU + {9EBF043A-3FF8-46CB-A204-3E3BDF35ED49}.All|x64.ActiveCfg = Release|Any CPU + {9EBF043A-3FF8-46CB-A204-3E3BDF35ED49}.All|x64.Build.0 = Release|Any CPU + {9EBF043A-3FF8-46CB-A204-3E3BDF35ED49}.All|x86.ActiveCfg = Release|Any CPU + {9EBF043A-3FF8-46CB-A204-3E3BDF35ED49}.All|x86.Build.0 = Release|Any CPU + {9EBF043A-3FF8-46CB-A204-3E3BDF35ED49}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9EBF043A-3FF8-46CB-A204-3E3BDF35ED49}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9EBF043A-3FF8-46CB-A204-3E3BDF35ED49}.Debug|ARM.ActiveCfg = Debug|Any CPU + {9EBF043A-3FF8-46CB-A204-3E3BDF35ED49}.Debug|ARM.Build.0 = Debug|Any CPU + {9EBF043A-3FF8-46CB-A204-3E3BDF35ED49}.Debug|x64.ActiveCfg = Debug|Any CPU + {9EBF043A-3FF8-46CB-A204-3E3BDF35ED49}.Debug|x64.Build.0 = Debug|Any CPU + {9EBF043A-3FF8-46CB-A204-3E3BDF35ED49}.Debug|x86.ActiveCfg = Debug|Any CPU + {9EBF043A-3FF8-46CB-A204-3E3BDF35ED49}.Debug|x86.Build.0 = Debug|Any CPU + {9EBF043A-3FF8-46CB-A204-3E3BDF35ED49}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9EBF043A-3FF8-46CB-A204-3E3BDF35ED49}.Release|Any CPU.Build.0 = Release|Any CPU + {9EBF043A-3FF8-46CB-A204-3E3BDF35ED49}.Release|ARM.ActiveCfg = Release|Any CPU + {9EBF043A-3FF8-46CB-A204-3E3BDF35ED49}.Release|ARM.Build.0 = Release|Any CPU + {9EBF043A-3FF8-46CB-A204-3E3BDF35ED49}.Release|x64.ActiveCfg = Release|Any CPU + {9EBF043A-3FF8-46CB-A204-3E3BDF35ED49}.Release|x64.Build.0 = Release|Any CPU + {9EBF043A-3FF8-46CB-A204-3E3BDF35ED49}.Release|x86.ActiveCfg = Release|Any CPU + {9EBF043A-3FF8-46CB-A204-3E3BDF35ED49}.Release|x86.Build.0 = Release|Any CPU + {6592E67D-A611-4643-B73F-3F1A7C1C5F50}.All|Any CPU.ActiveCfg = Release|x86 + {6592E67D-A611-4643-B73F-3F1A7C1C5F50}.All|Any CPU.Build.0 = Release|x86 + {6592E67D-A611-4643-B73F-3F1A7C1C5F50}.All|ARM.ActiveCfg = Release|x86 + {6592E67D-A611-4643-B73F-3F1A7C1C5F50}.All|ARM.Build.0 = Release|x86 + {6592E67D-A611-4643-B73F-3F1A7C1C5F50}.All|x64.ActiveCfg = Release|x86 + {6592E67D-A611-4643-B73F-3F1A7C1C5F50}.All|x64.Build.0 = Release|x86 + {6592E67D-A611-4643-B73F-3F1A7C1C5F50}.All|x86.ActiveCfg = Release|x86 + {6592E67D-A611-4643-B73F-3F1A7C1C5F50}.All|x86.Build.0 = Release|x86 + {6592E67D-A611-4643-B73F-3F1A7C1C5F50}.Debug|Any CPU.ActiveCfg = Debug|x86 + {6592E67D-A611-4643-B73F-3F1A7C1C5F50}.Debug|ARM.ActiveCfg = Debug|x86 + {6592E67D-A611-4643-B73F-3F1A7C1C5F50}.Debug|x64.ActiveCfg = Debug|x86 + {6592E67D-A611-4643-B73F-3F1A7C1C5F50}.Debug|x86.ActiveCfg = Debug|x86 + {6592E67D-A611-4643-B73F-3F1A7C1C5F50}.Debug|x86.Build.0 = Debug|x86 + {6592E67D-A611-4643-B73F-3F1A7C1C5F50}.Release|Any CPU.ActiveCfg = Release|x86 + {6592E67D-A611-4643-B73F-3F1A7C1C5F50}.Release|ARM.ActiveCfg = Release|x86 + {6592E67D-A611-4643-B73F-3F1A7C1C5F50}.Release|x64.ActiveCfg = Release|x86 + {6592E67D-A611-4643-B73F-3F1A7C1C5F50}.Release|x86.ActiveCfg = Release|x86 + {6592E67D-A611-4643-B73F-3F1A7C1C5F50}.Release|x86.Build.0 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE