diff --git a/Src/StartMenu/StartMenuDLL/ItemManager.cpp b/Src/StartMenu/StartMenuDLL/ItemManager.cpp index 27b2d17ed..c4b9734e1 100644 --- a/Src/StartMenu/StartMenuDLL/ItemManager.cpp +++ b/Src/StartMenu/StartMenuDLL/ItemManager.cpp @@ -1816,6 +1816,56 @@ CString GetPropertyStoreString( IPropertyStore *pStore, REFPROPERTYKEY key ) return res; } +// Name: System.AppUserModel.BestShortcut +// Type: Buffer -- VT_VECTOR | VT_UI1 (an ID list) +// FormatID: {9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3}, 10 +static const PROPERTYKEY PKEY_AppUserModel_BestShortcut={{0x9F4C2855, 0x9F79, 0x4B39, {0xA8, 0xD0, 0xE1, 0xD4, 0x2D, 0xE1, 0xD5, 0xF3}}, 10}; + +// Checks that the buffer holds a whole ID list, so it can be used as one +static bool IsValidIDList( const BYTE *data, ULONG size ) +{ + for (ULONG pos=0;pos+sizeof(USHORT)<=size;) + { + USHORT cb=*(const USHORT*)(data+pos); + if (cb==0) return true; + pos+=cb; + } + return false; +} + +// When the Start Menu has a shortcut to a Metro app, the Apps folder lists the shortcut in place of the app. Its item then has the +// shortcut's properties (no package, PKEY_MetroAppLauncher=0) and names the shortcut in PKEY_AppUserModel_BestShortcut. +// Returns the app the shortcut points to, with its property store, the same way a link to a Metro app is resolved +static bool GetMetroShortcutTarget( IPropertyStore *pStore, CComPtr &target, CComPtr &targetStore ) +{ + CComPtr pShortcut; + PROPVARIANT val; + PropVariantInit(&val); + if (SUCCEEDED(pStore->GetValue(PKEY_AppUserModel_BestShortcut,&val)) && val.vt==(VT_VECTOR|VT_UI1) && IsValidIDList(val.caub.pElems,val.caub.cElems)) + SHCreateItemFromIDList((PCIDLIST_ABSOLUTE)val.caub.pElems,IID_PPV_ARGS(&pShortcut)); + PropVariantClear(&val); + if (!pShortcut) return false; + + CComPtr pLink; + CAbsolutePidl targetPidl; + CComPtr pTarget; + CComPtr pTargetStore; + if (FAILED(pShortcut->BindToHandler(NULL,BHID_SFUIObject,IID_PPV_ARGS(&pLink))) || FAILED(pLink->GetIDList(&targetPidl)) || + FAILED(SHCreateItemFromIDList(targetPidl,IID_PPV_ARGS(&pTarget))) || FAILED(pTarget->BindToHandler(NULL,BHID_PropertyStore,IID_PPV_ARGS(&pTargetStore)))) + return false; + + // only a shortcut to the same app, and only if it is a Metro app + if (_wcsicmp(GetPropertyStoreString(pStore,PKEY_AppUserModel_ID),GetPropertyStoreString(pTargetStore,PKEY_AppUserModel_ID))!=0) + return false; + bool bMetro=SUCCEEDED(pTargetStore->GetValue(PKEY_MetroAppLauncher,&val)) && (val.vt==VT_I4 || val.vt==VT_UI4) && val.intVal; + PropVariantClear(&val); + if (!bMetro) return false; + + target=std::move(pTarget); + targetStore=std::move(pTargetStore); + return true; +} + // doesn't require the lock to be held void CItemManager::RefreshItemInfo( ItemInfo *pInfo, int refreshFlags, IShellItem *pItem0, bool bHasWriteLock ) { @@ -1935,6 +1985,15 @@ void CItemManager::RefreshItemInfo( ItemInfo *pInfo, int refreshFlags, IShellIte { newInfo.bLink=false; pItem->BindToHandler(NULL,BHID_PropertyStore,IID_IPropertyStore,(void**)&pStore); + // an Apps folder item that stands for a shortcut to a Metro app: use the app, or it would be taken for a desktop program + // without an appid (wrong icon, and not recognized as a pinned program) + CComPtr target; + CComPtr store; + if (pStore && GetMetroShortcutTarget(pStore,target,store)) + { + pItem=std::move(target); + pStore=store; + } } if (pStore) { @@ -1957,7 +2016,10 @@ void CItemManager::RefreshItemInfo( ItemInfo *pInfo, int refreshFlags, IShellIte if (newInfo.bMetroApp) { pAppItem=pItem; - newInfo.packagePath=GetPropertyStoreString(pStore,PKEY_MetroPackagePath); + // the package installed now, not the one named by the item of a shortcut, which is gone after an update + newInfo.packagePath=GetPackageInstallPath(GetPackageFullName(newInfo.appid)); + if (newInfo.packagePath.IsEmpty()) + newInfo.packagePath=GetPropertyStoreString(pStore,PKEY_MetroPackagePath); if (!newInfo.packagePath.IsEmpty()) { FILETIME writeTime={0}, createTime={0}; @@ -2473,6 +2535,18 @@ static bool SetResContextTargetSize( IResourceContext *pResContext, int size ) void CItemManager::LoadMetroIcon( IShellItem *pItem, int &refreshFlags, const IconInfo *&smallIcon, const IconInfo *&largeIcon, const IconInfo *&extraLargeIcon, const DWORD *pMetroColor ) { + CComPtr pStore; + pItem->BindToHandler(NULL,BHID_PropertyStore,IID_IPropertyStore,(void**)&pStore); + if (!pStore) return; + + // the package installed now. The item of a shortcut names the package the shortcut was created with, which is gone after an update + CString appid=GetPropertyStoreString(pStore,PKEY_AppUserModel_ID); + CString savedPackageName=GetPropertyStoreString(pStore,PKEY_MetroPackageName); + CString packageName=GetPackageFullName(appid); + if (packageName.IsEmpty()) + packageName=savedPackageName; + if (packageName.IsEmpty()) return; + unsigned int hash; { CComString pName; @@ -2481,6 +2555,7 @@ void CItemManager::LoadMetroIcon( IShellItem *pItem, int &refreshFlags, const Ic } hash=CalcFNVHash(pMetroColor,4,hash); + hash=CalcFNVHash(packageName,hash); // a new version of the app can come with a new icon FindInCache(hash,refreshFlags,smallIcon,largeIcon,extraLargeIcon); if (!refreshFlags) return; @@ -2488,13 +2563,13 @@ void CItemManager::LoadMetroIcon( IShellItem *pItem, int &refreshFlags, const Ic // extract icon HBITMAP hSmallBitmap=NULL, hLargeBitmap=NULL, hExtraLargeBitmap=NULL; - CComPtr pStore; - pItem->BindToHandler(NULL,BHID_PropertyStore,IID_IPropertyStore,(void**)&pStore); - if (!pStore) return; - - CString packageName=GetPropertyStoreString(pStore,PKEY_MetroPackageName); - if (packageName.IsEmpty()) return; - CString iconName=GetPropertyStoreString(pStore,PKEY_MetroIcon); + // the logo named by the item belongs to the package the item names. When that is another version, or the item is the Apps folder's + // stand-in for a shortcut to the app (no package, no logo), take the logo from the manifest of the package installed now + CString iconName; + if (_wcsicmp(savedPackageName,packageName)==0) + iconName=GetPropertyStoreString(pStore,PKEY_MetroIcon); + if (iconName.IsEmpty()) + iconName=GetAppLogoName(appid); if (iconName.IsEmpty()) return; CComPtr pResManager; diff --git a/Src/StartMenu/StartMenuDLL/MetroLinkManager.cpp b/Src/StartMenu/StartMenuDLL/MetroLinkManager.cpp index 67406b678..102616448 100644 --- a/Src/StartMenu/StartMenuDLL/MetroLinkManager.cpp +++ b/Src/StartMenu/StartMenuDLL/MetroLinkManager.cpp @@ -10,7 +10,11 @@ #include "ResourceHelper.h" #include "Translations.h" #include +#include +#include +#include #include +#include PROPERTYKEY PKEY_MetroAppLink={{0x9F4C2855, 0x9F79, 0x4B39, {0xA8, 0xD0, 0xE1, 0xD4, 0x2D, 0xE1, 0xD5, 0xF3}}, 10}; // pidl PROPERTYKEY PKEY_MetroAppLauncher={{0x9F4C2855, 0x9F79, 0x4B39, {0xA8, 0xD0, 0xE1, 0xD4, 0x2D, 0xE1, 0xD5, 0xF3}}, 14}; // =1 for metro apps @@ -373,8 +377,127 @@ bool IsEdgeDefaultBrowser( void ) return false; } +// Splits an appid (PackageFamilyName!AppName) into its two parts. Returns false if it is not one +static bool SplitAppId(const wchar_t* appId, CString& family, CString& appName) +{ + const wchar_t* bang = appId ? wcschr(appId, '!') : nullptr; + if (!bang) + return false; + family.SetString(appId, (int)(bang - appId)); + appName = bang + 1; + return true; +} + +// A package family can also list a bundle and resource packages; the app is in the main package, the one +// without a resource id. A full name is Name_Version_Architecture_ResourceId_PublisherId, and no part contains '_' +static bool IsMainPackage(const wchar_t* fullName) +{ + const wchar_t* resourceId = fullName; + for (int part = 0; part < 3 && resourceId; part++) + { + resourceId = wcschr(resourceId, '_'); + if (resourceId) resourceId++; + } + return resourceId && *resourceId == '_'; +} + +// The full name of the app's package as installed now, found from the package family name in the appid. +// The full name contains the version, so it changes with every update, while the appid does not. A shortcut to a +// Metro app keeps the full name of the package it was created with, which is gone after an update. And with such a +// shortcut in the Start Menu, the Apps folder item for the appid is the shortcut, without a package. +// Returns an empty string if the app is not installed, or before Windows 8 +static CString GetInstalledPackageFullName(const wchar_t* appId) +{ + static auto pGetPackagesByPackageFamily = static_cast((void*)GetProcAddress(GetModuleHandle(L"kernel32.dll"), "GetPackagesByPackageFamily")); + CString family, appName; + if (!pGetPackagesByPackageFamily || !SplitAppId(appId, family, appName)) + return {}; + + UINT32 count = 0, bufferLength = 0; + if (pGetPackagesByPackageFamily(family, &count, nullptr, &bufferLength, nullptr) != ERROR_INSUFFICIENT_BUFFER || count == 0) + return {}; + std::vector names(count); + std::vector buffer(bufferLength); + if (pGetPackagesByPackageFamily(family, &count, names.data(), &bufferLength, buffer.data()) != ERROR_SUCCESS) + return {}; + + for (PWSTR name : names) + { + if (IsMainPackage(name)) + return name; + } + return {}; +} + +// Returns the folder where the package is installed, or an empty string +CString GetPackageInstallPath(const wchar_t* packageFullName) +{ + static auto pGetPackagePathByFullName = static_cast((void*)GetProcAddress(GetModuleHandle(L"kernel32.dll"), "GetPackagePathByFullName")); + if (!pGetPackagePathByFullName || !packageFullName || !*packageFullName) + return {}; + wchar_t path[_MAX_PATH]; + UINT32 length = _countof(path); + if (pGetPackagePathByFullName(packageFullName, &length, path) != ERROR_SUCCESS) + return {}; + return path; +} + +// Opens the list of applications in the manifest of an installed package. Returns null if the manifest can't be read +static CComPtr ReadManifestApplications(const wchar_t* packagePath) +{ + CString path = packagePath; + path += L"\\AppxManifest.xml"; + CComPtr factory; + CComPtr stream; + CComPtr reader; + CComPtr apps; + if (FAILED(factory.CoCreateInstance(__uuidof(AppxFactory))) || FAILED(SHCreateStreamOnFileEx(path, STGM_READ | STGM_SHARE_DENY_NONE, 0, FALSE, nullptr, &stream)) || + FAILED(factory->CreateManifestReader(stream, &reader)) || FAILED(reader->GetApplications(&apps))) + return nullptr; + return apps; +} + +// The small logo of a manifest application: Square44x44Logo, or SmallLogo in Windows 8 manifests +static CString GetManifestLogo(IAppxManifestApplication* app) +{ + for (const wchar_t* attribute : {L"Square44x44Logo", L"SmallLogo"}) + { + CComString logo; + if (SUCCEEDED(app->GetStringValue(attribute, &logo)) && logo && *logo) + return (const wchar_t*)logo; + } + return {}; +} + +// Returns the small logo of the app (a resource name like Assets\AppList.png), from the manifest of the package +// installed now, or an empty string +CString GetAppLogoName(const wchar_t* appId) +{ + CString family, appName; + if (!SplitAppId(appId, family, appName)) + return {}; + CString packagePath = GetPackageInstallPath(GetInstalledPackageFullName(appId)); + CComPtr apps = packagePath.IsEmpty() ? nullptr : ReadManifestApplications(packagePath); + if (!apps) + return {}; + + BOOL bHasCurrent = FALSE; + for (; SUCCEEDED(apps->GetHasCurrent(&bHasCurrent)) && bHasCurrent; apps->MoveNext(&bHasCurrent)) + { + CComPtr app; + CComString id; + if (SUCCEEDED(apps->GetCurrent(&app)) && SUCCEEDED(app->GetStringValue(L"Id", &id)) && _wcsicmp(id, appName) == 0) + return GetManifestLogo(app); + } + return {}; +} + CString GetPackageFullName(const wchar_t* appId) { + CString packageName = GetInstalledPackageFullName(appId); + if (!packageName.IsEmpty()) + return packageName; + CComPtr item; if (SUCCEEDED(SHCreateItemInKnownFolder(FOLDERID_AppsFolder, 0, appId, IID_PPV_ARGS(&item)))) { diff --git a/Src/StartMenu/StartMenuDLL/MetroLinkManager.h b/Src/StartMenu/StartMenuDLL/MetroLinkManager.h index 83b41886f..f0b8d962f 100644 --- a/Src/StartMenu/StartMenuDLL/MetroLinkManager.h +++ b/Src/StartMenu/StartMenuDLL/MetroLinkManager.h @@ -54,5 +54,11 @@ CComPtr GetMetroPinMenu( const wchar_t *appid ); // Determines if Edge is the default browser bool IsEdgeDefaultBrowser( void ); -// Returns full package name for given App ID +// Returns full package name for given App ID, of the package installed now CString GetPackageFullName(const wchar_t* appId); + +// Returns the folder where the package is installed +CString GetPackageInstallPath(const wchar_t* packageFullName); + +// Returns the small logo of the app, from the manifest of the package installed now +CString GetAppLogoName(const wchar_t* appId); diff --git a/Src/StartMenu/StartMenuDLL/SearchManager.cpp b/Src/StartMenu/StartMenuDLL/SearchManager.cpp index 037794941..c2ca36e34 100644 --- a/Src/StartMenu/StartMenuDLL/SearchManager.cpp +++ b/Src/StartMenu/StartMenuDLL/SearchManager.cpp @@ -692,7 +692,10 @@ void CSearchManager::SearchThread( void ) { CComString pName; if (SUCCEEDED(it->pItem->GetDisplayName(SIGDN_NORMALDISPLAY,&pName))) - AddSearchItem(it->pItem,pName,COLLECT_PROGRAMS|COLLECT_METRO,CATEGORY_PROGRAM,searchRequest); + { + pName.MakeUpper(); // like the names from the folders, so GetSearchResults can tell a Start Menu shortcut to the same app is a duplicate + AddSearchItem(it->pItem,pName,COLLECT_PROGRAMS|COLLECT_METRO,CATEGORY_PROGRAM,searchRequest); + } } if (searchRequest.requestId