rewrite uwp by not using GetAppListEntriesAsync,

because
1. logo is not squarelogo44x44
2. can't get details info (background etc)
This commit is contained in:
bao-qian
2016-11-29 01:18:38 +00:00
parent 8533f6edf1
commit 8a19d25396
2 changed files with 312 additions and 270 deletions

View File

@@ -5,13 +5,13 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Security.Principal; using System.Security.Principal;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Media; using System.Windows.Media;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
using System.Xml.Linq;
using Windows.ApplicationModel; using Windows.ApplicationModel;
using Windows.ApplicationModel.Core;
using Windows.Foundation;
using Windows.Management.Deployment; using Windows.Management.Deployment;
using Windows.Storage.Streams;
using AppxPackaing; using AppxPackaing;
using Shell; using Shell;
using Wox.Infrastructure; using Wox.Infrastructure;
@@ -26,29 +26,38 @@ namespace Wox.Plugin.Program.Programs
public string Name { get; } public string Name { get; }
public string FullName { get; } public string FullName { get; }
public string FamilyName { 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 string Location { get; set; }
public Application[] Apps { get; set; } public Application[] Apps { get; set; }
public Package Package { get; } public Package Package { get; }
public PackageVersion Version { get; set; }
public UWP(Package package) public UWP(Package package)
{ {
Package = package; Package = package;
Location = Package.InstalledLocation.Path;
Name = Package.Id.Name; Name = Package.Id.Name;
FullName = Package.Id.FullName; FullName = Package.Id.FullName;
FamilyName = Package.Id.FamilyName; FamilyName = Package.Id.FamilyName;
Location = Package.InstalledLocation.Path; InitializeAppInfo();
Apps = MergedApps(); 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 path = Path.Combine(Location, "AppxManifest.xml");
var appx = new AppxFactory();
var namespaces = XmlNamespaces(path);
InitPackageVersion(namespaces);
var appxFactory = new AppxFactory();
IStream stream; IStream stream;
const uint noAttribute = 0x80; const uint noAttribute = 0x80;
const Stgm exclusiveRead = Stgm.Read | Stgm.ShareExclusive; const Stgm exclusiveRead = Stgm.Read | Stgm.ShareExclusive;
@@ -56,129 +65,249 @@ namespace Wox.Plugin.Program.Programs
if (result == Hresult.Ok) if (result == Hresult.Ok)
{ {
var reader = appx.CreateManifestReader(stream); var reader = appxFactory.CreateManifestReader(stream);
var manifestApps = reader.GetApplications();
var properties = reader.GetProperties(); var apps = new List<Application>();
PublisherDisplayName = properties.GetStringValue("PublisherDisplayName"); while (manifestApps.GetHasCurrent() != 0)
DisplayName = properties.GetStringValue("DisplayName");
Description = properties.GetStringValue("Description");
var apps = reader.GetApplications();
var parsedApps = new List<Application>();
while (apps.GetHasCurrent() != 0)
{ {
var current = apps.GetCurrent(); var manifestApp = manifestApps.GetCurrent();
var appListEntry = current.GetStringValue("AppListEntry"); var appListEntry = manifestApp.GetStringValue("AppListEntry");
if (appListEntry != "none") if (appListEntry != "none")
{ {
var app = new Application var app = new Application
{ {
UserModelId = current.GetAppUserModelId(), UserModelId = manifestApp.GetAppUserModelId(),
BackgroundColor = current.GetStringValue("BackgroundColor") ?? string.Empty, DisplayName = manifestApp.GetStringValue("DisplayName"),
Location = Location, Description = manifestApp.GetStringValue("Description"),
LogoPath = Application.LogoFromManifest(current, Location), BackgroundColor = manifestApp.GetStringValue("BackgroundColor"),
Valid = true // useless for now Location = Location
}; };
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);
}
manifestApps.MoveNext();
}
Apps = apps.Where(a => a.AppListEntry != "none").ToArray();
}
}
if (!string.IsNullOrEmpty(app.UserModelId)) /// http://www.hanselman.com/blog/GetNamespacesFromAnXMLDocumentWithXPathDocumentAndLINQToXML.aspx
private static string[] XmlNamespaces(string path)
{ {
parsedApps.Add(app); XDocument z = XDocument.Load(path);
} if (z.Root != null)
} {
apps.MoveNext(); var namespaces = z.Root.Attributes().
} Where(a => a.IsNamespaceDeclaration).
GroupBy(
return parsedApps.ToArray(); 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 else
{ {
return new Application[] { }; Log.Error($"can't find namespaces for <{path}>");
return new string[] {};
} }
} }
private Application[] AppDisplayInfos() private void InitPackageVersion(string[] namespaces)
{ {
IReadOnlyList<AppListEntry> apps; var versionFromNamespace = new Dictionary<string, PackageVersion>
try
{ {
apps = Package.GetAppListEntriesAsync().AsTask().Result; {"http://schemas.microsoft.com/appx/manifest/foundation/windows10", PackageVersion.Windows10},
} {"http://schemas.microsoft.com/appx/2013/manifest", PackageVersion.Windows81},
catch (Exception e) {"http://schemas.microsoft.com/appx/2010/manifest", PackageVersion.Windows8},
{
var message = $"{e.Message} @ {Name}";
Console.WriteLine(message);
return new Application[] { };
}
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; foreach (var n in versionFromNamespace.Keys)
{
if (namespaces.Contains(n))
{
Version = versionFromNamespace[n];
return;
}
} }
private Application[] MergedApps() Log.Error($"Unknown Appmanifest version: {FullName}");
{ Version = PackageVersion.Unknown;
// todo can't find api, so just hard code it
if (Location.Contains("SystemApps") || Location.Contains("WindowsApps"))
{
// we must parse AppInfo first, because we want to make sure AppListEntry != "none"
var infos = AppInfos();
if (infos.Length > 0)
{
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++)
{
var j = length - i - 1;
apps[i].DisplayName = displayInfos[j].DisplayName;
apps[i].Description = displayInfos[j].Description;
apps[i].LogoStream = displayInfos[j].LogoStream;
} }
return apps;
private string LogoUriFromManifest(IAppxManifestApplication app)
{
var logoKeyFromVersion = new Dictionary<PackageVersion, string>
{
{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;
} }
} }
return new Application[] { };
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<string> {path};
// todo hidpi icon
if (Version == PackageVersion.Windows10)
{
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))
{
return loaded;
}
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;
}
}
else
{
return resourceReference;
}
} }
public static Application[] All() public static Application[] All()
{ {
var windows10 = new Version(10, 0); var windows10 = new Version(10, 0);
var support = Environment.OSVersion.Version.Major >= windows10.Major; var support = Environment.OSVersion.Version.Major >= windows10.Major;
if (support) if (support)
{ {
var applications = CurrentUserPackages().AsParallel().SelectMany(p => new UWP(p).Apps); var watch = new System.Diagnostics.Stopwatch();
applications = applications.Where(a => a.Valid); watch.Start();
return applications.ToArray();
var applications = CurrentUserPackages().AsParallel().SelectMany(p => new UWP(p).Apps).ToArray();
watch.Stop();
Log.Info("UWP ALL" + watch.ElapsedMilliseconds);
return applications;
} }
else else
{ {
return new Application[] {}; return new Application[] {};
} }
} }
private static IEnumerable<Package> CurrentUserPackages() private static IEnumerable<Package> CurrentUserPackages()
@@ -190,7 +319,9 @@ namespace Wox.Plugin.Program.Programs
var userSecurityId = user.Value; var userSecurityId = user.Value;
var packageManager = new PackageManager(); var packageManager = new PackageManager();
var packages = packageManager.FindPackagesForUser(userSecurityId); 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; return packages;
} }
else else
@@ -225,16 +356,15 @@ namespace Wox.Plugin.Program.Programs
public class Application : IProgram public class Application : IProgram
{ {
public string AppListEntry { get; set; }
public string DisplayName { get; set; } public string DisplayName { get; set; }
public string Description { get; set; } public string Description { get; set; }
public RandomAccessStreamReference LogoStream { get; set; }
public string UserModelId { get; set; } public string UserModelId { get; set; }
public string PublisherDisplayName { get; set; }
public string BackgroundColor { 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 string Location { get; set; }
public bool Valid { get; set; }
private int Score(string query) private int Score(string query)
{ {
@@ -294,14 +424,16 @@ namespace Wox.Plugin.Program.Programs
return contextMenus; return contextMenus;
} }
private void Launch(IPublicAPI api) private async void Launch(IPublicAPI api)
{
try
{ {
var appManager = new ApplicationActivationManager(); var appManager = new ApplicationActivationManager();
uint unusedPid; uint unusedPid;
const string noArgs = ""; const string noArgs = "";
const ACTIVATEOPTIONS noFlags = ACTIVATEOPTIONS.AO_NONE; const ACTIVATEOPTIONS noFlags = ACTIVATEOPTIONS.AO_NONE;
await Task.Run(() =>
{
try
{
appManager.ActivateApplication(UserModelId, noArgs, noFlags, out unusedPid); appManager.ActivateApplication(UserModelId, noArgs, noFlags, out unusedPid);
} }
catch (Exception) catch (Exception)
@@ -310,149 +442,38 @@ namespace Wox.Plugin.Program.Programs
var message = $"Can't start UWP: {DisplayName}"; var message = $"Can't start UWP: {DisplayName}";
api.ShowMsg(name, message, string.Empty); api.ShowMsg(name, message, string.Empty);
} }
});
} }
public ImageSource Logo() public ImageSource Logo()
{ {
var logo = !string.IsNullOrEmpty(LogoPath) ? ImageFromPath(LogoPath) : ImageFromStream(LogoStream); var logo = ImageFromPath(LogoPath);
var validBaground = !string.IsNullOrEmpty(BackgroundColor) && BackgroundColor != "transparent"; var plated = PlatedImage(logo);
var plated = validBaground ? PlatedImage(logo) : logo;
// todo magic! temp fix for cross thread object // todo magic! temp fix for cross thread object
plated.Freeze(); plated.Freeze();
return plated; 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<string>();
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) 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<string>();
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)); var image = new BitmapImage(new Uri(path));
return image; return image;
} }
} else
private BitmapImage ImageFromStream(RandomAccessStreamReference reference)
{ {
IRandomAccessStreamWithContentType stream; Log.Error($"Can't get logo for <{UserModelId}> with path <{path}>");
try
{
stream = reference.OpenReadAsync().AsTask().Result;
}
catch (Exception e)
{
var message = $"{e.Message} @ {DisplayName}";
Log.Error(message);
Log.Exception(e);
return new BitmapImage(new Uri(Constant.ErrorIcon)); 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) private ImageSource PlatedImage(BitmapImage image)
{
if (!string.IsNullOrEmpty(BackgroundColor) && BackgroundColor != "transparent")
{ {
var width = image.Width; var width = image.Width;
var height = image.Height; var height = image.Height;
@@ -492,9 +513,16 @@ namespace Wox.Plugin.Program.Programs
} }
else else
{ {
Log.Error($"Can't convert background string <{BackgroundColor}> to color");
return new BitmapImage(new Uri(Constant.ErrorIcon)); return new BitmapImage(new Uri(Constant.ErrorIcon));
} }
} }
else
{
// todo use windows theme as background
return image;
}
}
public override string ToString() public override string ToString()
{ {
@@ -502,19 +530,32 @@ namespace Wox.Plugin.Program.Programs
} }
} }
public enum PackageVersion
{
Windows10,
Windows81,
Windows8,
Unknown
}
[Flags] [Flags]
private enum Stgm : uint private enum Stgm : uint
{ {
Read = 0x0, Read = 0x0,
ShareExclusive = 0x10 ShareExclusive = 0x10,
} }
private enum Hresult : uint private enum Hresult : uint
{ {
Ok = 0x0000 Ok = 0x0000,
} }
[DllImport("shlwapi.dll", CharSet = CharSet.Unicode)] [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);
} }
} }

View File

@@ -60,6 +60,7 @@
<Reference Include="System.Windows.Forms" /> <Reference Include="System.Windows.Forms" />
<Reference Include="System.Xaml" /> <Reference Include="System.Xaml" />
<Reference Include="System.Xml" /> <Reference Include="System.Xml" />
<Reference Include="System.Xml.Linq" />
<Reference Include="WindowsBase" /> <Reference Include="WindowsBase" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>