using System; using System.Collections.Generic; 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; namespace Flow.Launcher.Plugin.Program.Programs { [Serializable] public class UWP { public string Name { get; } public string FullName { get; } public string FamilyName { get; } public string Location { get; set; } public Application[] Apps { get; set; } public PackageVersion Version { get; set; } public UWP(Package package) { Location = package.InstalledLocation.Path; Name = package.Id.Name; FullName = package.Id.FullName; FamilyName = package.Id.FamilyName; InitializeAppInfo(); } private void InitializeAppInfo() { var path = Path.Combine(Location, "AppxManifest.xml"); var namespaces = XmlNamespaces(path); InitPackageVersion(namespaces); const uint noAttribute = 0x80; const Stgm nonExclusiveRead = Stgm.Read | Stgm.ShareDenyNone; var hResult = SHCreateStreamOnFileEx(path, nonExclusiveRead, noAttribute, false, null, out IStream stream); if (hResult == Hresult.Ok) { List _apps = AppxPackageHelper.GetAppsFromManifest(stream); Apps = _apps.Select(x => new Application(x, this)) .Where(a => !string.IsNullOrEmpty(a.UserModelId) && !string.IsNullOrEmpty(a.DisplayName)) .ToArray(); } else { 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(); } if (stream != null && Marshal.ReleaseComObject(stream) > 0) { Log.Error("Flow.Launcher.Plugin.Program.Programs.UWP", "AppxManifest.xml was leaked"); } } /// 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 { ProgramLogger.LogException($"|UWP|XmlNamespaces|{path}" + $"|Error occured while trying to get the XML from {path}", new ArgumentNullException()); return Array.Empty(); } } private void InitPackageVersion(string[] namespaces) { var versionFromNamespace = new Dictionary { { "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) { 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()); Version = PackageVersion.Unknown; } 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 => { UWP u; try { u = new UWP(p); } #if !DEBUG catch (Exception e) { ProgramLogger.LogException($"|UWP|All|{p.InstalledLocation}|An unexpected error occured and unable to convert Package to UWP for {p.Id.FullName}", e); return Array.Empty(); } #endif #if DEBUG //make developer aware and implement handling catch { throw; } #endif return u.Apps; }).ToArray(); var updatedListWithoutDisabledApps = applications .Where(t1 => !Main._settings.DisabledProgramSources .Any(x => x.UniqueIdentifier == t1.UniqueIdentifier)); return updatedListWithoutDisabledApps.ToArray(); } else { return Array.Empty(); } } private static IEnumerable CurrentUserPackages() { var u = WindowsIdentity.GetCurrent().User; if (u != null) { var id = u.Value; PackageManager m; try { m = new PackageManager(); } catch { // Bug from https://github.com/microsoft/CsWinRT, using Microsoft.Windows.SDK.NET.Ref 10.0.19041.0. // Only happens on the first time, so a try catch can fix it. m = new PackageManager(); } 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); } catch (Exception e) { ProgramLogger.LogException("UWP", "CurrentUserPackages", $"id", "An unexpected error occured and " + $"unable to verify if package is valid", e); return false; } return valid; }); return ps; } else { return Array.Empty(); } } private static Channel PackageChangeChannel = Channel.CreateBounded(1); public static async Task WatchPackageChange() { if (Environment.OSVersion.Version.Major >= 10) { var catalog = PackageCatalog.OpenForCurrentUser(); catalog.PackageInstalling += (_, args) => { if (args.IsComplete) PackageChangeChannel.Writer.TryWrite(default); }; catalog.PackageUninstalling += (_, args) => { if (args.IsComplete) PackageChangeChannel.Writer.TryWrite(default); }; catalog.PackageUpdating += (_, args) => { if (args.IsComplete) PackageChangeChannel.Writer.TryWrite(default); }; while (await PackageChangeChannel.Reader.WaitToReadAsync().ConfigureAwait(false)) { await Task.Delay(3000).ConfigureAwait(false); PackageChangeChannel.Reader.TryRead(out _); await Task.Run(Main.IndexUwpPrograms); } } } public override string ToString() { return FamilyName; } public override bool Equals(object obj) { if (obj is UWP uwp) { return FamilyName.Equals(uwp.FamilyName); } else { return false; } } public override int GetHashCode() { return FamilyName.GetHashCode(); } [Serializable] public class Application : IProgram { public string UniqueIdentifier { get; set; } 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 Name => DisplayName; public string Location => Package.Location; public bool Enabled { get; set; } public bool CanRunElevated { get; set; } public string LogoUri { get; set; } public string LogoPath { get; set; } public UWP Package { get; set; } public Application() { } public Result Result(string query, IPublicAPI api) { string title; MatchResult matchResult; // We suppose Name won't be null if (!Main._settings.EnableDescription || Description == null || Name.StartsWith(Description)) { title = Name; matchResult = StringMatcher.FuzzySearch(query, title); } else if (Description.StartsWith(Name)) { title = Description; matchResult = StringMatcher.FuzzySearch(query, Description); } else { title = $"{Name}: {Description}"; var nameMatch = StringMatcher.FuzzySearch(query, Name); var desciptionMatch = StringMatcher.FuzzySearch(query, Description); if (desciptionMatch.Score > nameMatch.Score) { for (int i = 0; i < desciptionMatch.MatchData.Count; i++) { desciptionMatch.MatchData[i] += Name.Length + 2; // 2 is ": " } matchResult = desciptionMatch; } else matchResult = nameMatch; } if (!matchResult.Success) return null; var result = new Result { Title = title, SubTitle = Main._settings.HideAppsPath ? string.Empty : Package.Location, Icon = Logo, Score = matchResult.Score, TitleHighlightData = matchResult.MatchData, ContextData = this, Action = e => { var elevated = ( e.SpecialKeyState.CtrlPressed && e.SpecialKeyState.ShiftPressed && !e.SpecialKeyState.AltPressed && !e.SpecialKeyState.WinPressed ); if (elevated && CanRunElevated) { 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); } } return true; } }; return result; } public List ContextMenus(IPublicAPI api) { var contextMenus = new List { new Result { Title = api.GetTranslation("flowlauncher_plugin_program_open_containing_folder"), Action = _ => { Main.Context.API.OpenDirectory(Package.Location); return true; }, IcoPath = "Images/folder.png" } }; if (CanRunElevated) { contextMenus.Add(new Result { Title = api.GetTranslation("flowlauncher_plugin_program_run_as_administrator"), Action = _ => { LaunchElevated(); return true; }, IcoPath = "Images/cmd.png" }); } 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() { string command = "shell:AppsFolder\\" + UniqueIdentifier; command = Environment.ExpandEnvironmentVariables(command.Trim()); var info = new ProcessStartInfo(command) { UseShellExecute = true, Verb = "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() { 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; } } return false; } internal string ResourceFromPri(string packageFullName, string packageName, string rawReferenceValue) { if (string.IsNullOrWhiteSpace(rawReferenceValue) || !rawReferenceValue.StartsWith("ms-resource:")) return rawReferenceValue; var formattedPriReference = FormattedPriReferenceValue(packageName, rawReferenceValue); 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 { var e = Marshal.GetExceptionForHR((int)hResult); ProgramLogger.LogException($"|UWP|ResourceFromPri|{Package.Location}|Load pri failed {source} with HResult {hResult} and location {Package.Location}", e); 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}"; } return $"{prefix}//{packageName}{key}"; } internal string LogoUriFromManifest(AppxPackageHelper.IAppxManifestApplication app) { var logoKeyFromVersion = new Dictionary { { 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)); } return logoPath; string TryToFindLogo(string uri, string path) { var extension = Path.GetExtension(path); if (extension != null) { //if (File.Exists(path)) //{ // return path; // shortcut, avoid enumerating files //} 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)) { // 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()); return string.Empty; } var files = Directory.EnumerateFiles(logoDir); // 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) { 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) { // try to find the closest to 44x44 logo closest = logo; if (pixelCountDiff == 0) break; // found 44x44 min = pixelCountDiff; } } selected = closest; if (!string.IsNullOrEmpty(selected)) { return selected; } 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()); return string.Empty; } } else { ProgramLogger.LogException($"|UWP|LogoPathFromUri|{Package.Location}" + $"|Unable to find extension from {uri} for {UserModelId} " + $"in package location {Package.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; } } public override string ToString() { return $"{DisplayName}: {Description}"; } public override bool Equals(object obj) { if (obj is Application other) { return UniqueIdentifier == other.UniqueIdentifier; } else { return false; } } public override int GetHashCode() { return UniqueIdentifier.GetHashCode(); } } public enum PackageVersion { Windows10, Windows81, 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); } }