diff --git a/Plugins/Wox.Plugin.Program/Programs/UWP.cs b/Plugins/Wox.Plugin.Program/Programs/UWP.cs index f6c1c4ee55..2800437ba9 100644 --- a/Plugins/Wox.Plugin.Program/Programs/UWP.cs +++ b/Plugins/Wox.Plugin.Program/Programs/UWP.cs @@ -5,13 +5,13 @@ using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Security.Principal; +using System.Text; +using System.Threading.Tasks; using System.Windows.Media; using System.Windows.Media.Imaging; +using System.Xml.Linq; using Windows.ApplicationModel; -using Windows.ApplicationModel.Core; -using Windows.Foundation; using Windows.Management.Deployment; -using Windows.Storage.Streams; using AppxPackaing; using Shell; using Wox.Infrastructure; @@ -26,29 +26,38 @@ namespace Wox.Plugin.Program.Programs public string Name { get; } public string FullName { get; } public string FamilyName { get; } - - public string DisplayName { get; set; } - public string Description { get; set; } - public string PublisherDisplayName { get; set; } public string Location { get; set; } public Application[] Apps { get; set; } public Package Package { get; } + public PackageVersion Version { get; set; } + public UWP(Package package) { Package = package; + Location = Package.InstalledLocation.Path; Name = Package.Id.Name; FullName = Package.Id.FullName; FamilyName = Package.Id.FamilyName; - Location = Package.InstalledLocation.Path; - Apps = MergedApps(); + InitializeAppInfo(); + Apps = Apps.Where(a => + { + var valid = + !string.IsNullOrEmpty(a.UserModelId) && + !string.IsNullOrEmpty(a.DisplayName); + return valid; + }).ToArray(); } - private Application[] AppInfos() + private void InitializeAppInfo() { var path = Path.Combine(Location, "AppxManifest.xml"); - var appx = new AppxFactory(); + + var namespaces = XmlNamespaces(path); + InitPackageVersion(namespaces); + + var appxFactory = new AppxFactory(); IStream stream; const uint noAttribute = 0x80; const Stgm exclusiveRead = Stgm.Read | Stgm.ShareExclusive; @@ -56,129 +65,249 @@ namespace Wox.Plugin.Program.Programs if (result == Hresult.Ok) { - var reader = appx.CreateManifestReader(stream); - - var properties = reader.GetProperties(); - PublisherDisplayName = properties.GetStringValue("PublisherDisplayName"); - DisplayName = properties.GetStringValue("DisplayName"); - Description = properties.GetStringValue("Description"); - - var apps = reader.GetApplications(); - var parsedApps = new List(); - while (apps.GetHasCurrent() != 0) + var reader = appxFactory.CreateManifestReader(stream); + var manifestApps = reader.GetApplications(); + var apps = new List(); + while (manifestApps.GetHasCurrent() != 0) { - var current = apps.GetCurrent(); - var appListEntry = current.GetStringValue("AppListEntry"); + var manifestApp = manifestApps.GetCurrent(); + var appListEntry = manifestApp.GetStringValue("AppListEntry"); if (appListEntry != "none") { var app = new Application { - UserModelId = current.GetAppUserModelId(), - BackgroundColor = current.GetStringValue("BackgroundColor") ?? string.Empty, - Location = Location, - LogoPath = Application.LogoFromManifest(current, Location), - Valid = true // useless for now + UserModelId = manifestApp.GetAppUserModelId(), + DisplayName = manifestApp.GetStringValue("DisplayName"), + Description = manifestApp.GetStringValue("Description"), + BackgroundColor = manifestApp.GetStringValue("BackgroundColor"), + Location = Location }; - - if (!string.IsNullOrEmpty(app.UserModelId)) - { - parsedApps.Add(app); - } + app.DisplayName = ResourceFromPri(FullName, app.DisplayName); + app.Description = ResourceFromPri(FullName, app.Description); + // todo move into Application class, so it can specific app info when logo error + app.LogoUri = LogoUriFromManifest(manifestApp); + app.LogoPath = LogoPathFromUri(app.LogoUri); + apps.Add(app); } - apps.MoveNext(); + manifestApps.MoveNext(); } + Apps = apps.Where(a => a.AppListEntry != "none").ToArray(); + } + } - return parsedApps.ToArray(); + /// http://www.hanselman.com/blog/GetNamespacesFromAnXMLDocumentWithXPathDocumentAndLINQToXML.aspx + private static string[] XmlNamespaces(string path) + { + XDocument z = XDocument.Load(path); + if (z.Root != null) + { + var namespaces = z.Root.Attributes(). + Where(a => a.IsNamespaceDeclaration). + GroupBy( + a => a.Name.Namespace == XNamespace.None ? string.Empty : a.Name.LocalName, + a => XNamespace.Get(a.Value) + ).Select( + g => g.First().ToString() + ).ToArray(); + return namespaces; } else { - return new Application[] { }; + Log.Error($"can't find namespaces for <{path}>"); + return new string[] {}; } } - private Application[] AppDisplayInfos() + private void InitPackageVersion(string[] namespaces) { - IReadOnlyList apps; - try + var versionFromNamespace = new Dictionary { - apps = Package.GetAppListEntriesAsync().AsTask().Result; - } - catch (Exception e) + {"http://schemas.microsoft.com/appx/manifest/foundation/windows10", PackageVersion.Windows10}, + {"http://schemas.microsoft.com/appx/2013/manifest", PackageVersion.Windows81}, + {"http://schemas.microsoft.com/appx/2010/manifest", PackageVersion.Windows8}, + }; + + foreach (var n in versionFromNamespace.Keys) { - var message = $"{e.Message} @ {Name}"; - Console.WriteLine(message); - return new Application[] { }; + if (namespaces.Contains(n)) + { + Version = versionFromNamespace[n]; + return; + } } - var displayinfos = apps.Select(a => - { - RandomAccessStreamReference logo; - try - { - // todo: which size is valid? - logo = a.DisplayInfo.GetLogo(new Size(44, 44)); - } - catch (Exception e) - { - var message = $"Can't get logo for {Name}"; - Log.Error(message); - Log.Exception(e); - logo = RandomAccessStreamReference.CreateFromUri(new Uri(Constant.ErrorIcon)); - } - var parsed = new Application - { - DisplayName = a.DisplayInfo.DisplayName, - Description = a.DisplayInfo.Description, - LogoStream = logo - }; - return parsed; - }).ToArray(); - - return displayinfos; + Log.Error($"Unknown Appmanifest version: {FullName}"); + Version = PackageVersion.Unknown; } - private Application[] MergedApps() + private string LogoUriFromManifest(IAppxManifestApplication app) { - // todo can't find api, so just hard code it - if (Location.Contains("SystemApps") || Location.Contains("WindowsApps")) + var logoKeyFromVersion = new Dictionary { - // we must parse AppInfo first, because we want to make sure AppListEntry != "none" - var infos = AppInfos(); - if (infos.Length > 0) + {PackageVersion.Windows10, "Square44x44Logo"}, + {PackageVersion.Windows81, "Square30x30Logo"}, + {PackageVersion.Windows8, "SmallLogo"}, + }; + if (logoKeyFromVersion.ContainsKey(Version)) + { + var key = logoKeyFromVersion[Version]; + var logoUri = app.GetStringValue(key); + return logoUri; + } + else + { + return string.Empty; + } + } + + private string LogoPathFromUri(string uri) + { + // all https://msdn.microsoft.com/windows/uwp/controls-and-patterns/tiles-and-notifications-app-assets + // windows 10 https://msdn.microsoft.com/en-us/library/windows/apps/dn934817.aspx + // windows 8.1 https://msdn.microsoft.com/en-us/library/windows/apps/hh965372.aspx#target_size + // windows 8 https://msdn.microsoft.com/en-us/library/windows/apps/br211475.aspx + + string path; + if (uri.Contains("\\")) + { + path = Path.Combine(Location, uri); + } + else + { + // for C:\Windows\MiracastView etc + path = Path.Combine(Location, "Assets", uri); + } + + var extension = Path.GetExtension(path); + if (extension != null) + { + var end = path.Length - extension.Length; + var prefix = path.Substring(0, end); + var paths = new List {path}; + + // todo hidpi icon + if (Version == PackageVersion.Windows10) { - var displayInfos = AppDisplayInfos(); - var apps = infos; - // todo: temp hack for multipla application mismatch problem - // e.g. mail and calendar, skype video and messaging - // https://github.com/Wox-launcher/Wox/issues/198#issuecomment-244778783 - var length = infos.Length; - for (int i = 0; i < length; i++) + paths.Add($"{prefix}.scale-100{extension}"); + paths.Add($"{prefix}.scale-200{extension}"); + } + else if (Version == PackageVersion.Windows81) + { + paths.Add($"{prefix}.scale-100{extension}"); + paths.Add($"{prefix}.scale-120{extension}"); + paths.Add($"{prefix}.scale-140{extension}"); + paths.Add($"{prefix}.scale-160{extension}"); + paths.Add($"{prefix}.scale-180{extension}"); + } + else if (Version == PackageVersion.Windows8) + { + paths.Add($"{prefix}.scale-100{extension}"); + } + + var selected = paths.FirstOrDefault(File.Exists); + if (!string.IsNullOrEmpty(selected)) + { + return selected; + } + else + { + Log.Error($"<{FullName}> can't find logo uri: <{uri}>"); + return string.Empty; + } + } + else + { + Log.Error($"<{FullName}> cantains uri doesn't have extension: <{uri}>"); + return string.Empty; + } + } + + + private string ResourceFromPri(string packageFullName, string resourceReference) + { + const string prefix = "ms-resource:"; + if (!string.IsNullOrWhiteSpace(resourceReference) && resourceReference.StartsWith(prefix)) + { + // magic comes from @talynone + // https://github.com/talynone/Wox.Plugin.WindowsUniversalAppLauncher/blob/master/StoreAppLauncher/Helpers/NativeApiHelper.cs#L139-L153 + string key = resourceReference.Substring(prefix.Length); + string parsed; + if (key.StartsWith("//")) + { + parsed = prefix + key; + } + else if (key.StartsWith("/")) + { + parsed = prefix + "//" + key; + } + else + { + parsed = $"{prefix}//{Name}/resources/{key}"; + } + + var outBuffer = new StringBuilder(128); + string source = $"@{{{packageFullName}? {parsed}}}"; + var capacity = (uint) outBuffer.Capacity; + var hResult = SHLoadIndirectString(source, outBuffer, capacity, IntPtr.Zero); + if (hResult == Hresult.Ok) + { + var loaded = outBuffer.ToString(); + if (!string.IsNullOrEmpty(loaded)) { - var j = length - i - 1; - apps[i].DisplayName = displayInfos[j].DisplayName; - apps[i].Description = displayInfos[j].Description; - apps[i].LogoStream = displayInfos[j].LogoStream; + return loaded; } - return apps; + else + { + var error = $"Load {source} failed, null or empty result"; + Debug.WriteLine(error); + return string.Empty; + } + } + else + { + // known hresult 2147942522: + // 'Microsoft Corporation' violates pattern constraint of '\bms-resource:.{1,256}'. + // for + // Microsoft.MicrosoftOfficeHub_17.7608.23501.0_x64__8wekyb3d8bbwe: ms-resource://Microsoft.MicrosoftOfficeHub/officehubintl/AppManifest_GetOffice_Description + // Microsoft.BingFoodAndDrink_3.0.4.336_x64__8wekyb3d8bbwe: ms-resource:AppDescription + var message = $"Load {source} failed, HResult error code: {hResult}"; + Log.Error(message); + var exception = Marshal.GetExceptionForHR((int) hResult); + Log.Exception(exception); + return string.Empty; } } - return new Application[] { }; + else + { + return resourceReference; + } } public static Application[] All() { + + var windows10 = new Version(10, 0); var support = Environment.OSVersion.Version.Major >= windows10.Major; if (support) { - var applications = CurrentUserPackages().AsParallel().SelectMany(p => new UWP(p).Apps); - applications = applications.Where(a => a.Valid); - return applications.ToArray(); + var watch = new System.Diagnostics.Stopwatch(); + watch.Start(); + + var applications = CurrentUserPackages().AsParallel().SelectMany(p => new UWP(p).Apps).ToArray(); + + watch.Stop(); + Log.Info("UWP ALL" + watch.ElapsedMilliseconds); + + return applications; + } else { - return new Application[] { }; + return new Application[] {}; } + + } private static IEnumerable CurrentUserPackages() @@ -190,12 +319,14 @@ namespace Wox.Plugin.Program.Programs var userSecurityId = user.Value; var packageManager = new PackageManager(); var packages = packageManager.FindPackagesForUser(userSecurityId); - packages = packages.Where(p => !p.IsFramework && !p.IsDevelopmentMode && !string.IsNullOrEmpty(p.InstalledLocation.Path)); + packages = + packages.Where( + p => !p.IsFramework && !p.IsDevelopmentMode && !string.IsNullOrEmpty(p.InstalledLocation.Path)); return packages; } else { - return new Package[] { }; + return new Package[] {}; } } @@ -225,23 +356,22 @@ namespace Wox.Plugin.Program.Programs public class Application : IProgram { + public string AppListEntry { get; set; } public string DisplayName { get; set; } public string Description { get; set; } - public RandomAccessStreamReference LogoStream { get; set; } public string UserModelId { get; set; } - public string PublisherDisplayName { get; set; } public string BackgroundColor { get; set; } - public string LogoPath { get; set; } + public string LogoUri { get; set; } + public string LogoPath { get; set; } public string Location { get; set; } - public bool Valid { get; set; } private int Score(string query) { var score1 = StringMatcher.Score(DisplayName, query); var score2 = StringMatcher.ScoreForPinyin(DisplayName, query); var score3 = StringMatcher.Score(Description, query); - var score = new[] { score1, score2, score3 }.Max(); + var score = new[] {score1, score2, score3}.Max(); return score; } @@ -294,205 +424,103 @@ namespace Wox.Plugin.Program.Programs return contextMenus; } - private void Launch(IPublicAPI api) + private async void Launch(IPublicAPI api) { - try + var appManager = new ApplicationActivationManager(); + uint unusedPid; + const string noArgs = ""; + const ACTIVATEOPTIONS noFlags = ACTIVATEOPTIONS.AO_NONE; + await Task.Run(() => { - var appManager = new ApplicationActivationManager(); - uint unusedPid; - const string noArgs = ""; - const ACTIVATEOPTIONS noFlags = ACTIVATEOPTIONS.AO_NONE; - appManager.ActivateApplication(UserModelId, noArgs, noFlags, out unusedPid); - } - catch (Exception) - { - var name = "Plugin: Program"; - var message = $"Can't start UWP: {DisplayName}"; - api.ShowMsg(name, message, string.Empty); - } + try + { + appManager.ActivateApplication(UserModelId, noArgs, noFlags, out unusedPid); + } + catch (Exception) + { + var name = "Plugin: Program"; + var message = $"Can't start UWP: {DisplayName}"; + api.ShowMsg(name, message, string.Empty); + } + }); } public ImageSource Logo() { - var logo = !string.IsNullOrEmpty(LogoPath) ? ImageFromPath(LogoPath) : ImageFromStream(LogoStream); - var validBaground = !string.IsNullOrEmpty(BackgroundColor) && BackgroundColor != "transparent"; - var plated = validBaground ? PlatedImage(logo) : logo; + var logo = ImageFromPath(LogoPath); + var plated = PlatedImage(logo); // todo magic! temp fix for cross thread object plated.Freeze(); return plated; } - internal static string LogoFromManifest(IAppxManifestApplication application, string location) - { - // todo use hidpi logo when use hidpi screen - var path1 = Path.Combine(location, application.GetStringValue("Square44x44Logo")); - path1 = LogoFromPath(path1); - if (!string.IsNullOrEmpty(path1)) - { - return path1; - } - else - { - var path2 = Path.Combine(location, application.GetStringValue("Square150x150Logo")); - path2 = LogoFromPath(path2); - if (!string.IsNullOrEmpty(path2)) - { - return path2; - } - else - { - return Constant.ErrorIcon; - } - } - } - - private static string LogoFromPath(string path) - { - if (!File.Exists(path)) - { - // https://msdn.microsoft.com/windows/uwp/controls-and-patterns/tiles-and-notifications-app-assets - var extension = Path.GetExtension(path); - if (!string.IsNullOrEmpty(extension)) - { - var paths = new List(); - var end = path.Length - extension.Length; - var prefix = path.Substring(0, end); - // todo: remove hard cod scale - paths.Add($"{prefix}.scale-200{extension}"); - paths.Add($"{prefix}.scale-100{extension}"); - - // hack for C:\Windows\ImmersiveControlPanel - var directory = Directory.GetParent(path).FullName; - var filename = Path.GetFileNameWithoutExtension(path); - prefix = Path.Combine(directory, "images", filename); - paths.Add($"{prefix}.scale-200{extension}"); - paths.Add($"{prefix}.scale-100{extension}"); - - foreach (var p in paths) - { - if (File.Exists(p)) - { - return p; - } - } - return string.Empty; - } - return string.Empty; - } - else - { - // for js based application, e.g cut the rope - return path; - } - } private BitmapImage ImageFromPath(string path) { - if (!File.Exists(path)) + if (File.Exists(path)) { - // https://msdn.microsoft.com/windows/uwp/controls-and-patterns/tiles-and-notifications-app-assets - - var extension = Path.GetExtension(path); - if (!string.IsNullOrEmpty(extension)) - { - var paths = new List(); - var prefix = path.Substring(0, extension.Length); - // todo: remove hard cod scale - paths.Add($"{prefix}.scale-200{extension}"); - paths.Add($"{prefix}.scale-100{extension}"); - // hack for C:\Windows\ImmersiveControlPanel - var directory = Directory.GetParent(path).FullName; - var filename = Path.GetFileNameWithoutExtension(path); - prefix = Path.Combine(directory, "images", filename); - paths.Add($"{prefix}.scale-200{extension}"); - paths.Add($"{prefix}.scale-100{extension}"); - foreach (var p in paths) - { - if (File.Exists(p)) - { - return new BitmapImage(new Uri(p)); - } - } - return new BitmapImage(new Uri(Constant.ErrorIcon)); - } - else - { - return new BitmapImage(new Uri(Constant.ErrorIcon)); - } - } - else - { - // for js based application, e.g cut the rope var image = new BitmapImage(new Uri(path)); return image; } - } - - private BitmapImage ImageFromStream(RandomAccessStreamReference reference) - { - IRandomAccessStreamWithContentType stream; - try + else { - stream = reference.OpenReadAsync().AsTask().Result; - } - catch (Exception e) - { - var message = $"{e.Message} @ {DisplayName}"; - Log.Error(message); - Log.Exception(e); + Log.Error($"Can't get logo for <{UserModelId}> with path <{path}>"); return new BitmapImage(new Uri(Constant.ErrorIcon)); } - - var image = new BitmapImage(); - image.BeginInit(); - image.StreamSource = stream.AsStream(); - image.EndInit(); - return image; } private ImageSource PlatedImage(BitmapImage image) { - var width = image.Width; - var height = image.Height; - var x = 0; - var y = 0; - - var group = new DrawingGroup(); - - var converted = ColorConverter.ConvertFromString(BackgroundColor); - if (converted != null) + if (!string.IsNullOrEmpty(BackgroundColor) && BackgroundColor != "transparent") { - var color = (Color)converted; - var brush = new SolidColorBrush(color); - var pen = new Pen(brush, 1); - var backgroundArea = new Rect(0, 0, width, width); - var rectabgle = new RectangleGeometry(backgroundArea); - var rectDrawing = new GeometryDrawing(brush, pen, rectabgle); - group.Children.Add(rectDrawing); + var width = image.Width; + var height = image.Height; + var x = 0; + var y = 0; - var imageArea = new Rect(x, y, image.Width, image.Height); - var imageDrawing = new ImageDrawing(image, imageArea); - group.Children.Add(imageDrawing); + var group = new DrawingGroup(); - // http://stackoverflow.com/questions/6676072/get-system-drawing-bitmap-of-a-wpf-area-using-visualbrush - var visual = new DrawingVisual(); - var context = visual.RenderOpen(); - context.DrawDrawing(group); - context.Close(); - const int dpiScale100 = 96; - var bitmap = new RenderTargetBitmap( - Convert.ToInt32(width), Convert.ToInt32(height), - dpiScale100, dpiScale100, - PixelFormats.Pbgra32 - ); - bitmap.Render(visual); - return bitmap; + var converted = ColorConverter.ConvertFromString(BackgroundColor); + if (converted != null) + { + var color = (Color) converted; + var brush = new SolidColorBrush(color); + var pen = new Pen(brush, 1); + var backgroundArea = new Rect(0, 0, width, width); + var rectabgle = new RectangleGeometry(backgroundArea); + var rectDrawing = new GeometryDrawing(brush, pen, rectabgle); + group.Children.Add(rectDrawing); + + var imageArea = new Rect(x, y, image.Width, image.Height); + var imageDrawing = new ImageDrawing(image, imageArea); + group.Children.Add(imageDrawing); + + // http://stackoverflow.com/questions/6676072/get-system-drawing-bitmap-of-a-wpf-area-using-visualbrush + var visual = new DrawingVisual(); + var context = visual.RenderOpen(); + context.DrawDrawing(group); + context.Close(); + const int dpiScale100 = 96; + var bitmap = new RenderTargetBitmap( + Convert.ToInt32(width), Convert.ToInt32(height), + dpiScale100, dpiScale100, + PixelFormats.Pbgra32 + ); + bitmap.Render(visual); + return bitmap; + } + else + { + Log.Error($"Can't convert background string <{BackgroundColor}> to color"); + return new BitmapImage(new Uri(Constant.ErrorIcon)); + } } else { - return new BitmapImage(new Uri(Constant.ErrorIcon)); + // todo use windows theme as background + return image; } } @@ -502,19 +530,32 @@ namespace Wox.Plugin.Program.Programs } } + public enum PackageVersion + { + Windows10, + Windows81, + Windows8, + Unknown + } + [Flags] private enum Stgm : uint { Read = 0x0, - ShareExclusive = 0x10 + ShareExclusive = 0x10, } private enum Hresult : uint { - Ok = 0x0000 + Ok = 0x0000, } [DllImport("shlwapi.dll", CharSet = CharSet.Unicode)] - private static extern Hresult SHCreateStreamOnFileEx(string fileName, Stgm grfMode, uint attributes, bool create, IStream reserved, out IStream stream); + private static extern Hresult SHCreateStreamOnFileEx(string fileName, Stgm grfMode, uint attributes, bool create, + IStream reserved, out IStream stream); + + [DllImport("shlwapi.dll", CharSet = CharSet.Unicode)] + private static extern Hresult SHLoadIndirectString(string pszSource, StringBuilder pszOutBuf, uint cchOutBuf, + IntPtr ppvReserved); } -} +} \ No newline at end of file diff --git a/Plugins/Wox.Plugin.Program/Wox.Plugin.Program.csproj b/Plugins/Wox.Plugin.Program/Wox.Plugin.Program.csproj index a7a286a49c..690940b247 100644 --- a/Plugins/Wox.Plugin.Program/Wox.Plugin.Program.csproj +++ b/Plugins/Wox.Plugin.Program/Wox.Plugin.Program.csproj @@ -60,6 +60,7 @@ +