diff --git a/Flow.Launcher.Test/Plugins/ProgramTest.cs b/Flow.Launcher.Test/Plugins/ProgramTest.cs deleted file mode 100644 index e3a05f484f0..00000000000 --- a/Flow.Launcher.Test/Plugins/ProgramTest.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Flow.Launcher.Plugin.Program.Programs; -using NUnit.Framework; -using System; -using Windows.ApplicationModel; - -namespace Flow.Launcher.Test.Plugins -{ - [TestFixture] - public class ProgramTest - { - [TestCase("Microsoft.WindowsCamera", "ms-resource:LensSDK/Resources/AppTitle", "ms-resource://Microsoft.WindowsCamera/LensSDK/Resources/AppTitle")] - [TestCase("microsoft.windowscommunicationsapps", "ms-resource://microsoft.windowscommunicationsapps/hxoutlookintl/AppManifest_MailDesktop_DisplayName", - "ms-resource://microsoft.windowscommunicationsapps/hxoutlookintl/AppManifest_MailDesktop_DisplayName")] - [TestCase("windows.immersivecontrolpanel", "ms-resource:DisplayName", "ms-resource://windows.immersivecontrolpanel/Resources/DisplayName")] - [TestCase("Microsoft.MSPaint", "ms-resource:AppName", "ms-resource://Microsoft.MSPaint/Resources/AppName")] - public void WhenGivenPriReferenceValueShouldReturnCorrectFormat(string packageName, string rawPriReferenceValue, string expectedFormat) - { - // Arrange - var app = new UWP.Application(); - - // Act - var result = UWP.Application.FormattedPriReferenceValue(packageName, rawPriReferenceValue); - - // Assert - Assert.IsTrue(result == expectedFormat, - $"Expected Pri reference format: {expectedFormat}{Environment.NewLine} " + - $"Actual: {result}{Environment.NewLine}"); - } - } -} diff --git a/Plugins/Flow.Launcher.Plugin.Program/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.Program/Languages/en.xaml index 65432280590..700c37046d4 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Languages/en.xaml +++ b/Plugins/Flow.Launcher.Plugin.Program/Languages/en.xaml @@ -81,7 +81,9 @@ Success + Error Successfully disabled this program from displaying in your query This app is not intended to be run as administrator + Unable to run {0} \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.Program/Main.cs b/Plugins/Flow.Launcher.Plugin.Program/Main.cs index feeb8e78a24..c94c1dca2a9 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Main.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Threading.Tasks; using System.Windows.Controls; +using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.Storage; using Flow.Launcher.Plugin.Program.Programs; @@ -225,9 +226,9 @@ public static void StartProcess(Func runProcess, Proc } catch (Exception) { - var name = "Plugin: Program"; - var message = $"Unable to start: {info.FileName}"; - Context.API.ShowMsg(name, message, string.Empty); + var title = Context.API.GetTranslation("flowlauncher_plugin_program_disable_dlgtitle_error"); + var message = string.Format(Context.API.GetTranslation("flowlauncher_plugin_program_run_failed"), info.FileName); + Context.API.ShowMsg(title, string.Format(message, info.FileName), string.Empty); } } diff --git a/Plugins/Flow.Launcher.Plugin.Program/Programs/ApplicationActivationHelper.cs b/Plugins/Flow.Launcher.Plugin.Program/Programs/ApplicationActivationHelper.cs deleted file mode 100644 index 790a70392e6..00000000000 --- a/Plugins/Flow.Launcher.Plugin.Program/Programs/ApplicationActivationHelper.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -namespace Flow.Launcher.Plugin.Program.Programs -{ - public class ApplicationActivationHelper - { - // Reference : https://github.com/MicrosoftEdge/edge-launcher/blob/108e63df0b4cb5cd9d5e45aa7a264690851ec51d/MIcrosoftEdgeLauncherCsharp/Program.cs - public enum ActivateOptions - { - None = 0x00000000, - DesignMode = 0x00000001, - NoErrorUI = 0x00000002, - NoSplashScreen = 0x00000004, - } - - /// ApplicationActivationManager - [ComImport, Guid("2e941141-7f97-4756-ba1d-9decde894a3d"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - interface IApplicationActivationManager - { - IntPtr ActivateApplication([In] string appUserModelId, [In] string arguments, [In] ActivateOptions options, [Out] out uint processId); - IntPtr ActivateForFile([In] string appUserModelId, [In] IntPtr /*IShellItemArray* */ itemArray, [In] string verb, [Out] out uint processId); - IntPtr ActivateForProtocol([In] string appUserModelId, [In] IntPtr /* IShellItemArray* */itemArray, [Out] out uint processId); - } - - // Application Activation Manager Class - [ComImport, Guid("45BA127D-10A8-46EA-8AB7-56EA9078943C")] - public class ApplicationActivationManager : IApplicationActivationManager - { - - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)/*, PreserveSig*/] - public extern IntPtr ActivateApplication([In] string appUserModelId, [In] string arguments, [In] ActivateOptions options, [Out] out uint processId); - - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - public extern IntPtr ActivateForFile([In] string appUserModelId, [In] IntPtr /*IShellItemArray* */ itemArray, [In] string verb, [Out] out uint processId); - - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - public extern IntPtr ActivateForProtocol([In] string appUserModelId, [In] IntPtr /* IShellItemArray* */itemArray, [Out] out uint processId); - } - } -} diff --git a/Plugins/Flow.Launcher.Plugin.Program/Programs/AppxPackageHelper.cs b/Plugins/Flow.Launcher.Plugin.Program/Programs/AppxPackageHelper.cs deleted file mode 100644 index 62416609e9c..00000000000 --- a/Plugins/Flow.Launcher.Plugin.Program/Programs/AppxPackageHelper.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.InteropServices; -using System.Runtime.InteropServices.ComTypes; - -namespace Flow.Launcher.Plugin.Program.Programs -{ - public class AppxPackageHelper - { - // This function returns a list of attributes of applications - public static List GetAppsFromManifest(IStream stream) - { - IAppxFactory appxFactory = (IAppxFactory)new AppxFactory(); - List apps = new List(); - var reader = appxFactory.CreateManifestReader(stream); - var manifestApps = reader.GetApplications(); - while (manifestApps.GetHasCurrent()) - { - string appListEntry; - var manifestApp = manifestApps.GetCurrent(); - manifestApp.GetStringValue("AppListEntry", out appListEntry); - if (appListEntry != "none") - { - apps.Add(manifestApp); - } - manifestApps.MoveNext(); - } - return apps; - } - - // Reference : https://stackoverflow.com/questions/32122679/getting-icon-of-modern-windows-app-from-a-desktop-application - [Guid("5842a140-ff9f-4166-8f5c-62f5b7b0c781"), ComImport] - public class AppxFactory - { - } - - [Guid("BEB94909-E451-438B-B5A7-D79E767B75D8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - public interface IAppxFactory - { - void _VtblGap0_2(); // skip 2 methods - IAppxManifestReader CreateManifestReader(IStream inputStream); - } - - [Guid("4E1BD148-55A0-4480-A3D1-15544710637C"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - public interface IAppxManifestReader - { - void _VtblGap0_1(); // skip 1 method - IAppxManifestProperties GetProperties(); - void _VtblGap1_5(); // skip 5 methods - IAppxManifestApplicationsEnumerator GetApplications(); - } - - [Guid("9EB8A55A-F04B-4D0D-808D-686185D4847A"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - public interface IAppxManifestApplicationsEnumerator - { - IAppxManifestApplication GetCurrent(); - bool GetHasCurrent(); - bool MoveNext(); - } - - [Guid("5DA89BF4-3773-46BE-B650-7E744863B7E8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - public interface IAppxManifestApplication - { - [PreserveSig] - int GetStringValue([MarshalAs(UnmanagedType.LPWStr)] string name, [MarshalAs(UnmanagedType.LPWStr)] out string value); - - [PreserveSig] - int GetAppUserModelId([MarshalAs(UnmanagedType.LPWStr)] out string value); - } - - [Guid("03FAF64D-F26F-4B2C-AAF7-8FE7789B8BCA"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - public interface IAppxManifestProperties - { - [PreserveSig] - int GetBoolValue([MarshalAs(UnmanagedType.LPWStr)]string name, out bool value); - [PreserveSig] - int GetStringValue([MarshalAs(UnmanagedType.LPWStr)] string name, [MarshalAs(UnmanagedType.LPWStr)] out string value); - } - } -} diff --git a/Plugins/Flow.Launcher.Plugin.Program/Programs/UWP.cs b/Plugins/Flow.Launcher.Plugin.Program/Programs/UWP.cs index 8a731f5b998..627502ea1c5 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Programs/UWP.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Programs/UWP.cs @@ -3,22 +3,17 @@ using System.Diagnostics; using System.IO; using System.Linq; -using System.Runtime.InteropServices; -using System.Runtime.InteropServices.ComTypes; 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.Management.Deployment; using Flow.Launcher.Infrastructure; using Flow.Launcher.Plugin.Program.Logger; -using Rect = System.Windows.Rect; using Flow.Launcher.Plugin.SharedModels; -using Flow.Launcher.Infrastructure.Logger; using System.Threading.Channels; +using System.Xml; +using Windows.ApplicationModel.Core; namespace Flow.Launcher.Plugin.Program.Programs { @@ -30,9 +25,8 @@ public class UWP public string FamilyName { get; } public string Location { get; set; } - public Application[] Apps { get; set; } + public Application[] Apps { get; set; } = Array.Empty(); - public PackageVersion Version { get; set; } public UWP(Package package) { @@ -40,71 +34,133 @@ public UWP(Package package) Name = package.Id.Name; FullName = package.Id.FullName; FamilyName = package.Id.FamilyName; - InitializeAppInfo(); } - private void InitializeAppInfo() + public void InitAppsInPackage(Package package) { - var path = Path.Combine(Location, "AppxManifest.xml"); + var applist = new List(); + // WinRT + var appListEntries = package.GetAppListEntries(); + foreach (var app in appListEntries) + { + try + { + var tmp = new Application(app, this); + applist.Add(tmp); + } + catch (Exception e) + { + ProgramLogger.LogException($"|UWP|InitAppsInPackage|{Location}" + + "|Unexpected exception occurs when trying to construct a Application from package" + + $"{FullName} from location {Location}", e); + } + } + Apps = applist.ToArray(); + + try + { + var xmlDoc = GetManifestXml(); + if (xmlDoc == null) + { + return; + } - var namespaces = XmlNamespaces(path); - InitPackageVersion(namespaces); + var xmlRoot = xmlDoc.DocumentElement; + var packageVersion = GetPackageVersionFromManifest(xmlRoot); + if (!smallLogoNameFromVersion.TryGetValue(packageVersion, out string logoName) || + !bigLogoNameFromVersion.TryGetValue(packageVersion, out string bigLogoName)) + { + return; + } - const uint noAttribute = 0x80; - const Stgm nonExclusiveRead = Stgm.Read | Stgm.ShareDenyNone; - var hResult = SHCreateStreamOnFileEx(path, nonExclusiveRead, noAttribute, false, null, out IStream stream); + var namespaceManager = new XmlNamespaceManager(xmlDoc.NameTable); + namespaceManager.AddNamespace("d", "http://schemas.microsoft.com/appx/manifest/foundation/windows10"); // still need a name + namespaceManager.AddNamespace("rescap", "http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"); + namespaceManager.AddNamespace("uap10", "http://schemas.microsoft.com/appx/manifest/uap/windows10/10"); - if (hResult == Hresult.Ok) - { - List _apps = AppxPackageHelper.GetAppsFromManifest(stream); + var allowElevationNode = xmlRoot.SelectSingleNode("//rescap:Capability[@Name='allowElevation']", namespaceManager); + bool packageCanElevate = allowElevationNode != null; - Apps = _apps.Select(x => new Application(x, this)) - .Where(a => !string.IsNullOrEmpty(a.UserModelId) - && !string.IsNullOrEmpty(a.DisplayName)) - .ToArray(); + var appsNode = xmlRoot.SelectSingleNode("d:Applications", namespaceManager); + foreach (var app in Apps) + { + // According to https://learn.microsoft.com/windows/apps/desktop/modernize/grant-identity-to-nonpackaged-apps#create-a-package-manifest-for-the-sparse-package + // and https://learn.microsoft.com/uwp/schemas/appxpackage/uapmanifestschema/element-application#attributes + var id = app.UserModelId.Split('!')[1]; + var appNode = appsNode?.SelectSingleNode($"d:Application[@Id='{id}']", namespaceManager); + if (appNode != null) + { + app.CanRunElevated = packageCanElevate || Application.IfAppCanRunElevated(appNode); + + // local name to fit all versions + var visualElement = appNode.SelectSingleNode($"*[local-name()='VisualElements']", namespaceManager); + var logoUri = visualElement?.Attributes[logoName]?.Value; + app.LogoPath = app.LogoPathFromUri(logoUri, (64, 64)); + var previewUri = visualElement?.Attributes[bigLogoName]?.Value; + app.PreviewImagePath = app.LogoPathFromUri(previewUri, (128, 128)); + } + } } - else + catch (Exception e) { - var e = Marshal.GetExceptionForHR((int)hResult); - ProgramLogger.LogException($"|UWP|InitializeAppInfo|{path}" + - "|Error caused while trying to get the details of the UWP program", e); - - Apps = Array.Empty(); + ProgramLogger.LogException($"|UWP|InitAppsInPackage|{Location}" + + "|Unexpected exception occurs when trying to construct a Application from package" + + $"{FullName} from location {Location}", e); } + } - if (stream != null && Marshal.ReleaseComObject(stream) > 0) + private XmlDocument GetManifestXml() + { + var manifest = Path.Combine(Location, "AppxManifest.xml"); + try + { + var file = File.ReadAllText(manifest); + var xmlDoc = new XmlDocument(); + xmlDoc.LoadXml(file); + return xmlDoc; + } + catch (FileNotFoundException e) + { + ProgramLogger.LogException("UWP", "GetManifestXml", $"{Location}", "AppxManifest.xml not found.", e); + return null; + } + catch (Exception e) { - Log.Error("Flow.Launcher.Plugin.Program.Programs.UWP", "AppxManifest.xml was leaked"); + ProgramLogger.LogException("UWP", "GetManifestXml", $"{Location}", "An unexpected error occured and unable to parse AppxManifest.xml", e); + return null; } } - /// http://www.hanselman.com/blog/GetNamespacesFromAnXMLDocumentWithXPathDocumentAndLINQToXML.aspx - private static string[] XmlNamespaces(string path) + private PackageVersion GetPackageVersionFromManifest(XmlNode xmlRoot) { - XDocument z = XDocument.Load(path); - if (z.Root != null) + if (xmlRoot != 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; + + var namespaces = xmlRoot.Attributes; + foreach (XmlAttribute ns in namespaces) + { + if (versionFromNamespace.TryGetValue(ns.Value, out var packageVersion)) + { + return packageVersion; + } + } + + ProgramLogger.LogException($"|UWP|GetPackageVersionFromManifest|{Location}" + + "|Trying to get the package version of the UWP program, but an unknown UWP appmanifest version in package " + + $"{FullName} from location {Location}", new FormatException()); + return PackageVersion.Unknown; } else { - ProgramLogger.LogException($"|UWP|XmlNamespaces|{path}" + - $"|Error occured while trying to get the XML from {path}", new ArgumentNullException()); - - return Array.Empty(); + ProgramLogger.LogException($"|UWP|GetPackageVersionFromManifest|{Location}" + + "|Can't parse AppManifest.xml of package " + + $"{FullName} from location {Location}", new ArgumentNullException(nameof(xmlRoot))); + return PackageVersion.Unknown; } } - private void InitPackageVersion(string[] namespaces) + private static readonly Dictionary versionFromNamespace = new() { - var versionFromNamespace = new Dictionary - { { "http://schemas.microsoft.com/appx/manifest/foundation/windows10", PackageVersion.Windows10 }, @@ -114,23 +170,33 @@ private void InitPackageVersion(string[] namespaces) { "http://schemas.microsoft.com/appx/2010/manifest", PackageVersion.Windows8 }, - }; + }; - foreach (var n in versionFromNamespace.Keys) + private static readonly Dictionary smallLogoNameFromVersion = new() + { { - if (namespaces.Contains(n)) - { - Version = versionFromNamespace[n]; - return; - } - } - - ProgramLogger.LogException($"|UWP|XmlNamespaces|{Location}" + - "|Trying to get the package version of the UWP program, but a unknown UWP appmanifest version " - + $"{FullName} from location {Location} is returned.", new FormatException()); + PackageVersion.Windows10, "Square44x44Logo" + }, + { + PackageVersion.Windows81, "Square30x30Logo" + }, + { + PackageVersion.Windows8, "SmallLogo" + }, + }; - Version = PackageVersion.Unknown; - } + private static readonly Dictionary bigLogoNameFromVersion = new() + { + { + PackageVersion.Windows10, "Square150x150Logo" + }, + { + PackageVersion.Windows81, "Square150x150Logo" + }, + { + PackageVersion.Windows8, "Logo" + }, + }; public static Application[] All() { @@ -144,6 +210,7 @@ public static Application[] All() try { u = new UWP(p); + u.InitAppsInPackage(p); } #if !DEBUG catch (Exception e) @@ -194,22 +261,19 @@ private static IEnumerable CurrentUserPackages() var ps = m.FindPackagesForUser(id); ps = ps.Where(p => { - bool valid; try { var f = p.IsFramework; var d = p.IsDevelopmentMode; var path = p.InstalledLocation.Path; - valid = !f && !d && !string.IsNullOrEmpty(path); + return !f && !d && !string.IsNullOrEmpty(path); } catch (Exception e) { - ProgramLogger.LogException("UWP", "CurrentUserPackages", $"id", "An unexpected error occured and " + ProgramLogger.LogException("UWP", "CurrentUserPackages", $"{id}", "An unexpected error occured and " + $"unable to verify if package is valid", e); return false; } - - return valid; }); return ps; } @@ -248,7 +312,7 @@ public static async Task WatchPackageChange() PackageChangeChannel.Reader.TryRead(out _); await Task.Run(Main.IndexUwpPrograms); } - + } } @@ -279,23 +343,27 @@ public class Application : IProgram { private string _uid = string.Empty; public string UniqueIdentifier { get => _uid; set => _uid = value == null ? string.Empty : value.ToLowerInvariant(); } - public string DisplayName { get; set; } - public string Description { get; set; } - public string UserModelId { get; set; } - public string BackgroundColor { get; set; } - - public string EntryPoint { get; set; } + public string DisplayName { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public string UserModelId { get; set; } = string.Empty; + //public string BackgroundColor { get; set; } = string.Empty; // preserve for future use public string Name => DisplayName; - public string Location => Package.Location; - - public bool Enabled { get; set; } - public bool CanRunElevated { get; set; } + public string Location { get; set; } = string.Empty; - public string LogoUri { get; set; } - public string LogoPath { get; set; } - public UWP Package { get; set; } + public bool Enabled { get; set; } = false; + public bool CanRunElevated { get; set; } = false; + public string LogoPath { get; set; } = string.Empty; + public string PreviewImagePath { get; set; } = string.Empty; - public Application() { } + public Application(AppListEntry appListEntry, UWP package) + { + UserModelId = appListEntry.AppUserModelId; + UniqueIdentifier = appListEntry.AppUserModelId; + DisplayName = appListEntry.DisplayInfo.DisplayName; + Description = appListEntry.DisplayInfo.Description; + Location = package.Location; + Enabled = true; + } public Result Result(string query, IPublicAPI api) { @@ -335,8 +403,8 @@ public Result Result(string query, IPublicAPI api) var result = new Result { Title = title, - SubTitle = Main._settings.HideAppsPath ? string.Empty : Package.Location, - Icon = Logo, + SubTitle = Main._settings.HideAppsPath ? string.Empty : Location, + IcoPath = LogoPath, Score = matchResult.Score, TitleHighlightData = matchResult.MatchData, ContextData = this, @@ -349,20 +417,13 @@ public Result Result(string query, IPublicAPI api) !e.SpecialKeyState.WinPressed ); - if (elevated && CanRunElevated) + bool shouldRunElevated = elevated && CanRunElevated; + _ = Task.Run(() => Launch(shouldRunElevated)).ConfigureAwait(false); + if (elevated && !shouldRunElevated) { - LaunchElevated(); - } - else - { - Launch(api); - - if (elevated) - { - var title = "Plugin: Program"; - var message = api.GetTranslation("flowlauncher_plugin_program_run_as_administrator_not_supported_message"); - api.ShowMsg(title, message, string.Empty); - } + var title = api.GetTranslation("flowlauncher_plugin_program_disable_dlgtitle_error"); + var message = api.GetTranslation("flowlauncher_plugin_program_run_as_administrator_not_supported_message"); + api.ShowMsg(title, message, string.Empty); } return true; @@ -382,7 +443,7 @@ public List ContextMenus(IPublicAPI api) Title = api.GetTranslation("flowlauncher_plugin_program_open_containing_folder"), Action = _ => { - Main.Context.API.OpenDirectory(Package.Location); + Main.Context.API.OpenDirectory(Location); return true; }, @@ -397,7 +458,7 @@ public List ContextMenus(IPublicAPI api) Title = api.GetTranslation("flowlauncher_plugin_program_run_as_administrator"), Action = _ => { - LaunchElevated(); + Task.Run(() => Launch(true)).ConfigureAwait(false); return true; }, IcoPath = "Images/cmd.png" @@ -407,192 +468,61 @@ public List ContextMenus(IPublicAPI api) return contextMenus; } - private async void Launch(IPublicAPI api) - { - var appManager = new ApplicationActivationHelper.ApplicationActivationManager(); - const string noArgs = ""; - const ApplicationActivationHelper.ActivateOptions noFlags = ApplicationActivationHelper.ActivateOptions.None; - await Task.Run(() => - { - try - { - _ = appManager.ActivateApplication(UserModelId, noArgs, noFlags, out _); - } - catch (Exception) - { - var name = "Plugin: Program"; - var message = $"Can't start UWP: {DisplayName}"; - api.ShowMsg(name, message, string.Empty); - } - }); - } - - private void LaunchElevated() + private void Launch(bool elevated = false) { - string command = "shell:AppsFolder\\" + UniqueIdentifier; + string command = "shell:AppsFolder\\" + UserModelId; command = Environment.ExpandEnvironmentVariables(command.Trim()); var info = new ProcessStartInfo(command) { - UseShellExecute = true, Verb = "runas", + UseShellExecute = true, + Verb = elevated ? "runas" : "" }; Main.StartProcess(Process.Start, info); } - public Application(AppxPackageHelper.IAppxManifestApplication manifestApp, UWP package) - { - // This is done because we cannot use the keyword 'out' along with a property - - manifestApp.GetAppUserModelId(out string tmpUserModelId); - manifestApp.GetAppUserModelId(out string tmpUniqueIdentifier); - manifestApp.GetStringValue("DisplayName", out string tmpDisplayName); - manifestApp.GetStringValue("Description", out string tmpDescription); - manifestApp.GetStringValue("BackgroundColor", out string tmpBackgroundColor); - manifestApp.GetStringValue("EntryPoint", out string tmpEntryPoint); - - UserModelId = tmpUserModelId; - UniqueIdentifier = tmpUniqueIdentifier; - DisplayName = tmpDisplayName; - Description = tmpDescription; - BackgroundColor = tmpBackgroundColor; - EntryPoint = tmpEntryPoint; - - Package = package; - - DisplayName = ResourceFromPri(package.FullName, package.Name, DisplayName); - Description = ResourceFromPri(package.FullName, package.Name, Description); - LogoUri = LogoUriFromManifest(manifestApp); - LogoPath = LogoPathFromUri(LogoUri); - - Enabled = true; - CanRunElevated = CanApplicationRunElevated(); - } - - private bool CanApplicationRunElevated() + internal static bool IfAppCanRunElevated(XmlNode appNode) { - if (EntryPoint == "Windows.FullTrustApplication") - { - return true; - } - - var manifest = Package.Location + "\\AppxManifest.xml"; - if (File.Exists(manifest)) - { - var file = File.ReadAllText(manifest); - - if (file.Contains("TrustLevel=\"mediumIL\"", StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } + // According to https://learn.microsoft.com/windows/apps/desktop/modernize/grant-identity-to-nonpackaged-apps#create-a-package-manifest-for-the-sparse-package + // and https://learn.microsoft.com/uwp/schemas/appxpackage/uapmanifestschema/element-application#attributes - return false; + return appNode?.Attributes["EntryPoint"]?.Value == "Windows.FullTrustApplication" || + appNode?.Attributes["uap10:TrustLevel"]?.Value == "mediumIL"; } - internal string ResourceFromPri(string packageFullName, string packageName, string rawReferenceValue) + internal string LogoPathFromUri(string uri, (int, int) desiredSize) { - if (string.IsNullOrWhiteSpace(rawReferenceValue) || !rawReferenceValue.StartsWith("ms-resource:")) - return rawReferenceValue; - - var formattedPriReference = FormattedPriReferenceValue(packageName, rawReferenceValue); + // 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 - var outBuffer = new StringBuilder(128); - string source = $"@{{{packageFullName}? {formattedPriReference}}}"; - 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)) - { - return loaded; - } - else - { - ProgramLogger.LogException($"|UWP|ResourceFromPri|{Package.Location}|Can't load null or empty result " - + $"pri {source} in uwp location {Package.Location}", new NullReferenceException()); - return string.Empty; - } - } - else + if (string.IsNullOrWhiteSpace(uri)) { - var e = Marshal.GetExceptionForHR((int)hResult); - ProgramLogger.LogException($"|UWP|ResourceFromPri|{Package.Location}|Load pri failed {source} with HResult {hResult} and location {Package.Location}", e); + ProgramLogger.LogException($"|UWP|LogoPathFromUri|{Location}" + + $"|{UserModelId} 's logo uri is null or empty: {Location}", new ArgumentException("uri")); return string.Empty; } - } - - public static string FormattedPriReferenceValue(string packageName, string rawPriReferenceValue) - { - const string prefix = "ms-resource:"; - - if (string.IsNullOrWhiteSpace(rawPriReferenceValue) || !rawPriReferenceValue.StartsWith(prefix)) - return rawPriReferenceValue; - - string key = rawPriReferenceValue.Substring(prefix.Length); - if (key.StartsWith("//")) - return $"{prefix}{key}"; - - if (!key.StartsWith("/")) - { - key = $"/{key}"; - } - if (!key.ToLower().Contains("resources")) - { - key = $"/Resources{key}"; - } + string path = Path.Combine(Location, uri); - return $"{prefix}//{packageName}{key}"; - } - - internal string LogoUriFromManifest(AppxPackageHelper.IAppxManifestApplication app) - { - var logoKeyFromVersion = new Dictionary + var pxCount = desiredSize.Item1 * desiredSize.Item2; + var logoPath = TryToFindLogo(uri, path, pxCount); + if (logoPath == string.Empty) { + var tmp = Path.Combine(Location, "Assets", uri); + if (!path.Equals(tmp, StringComparison.OrdinalIgnoreCase)) { - PackageVersion.Windows10, "Square44x44Logo" - }, - { - PackageVersion.Windows81, "Square30x30Logo" - }, - { - PackageVersion.Windows8, "SmallLogo" - }, - }; - if (logoKeyFromVersion.ContainsKey(Package.Version)) - { - var key = logoKeyFromVersion[Package.Version]; - _ = app.GetStringValue(key, out string logoUri); - return logoUri; - } - else - { - return string.Empty; - } - } - - internal 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 = Path.Combine(Package.Location, uri); - - var logoPath = TryToFindLogo(uri, path); - if (String.IsNullOrEmpty(logoPath)) - { - // TODO: Don't know why, just keep it at the moment - // Maybe on older version of Windows 10? - // for C:\Windows\MiracastView etc - return TryToFindLogo(uri, Path.Combine(Package.Location, "Assets", uri)); + // TODO: Don't know why, just keep it at the moment + // Maybe on older version of Windows 10? + // for C:\Windows\MiracastView etc + return TryToFindLogo(uri, tmp, pxCount); + } } return logoPath; - string TryToFindLogo(string uri, string path) + string TryToFindLogo(string uri, string path, int px) { var extension = Path.GetExtension(path); if (extension != null) @@ -604,42 +534,38 @@ string TryToFindLogo(string uri, string path) var logoNamePrefix = Path.GetFileNameWithoutExtension(uri); // e.g Square44x44 var logoDir = Path.GetDirectoryName(path); // e.g ..\..\Assets - if (String.IsNullOrEmpty(logoNamePrefix) || String.IsNullOrEmpty(logoDir) || !Directory.Exists(logoDir)) + if (String.IsNullOrEmpty(logoNamePrefix) || !Directory.Exists(logoDir)) { // Known issue: Edge always triggers it since logo is not at uri - ProgramLogger.LogException($"|UWP|LogoPathFromUri|{Package.Location}" + - $"|{UserModelId} can't find logo uri for {uri} in package location (logo name or directory not found): {Package.Location}", new FileNotFoundException()); + ProgramLogger.LogException($"|UWP|LogoPathFromUri|{Location}" + + $"|{UserModelId} can't find logo uri for {uri} in package location (logo name or directory not found): {Location}", new FileNotFoundException()); return string.Empty; } - var files = Directory.EnumerateFiles(logoDir); + var logos = Directory.EnumerateFiles(logoDir, $"{logoNamePrefix}*{extension}"); // Currently we don't care which one to choose // Just ignore all qualifiers // select like logo.[xxx_yyy].png // https://learn.microsoft.com/en-us/windows/uwp/app-resources/tailor-resources-lang-scale-contrast - var logos = files.Where(file => - Path.GetFileName(file)?.StartsWith(logoNamePrefix, StringComparison.OrdinalIgnoreCase) ?? false - && extension.Equals(Path.GetExtension(file), StringComparison.OrdinalIgnoreCase) - ); var selected = logos.FirstOrDefault(); var closest = selected; int min = int.MaxValue; - foreach(var logo in logos) + foreach (var logo in logos) { var imageStream = File.OpenRead(logo); var decoder = BitmapDecoder.Create(imageStream, BitmapCreateOptions.IgnoreColorProfile, BitmapCacheOption.None); var height = decoder.Frames[0].PixelHeight; var width = decoder.Frames[0].PixelWidth; - int pixelCountDiff = Math.Abs(height * width - 1936); // 44*44=1936 - if(pixelCountDiff < min) + int pixelCountDiff = Math.Abs(height * width - px); + if (pixelCountDiff < min) { - // try to find the closest to 44x44 logo + // try to find the closest to desired size closest = logo; if (pixelCountDiff == 0) - break; // found 44x44 + break; // found min = pixelCountDiff; } } @@ -651,111 +577,112 @@ string TryToFindLogo(string uri, string path) } else { - ProgramLogger.LogException($"|UWP|LogoPathFromUri|{Package.Location}" + - $"|{UserModelId} can't find logo uri for {uri} in package location (can't find specified logo): {Package.Location}", new FileNotFoundException()); + ProgramLogger.LogException($"|UWP|LogoPathFromUri|{Location}" + + $"|{UserModelId} can't find logo uri for {uri} in package location (can't find specified logo): {Location}", new FileNotFoundException()); return string.Empty; } } else { - ProgramLogger.LogException($"|UWP|LogoPathFromUri|{Package.Location}" + + ProgramLogger.LogException($"|UWP|LogoPathFromUri|{Location}" + $"|Unable to find extension from {uri} for {UserModelId} " + - $"in package location {Package.Location}", new FileNotFoundException()); + $"in package location {Location}", new FileNotFoundException()); return string.Empty; } } } - public ImageSource Logo() - { - var logo = ImageFromPath(LogoPath); - var plated = PlatedImage(logo); // TODO: maybe get plated directly from app package? - - // todo magic! temp fix for cross thread object - plated.Freeze(); - return plated; - } - - - private BitmapImage ImageFromPath(string path) - { - // TODO: Consider using infrastructure.image.imageloader? - if (File.Exists(path)) - { - var image = new BitmapImage(); - image.BeginInit(); - image.UriSource = new Uri(path); - image.CacheOption = BitmapCacheOption.OnLoad; - image.EndInit(); - image.Freeze(); - return image; - } - else - { - ProgramLogger.LogException($"|UWP|ImageFromPath|{(string.IsNullOrEmpty(path) ? "Not Avaliable" : path)}" + - $"|Unable to get logo for {UserModelId} from {path} and" + - $" located in {Package.Location}", new FileNotFoundException()); - return new BitmapImage(new Uri(Constant.MissingImgIcon)); - } - } - - private ImageSource PlatedImage(BitmapImage image) - { - if (!string.IsNullOrEmpty(BackgroundColor) && BackgroundColor != "transparent") - { - 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) - { - 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 - { - ProgramLogger.LogException($"|UWP|PlatedImage|{Package.Location}" + - $"|Unable to convert background string {BackgroundColor} " + - $"to color for {Package.Location}", new InvalidOperationException()); - - return new BitmapImage(new Uri(Constant.MissingImgIcon)); - } - } - else - { - // todo use windows theme as background - return image; - } - } - + #region logo legacy + // preserve for potential future use + + //public ImageSource Logo() + //{ + // var logo = ImageFromPath(LogoPath); + // var plated = PlatedImage(logo); // TODO: maybe get plated directly from app package? + + // // todo magic! temp fix for cross thread object + // plated.Freeze(); + // return plated; + //} + //private BitmapImage ImageFromPath(string path) + //{ + // if (File.Exists(path)) + // { + // var image = new BitmapImage(); + // image.BeginInit(); + // image.UriSource = new Uri(path); + // image.CacheOption = BitmapCacheOption.OnLoad; + // image.EndInit(); + // image.Freeze(); + // return image; + // } + // else + // { + // ProgramLogger.LogException($"|UWP|ImageFromPath|{(string.IsNullOrEmpty(path) ? "Not Avaliable" : path)}" + + // $"|Unable to get logo for {UserModelId} from {path} and" + + // $" located in {Location}", new FileNotFoundException()); + // return new BitmapImage(new Uri(Constant.MissingImgIcon)); + // } + //} + + //private ImageSource PlatedImage(BitmapImage image) + //{ + // if (!string.IsNullOrEmpty(BackgroundColor) && BackgroundColor != "transparent") + // { + // 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) + // { + // 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 + // { + // ProgramLogger.LogException($"|UWP|PlatedImage|{Location}" + + // $"|Unable to convert background string {BackgroundColor} " + + // $"to color for {Location}", new InvalidOperationException()); + + // return new BitmapImage(new Uri(Constant.MissingImgIcon)); + // } + // } + // else + // { + // // todo use windows theme as background + // return image; + // } + //} + + #endregion public override string ToString() { return $"{DisplayName}: {Description}"; @@ -786,27 +713,5 @@ public enum PackageVersion Windows8, Unknown } - - [Flags] - private enum Stgm : uint - { - Read = 0x0, - ShareExclusive = 0x10, - ShareDenyNone = 0x40 - } - - private enum Hresult : uint - { - 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); - - [DllImport("shlwapi.dll", CharSet = CharSet.Unicode)] - private static extern Hresult SHLoadIndirectString(string pszSource, StringBuilder pszOutBuf, uint cchOutBuf, - IntPtr ppvReserved); - } } diff --git a/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs b/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs index 7fbffb548e4..e89970fb4bd 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs @@ -142,10 +142,10 @@ public Result Result(string query, IPublicAPI api) FileName = FullPath, WorkingDirectory = ParentDirectory, UseShellExecute = true, - Verb = runAsAdmin ? "runas" : null + Verb = runAsAdmin ? "runas" : "" }; - Task.Run(() => Main.StartProcess(Process.Start, info)); + _ = Task.Run(() => Main.StartProcess(Process.Start, info)); return true; }