Bug 1529276 - Create a new JumpListBuilder implementation that is simpler and doesn't block the main thread. r=mhowell,win-reviewers

Differential Revision: https://phabricator.services.mozilla.com/D181675
This commit is contained in:
Mike Conley 2023-11-22 21:28:56 +00:00
Родитель 7226d25048
Коммит 481dde8e1f
9 изменённых файлов: 1099 добавлений и 3 удалений

Просмотреть файл

@ -0,0 +1,49 @@
/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/**
* A WindowsJumpListShortcutDescription is a structure that describes an entry
* to be created in the Windows Jump List. Both tasks, as well as custom
* items can be described using this structure.
*
* nsIJumpListBuilder.populateJumpList accepts arrays of these structures.
*/
[GenerateInit, GenerateConversionToJS]
dictionary WindowsJumpListShortcutDescription {
/**
* The title of the Jump List item to be displayed to the user.
*/
required DOMString title;
/**
* The path to the executable that Windows should run when the item is
* selected in the Jump List.
*/
required DOMString path;
/**
* Arguments to be supplied to the executable when the item is selected in
* the Jump List.
*/
DOMString arguments;
/**
* A description of the item that is displayed as a tooltip.
*/
required DOMString description;
/**
* The path to an icon to assign to the Jump List item. If this is not
* supplied then the fallbackIconIndex is used instead.
*/
DOMString iconPath;
/**
* The icon index associated with the executable at the path to use in the
* event that no iconPath is supplied.
*/
required long fallbackIconIndex;
};

Просмотреть файл

@ -111,3 +111,8 @@ if CONFIG["MOZ_PLACES"]:
"PlacesEvent.webidl",
"PlacesObservers.webidl",
]
if CONFIG["MOZ_WIDGET_TOOLKIT"] == "windows":
WEBIDL_FILES += [
"WindowsJumpListShortcutDescription.webidl",
]

Просмотреть файл

@ -79,6 +79,7 @@ elif toolkit == "gtk":
]
elif toolkit == "windows":
XPIDL_SOURCES += [
"nsIJumpListBuilder.idl",
"nsILegacyJumpListBuilder.idl",
"nsILegacyJumpListItem.idl",
"nsIPrintSettingsWin.idl",

Просмотреть файл

@ -0,0 +1,119 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "nsISupports.idl"
interface nsIURI;
[scriptable, uuid(5769F08D-0303-4E38-8FE6-86B5473022F6)]
interface nsIJumpListBuilder : nsISupports
{
/**
* Returns the local filesystem path for a favicon for a page hosted at
* faviconURL if we happen to have written one to disk before. If we have not,
* then a background thread retrieves the favicon and will write it to disk
* and NS_ERROR_NOT_AVAILABLE will be thrown.
*
* @param {nsIURI} faviconURL
* The URL for the web page for which we would like a filesystem path for
* the favicon.
* @returns {AString}
* The local filesystem path for the favicon if it has been cached before.
* If it has not been cached before, this method will throw
* NS_ERROR_NOT_AVAILABLE.
* @throws NS_ERROR_NOT_AVAILABLE
* In the event that the favicon has never been cached to disk before.
*/
AString obtainAndCacheFavicon(in nsIURI faviconURL);
/**
* Returns a Promise that resolves with whether or not the Jump List backend
* on the background thread is up and running.
*
* @returns {Promise<boolean>}
* Resolves to true if the backend is ready to accept
* WindowsJumpListShortcutDescriptions. False, otherwise.
* @throws NS_ERROR_FAILURE
* If an attempt to communicate with the background thread fails.
*/
[implicit_jscontext]
Promise isAvailable();
/**
* Asks the Windows Jump List API for any items that might have been removed
* by the user from the Jump List UI.
*
* Important: This should be called prior to any attempt to call
* `populateJumpList` to ensure that any passed in
* WindowsJumpListShortcutDescriptions do not describe items that the user has
* just removed. Failing to do so will cause the Promise returned from
* `populateJumpList` to reject. This is a constraint of the underlying win32
* API. Please see
* https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-icustomdestinationlist-beginlist
* for more details.
*
* @returns {Promise<string[], nsresult>}
* On success, will return an array of strings for URLs of history that
* have been removed by the user via the Windows Jump List. These items will
* also have had their cached favicons removed from the disk off of the
* main thread. On failure, this will reject with the nsresult failure code.
* @throws NS_ERROR_FAILURE
* If an attempt to communicate with the background thread fails.
*/
[implicit_jscontext]
Promise checkForRemovals();
/**
* Writes a new set of items to the Windows Jump List. This occurs
* asynchronously, off of the main thread.
*
* Important: Callers should first call `checkForRemovals` to remove any
* browsing history items that the user chose to remove in the Jump List
* Only then should any WindowsJumpListShortcutDescriptions be created
* and passed to this method. Any attempt to add
* WindowsJumpListShortcutDescriptions matching items that have been removed
* by the user will result in the returned Promise rejecting. This is a
* constraint of the underlying win32 API. Please see
* https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-icustomdestinationlist-beginlist
* for more details.
*
* @param {WindowsJumpListShortcutDescription[]} aTaskDescriptions
* 0 or more WindowsJumpListShortcutDescriptions to place items within the
* "tasks" section of the Jump List.
* @param {AString} aCustomTitle
* An optional title for a custom sub-list within the Jump List that will be
* populated via aCustomDescriptions. This must be supplied if
* aCustomDescriptions is not empty.
* @param {WindowsJumpListShortcutDescription[]} aCustomDescriptions
* 0 or more WindowsJumpListShortcutDescriptions to place items within the
* custom section of the Jump List. aCustomTitle must be supplied if this
* array is non-empty.
* @returns {Promise<undefined, nsresult>}
* Returns a Promise that resolves if the Jump List was properly written
* to, and rejects otherwise with the nsresult of the failure.
* @throws NS_ERROR_INVALID_ARG
* If any of the passed arguments do not meet the requirements set out
* above.
* @throws NS_ERROR_FAILURE
* If an attempt to communicate with the background thread fails.
*/
[implicit_jscontext]
Promise populateJumpList(
in Array<jsval> aTaskDescriptions,
in AString aCustomTitle,
in Array<jsval> aCustomDescriptions
);
/**
* Removes all items from the Jump List.
*
* @returns {Promise<undefined, nsresult>}
* Resolves with undefined on successfully clearing the Jump List. If it
* fails to do so, it will reject with the failure code.
* @throws NS_ERROR_FAILURE
* If an attempt to communicate with the background thread fails.
*/
[implicit_jscontext]
Promise clearJumpList();
};

Просмотреть файл

@ -9,6 +9,7 @@
#include "nsIBaseWindow.idl"
interface nsIDocShell;
interface nsIJumpListBuilder;
interface nsITaskbarTabPreview;
interface nsITaskbarWindowPreview;
interface nsITaskbarPreviewController;
@ -132,6 +133,14 @@ interface nsIWinTaskbar : nsISupports
*/
nsILegacyJumpListBuilder createLegacyJumpListBuilder(in boolean aPrivateBrowsing);
/**
* Retrieves a Windows Jump List builder. This jump list builder can be used
* to asynchronously add, remove, and update items in the Windows Jump List.
*
* @throws NS_ERROR_UNEXPECTED if the builder failed to be created.
*/
nsIJumpListBuilder createJumpListBuilder(in boolean aPrivateBrowsing);
/**
* Application window taskbar group settings
*/

Просмотреть файл

@ -0,0 +1,793 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include <propkey.h>
#include <propvarutil.h>
#include <shellapi.h>
#include "JumpListBuilder.h"
#include "mozilla/Preferences.h"
#include "mozilla/dom/Promise.h"
#include "mozilla/dom/WindowsJumpListShortcutDescriptionBinding.h"
#include "mozilla/mscom/EnsureMTA.h"
#include "nsIObserverService.h"
using mozilla::dom::Promise;
using mozilla::dom::WindowsJumpListShortcutDescription;
namespace mozilla {
namespace widget {
NS_IMPL_ISUPPORTS(JumpListBuilder, nsIJumpListBuilder, nsIObserver)
#define TOPIC_PROFILE_BEFORE_CHANGE "profile-before-change"
#define TOPIC_CLEAR_PRIVATE_DATA "clear-private-data"
// The amount of time, in milliseconds, that our IO thread will stay alive after
// the last event it processes.
#define DEFAULT_THREAD_TIMEOUT_MS 30000
const char kPrefTaskbarEnabled[] = "browser.taskbar.lists.enabled";
/**
* A wrapper around a ICustomDestinationList that implements the JumpListBackend
* interface. This is an implementation of JumpListBackend that actually causes
* items to appear in a Windows jump list.
*/
class NativeJumpListBackend : public JumpListBackend {
// We use NS_INLINE_DECL_REFCOUNTING_ONEVENTTARGET because this
// class might be destroyed on a different thread than the one it
// was created on, since it's maintained by a LazyIdleThread.
//
// This is a workaround for bug 1648031.
NS_INLINE_DECL_REFCOUNTING_ONEVENTTARGET(JumpListBackend, override)
NativeJumpListBackend() {
MOZ_ASSERT(!NS_IsMainThread());
mscom::EnsureMTA([&]() {
RefPtr<ICustomDestinationList> destList;
HRESULT hr = ::CoCreateInstance(
CLSID_DestinationList, nullptr, CLSCTX_INPROC_SERVER,
IID_ICustomDestinationList, getter_AddRefs(destList));
if (FAILED(hr)) {
return;
}
mWindowsDestList = destList;
});
}
virtual bool IsAvailable() override {
MOZ_ASSERT(!NS_IsMainThread());
return mWindowsDestList != nullptr;
}
virtual HRESULT SetAppID(LPCWSTR pszAppID) override {
MOZ_ASSERT(!NS_IsMainThread());
MOZ_ASSERT(mWindowsDestList);
return mWindowsDestList->SetAppID(pszAppID);
}
virtual HRESULT BeginList(UINT* pcMinSlots, REFIID riid,
void** ppv) override {
MOZ_ASSERT(!NS_IsMainThread());
MOZ_ASSERT(mWindowsDestList);
return mWindowsDestList->BeginList(pcMinSlots, riid, ppv);
}
virtual HRESULT AddUserTasks(IObjectArray* poa) override {
MOZ_ASSERT(!NS_IsMainThread());
MOZ_ASSERT(mWindowsDestList);
return mWindowsDestList->AddUserTasks(poa);
}
virtual HRESULT AppendCategory(LPCWSTR pszCategory,
IObjectArray* poa) override {
MOZ_ASSERT(!NS_IsMainThread());
MOZ_ASSERT(mWindowsDestList);
return mWindowsDestList->AppendCategory(pszCategory, poa);
}
virtual HRESULT CommitList() override {
MOZ_ASSERT(!NS_IsMainThread());
MOZ_ASSERT(mWindowsDestList);
return mWindowsDestList->CommitList();
}
virtual HRESULT AbortList() override {
MOZ_ASSERT(!NS_IsMainThread());
MOZ_ASSERT(mWindowsDestList);
return mWindowsDestList->AbortList();
}
virtual HRESULT DeleteList(LPCWSTR pszAppID) override {
MOZ_ASSERT(!NS_IsMainThread());
MOZ_ASSERT(mWindowsDestList);
return mWindowsDestList->DeleteList(pszAppID);
}
virtual HRESULT AppendKnownCategory(KNOWNDESTCATEGORY category) override {
MOZ_ASSERT(!NS_IsMainThread());
MOZ_ASSERT(mWindowsDestList);
return mWindowsDestList->AppendKnownCategory(category);
}
protected:
virtual ~NativeJumpListBackend() override{};
private:
RefPtr<ICustomDestinationList> mWindowsDestList;
};
JumpListBuilder::JumpListBuilder(const nsAString& aAppUserModelId,
RefPtr<JumpListBackend> aTestingBackend) {
MOZ_ASSERT(NS_IsMainThread());
mAppUserModelId.Assign(aAppUserModelId);
Preferences::AddStrongObserver(this, kPrefTaskbarEnabled);
// Make a lazy thread for any IO.
mIOThread = new LazyIdleThread(DEFAULT_THREAD_TIMEOUT_MS, "Jump List",
LazyIdleThread::ManualShutdown);
nsCOMPtr<nsIObserverService> observerService =
do_GetService("@mozilla.org/observer-service;1");
if (observerService) {
observerService->AddObserver(this, TOPIC_PROFILE_BEFORE_CHANGE, false);
observerService->AddObserver(this, TOPIC_CLEAR_PRIVATE_DATA, false);
}
nsCOMPtr<nsIRunnable> runnable;
if (aTestingBackend) {
// Dispatch a task that hands a reference to the testing backend
// to the background thread. The testing backend was probably
// constructed on the main thread, and is responsible for doing
// any locking as well as cleanup.
runnable = NewRunnableMethod<RefPtr<JumpListBackend>>(
"SetupTestingBackend", this, &JumpListBuilder::DoSetupTestingBackend,
aTestingBackend);
} else {
// Dispatch a task that constructs the native jump list backend.
runnable = NewRunnableMethod("SetupBackend", this,
&JumpListBuilder::DoSetupBackend);
}
mIOThread->Dispatch(runnable, NS_DISPATCH_NORMAL);
// MSIX packages explicitly do not support setting the appid from within
// the app, as it is set in the package manifest instead.
if (!mozilla::widget::WinUtils::HasPackageIdentity()) {
mIOThread->Dispatch(
NewRunnableMethod<nsString>(
"SetAppID", this, &JumpListBuilder::DoSetAppID, aAppUserModelId),
NS_DISPATCH_NORMAL);
}
}
JumpListBuilder::~JumpListBuilder() {
Preferences::RemoveObserver(this, kPrefTaskbarEnabled);
}
void JumpListBuilder::DoSetupTestingBackend(
RefPtr<JumpListBackend> aTestingBackend) {
MOZ_ASSERT(!NS_IsMainThread());
mJumpListBackend = aTestingBackend;
}
void JumpListBuilder::DoSetupBackend() {
MOZ_ASSERT(!NS_IsMainThread());
MOZ_ASSERT(!mJumpListBackend);
mJumpListBackend = new NativeJumpListBackend();
}
void JumpListBuilder::DoShutdownBackend() {
MOZ_ASSERT(!NS_IsMainThread());
mJumpListBackend = nullptr;
}
void JumpListBuilder::DoSetAppID(nsString aAppUserModelID) {
MOZ_ASSERT(!NS_IsMainThread());
MOZ_ASSERT(mJumpListBackend);
mJumpListBackend->SetAppID(aAppUserModelID.get());
}
NS_IMETHODIMP
JumpListBuilder::ObtainAndCacheFavicon(nsIURI* aFaviconURI,
nsAString& aCachedIconPath) {
MOZ_ASSERT(NS_IsMainThread());
nsString iconFilePath;
nsresult rv = mozilla::widget::FaviconHelper::ObtainCachedIconFile(
aFaviconURI, iconFilePath, mIOThread, false);
NS_ENSURE_SUCCESS(rv, rv);
aCachedIconPath = iconFilePath;
return NS_OK;
}
NS_IMETHODIMP
JumpListBuilder::IsAvailable(JSContext* aCx, Promise** aPromise) {
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(aPromise);
MOZ_ASSERT(mIOThread);
ErrorResult result;
RefPtr<Promise> promise =
Promise::Create(xpc::CurrentNativeGlobal(aCx), result);
if (MOZ_UNLIKELY(result.Failed())) {
return result.StealNSResult();
}
nsMainThreadPtrHandle<Promise> promiseHolder(
new nsMainThreadPtrHolder<Promise>("JumpListBuilder::IsAvailable promise",
promise));
nsCOMPtr<nsIRunnable> runnable =
NewRunnableMethod<nsMainThreadPtrHandle<Promise>>(
"IsAvailable", this, &JumpListBuilder::DoIsAvailable,
std::move(promiseHolder));
nsresult rv = mIOThread->Dispatch(runnable, NS_DISPATCH_NORMAL);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
promise.forget(aPromise);
return NS_OK;
}
NS_IMETHODIMP
JumpListBuilder::CheckForRemovals(JSContext* aCx, Promise** aPromise) {
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(aPromise);
MOZ_ASSERT(mIOThread);
ErrorResult result;
RefPtr<Promise> promise =
Promise::Create(xpc::CurrentNativeGlobal(aCx), result);
if (MOZ_UNLIKELY(result.Failed())) {
return result.StealNSResult();
}
nsMainThreadPtrHandle<Promise> promiseHolder(
new nsMainThreadPtrHolder<Promise>(
"JumpListBuilder::CheckForRemovals promise", promise));
nsCOMPtr<nsIRunnable> runnable =
NewRunnableMethod<nsMainThreadPtrHandle<Promise>>(
"CheckForRemovals", this, &JumpListBuilder::DoCheckForRemovals,
std::move(promiseHolder));
nsresult rv = mIOThread->Dispatch(runnable, NS_DISPATCH_NORMAL);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
promise.forget(aPromise);
return NS_OK;
}
NS_IMETHODIMP
JumpListBuilder::PopulateJumpList(
const nsTArray<JS::Value>& aTaskDescriptions, const nsAString& aCustomTitle,
const nsTArray<JS::Value>& aCustomDescriptions, JSContext* aCx,
Promise** aPromise) {
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(aPromise);
MOZ_ASSERT(mIOThread);
if (aCustomDescriptions.Length() && aCustomTitle.IsEmpty()) {
return NS_ERROR_INVALID_ARG;
}
// Get rid of the old icons
nsCOMPtr<nsIRunnable> event =
new mozilla::widget::AsyncDeleteAllFaviconsFromDisk(true);
mIOThread->Dispatch(event, NS_DISPATCH_NORMAL);
nsTArray<WindowsJumpListShortcutDescription> taskDescs;
for (auto& jsval : aTaskDescriptions) {
JS::Rooted<JS::Value> rootedVal(aCx);
if (NS_WARN_IF(!dom::ToJSValue(aCx, jsval, &rootedVal))) {
return NS_ERROR_INVALID_ARG;
}
WindowsJumpListShortcutDescription desc;
if (!desc.Init(aCx, rootedVal)) {
return NS_ERROR_INVALID_ARG;
}
taskDescs.AppendElement(std::move(desc));
}
nsTArray<WindowsJumpListShortcutDescription> customDescs;
for (auto& jsval : aCustomDescriptions) {
JS::Rooted<JS::Value> rootedVal(aCx);
if (NS_WARN_IF(!dom::ToJSValue(aCx, jsval, &rootedVal))) {
return NS_ERROR_INVALID_ARG;
}
WindowsJumpListShortcutDescription desc;
if (!desc.Init(aCx, rootedVal)) {
return NS_ERROR_INVALID_ARG;
}
customDescs.AppendElement(std::move(desc));
}
ErrorResult result;
RefPtr<Promise> promise =
Promise::Create(xpc::CurrentNativeGlobal(aCx), result);
if (MOZ_UNLIKELY(result.Failed())) {
return result.StealNSResult();
}
nsMainThreadPtrHandle<Promise> promiseHolder(
new nsMainThreadPtrHolder<Promise>(
"JumpListBuilder::PopulateJumpList promise", promise));
nsCOMPtr<nsIRunnable> runnable = NewRunnableMethod<
StoreCopyPassByRRef<nsTArray<WindowsJumpListShortcutDescription>>,
nsString,
StoreCopyPassByRRef<nsTArray<WindowsJumpListShortcutDescription>>,
nsMainThreadPtrHandle<Promise>>(
"PopulateJumpList", this, &JumpListBuilder::DoPopulateJumpList,
std::move(taskDescs), aCustomTitle, std::move(customDescs),
std::move(promiseHolder));
nsresult rv = mIOThread->Dispatch(runnable, NS_DISPATCH_NORMAL);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
promise.forget(aPromise);
return NS_OK;
}
NS_IMETHODIMP
JumpListBuilder::ClearJumpList(JSContext* aCx, Promise** aPromise) {
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(aPromise);
MOZ_ASSERT(mIOThread);
ErrorResult result;
RefPtr<Promise> promise =
Promise::Create(xpc::CurrentNativeGlobal(aCx), result);
if (MOZ_UNLIKELY(result.Failed())) {
return result.StealNSResult();
}
nsMainThreadPtrHandle<Promise> promiseHolder(
new nsMainThreadPtrHolder<Promise>(
"JumpListBuilder::ClearJumpList promise", promise));
nsCOMPtr<nsIRunnable> runnable =
NewRunnableMethod<nsMainThreadPtrHandle<Promise>>(
"ClearJumpList", this, &JumpListBuilder::DoClearJumpList,
std::move(promiseHolder));
nsresult rv = mIOThread->Dispatch(runnable, NS_DISPATCH_NORMAL);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
promise.forget(aPromise);
return NS_OK;
}
void JumpListBuilder::DoIsAvailable(
const nsMainThreadPtrHandle<Promise>& aPromiseHolder) {
MOZ_ASSERT(!NS_IsMainThread());
MOZ_ASSERT(aPromiseHolder);
if (!mJumpListBackend) {
NS_DispatchToMainThread(NS_NewRunnableFunction(
"DoIsAvailable", [promiseHolder = std::move(aPromiseHolder)]() {
promiseHolder.get()->MaybeResolve(false);
}));
return;
}
bool isAvailable = mJumpListBackend->IsAvailable();
NS_DispatchToMainThread(NS_NewRunnableFunction(
"DoIsAvailable",
[promiseHolder = std::move(aPromiseHolder), isAvailable]() {
promiseHolder.get()->MaybeResolve(isAvailable);
}));
}
void JumpListBuilder::DoCheckForRemovals(
const nsMainThreadPtrHandle<Promise>& aPromiseHolder) {
MOZ_ASSERT(!NS_IsMainThread());
MOZ_ASSERT(aPromiseHolder);
nsresult rv = NS_ERROR_UNEXPECTED;
auto errorHandler = MakeScopeExit([&aPromiseHolder, &rv]() {
NS_DispatchToMainThread(NS_NewRunnableFunction(
"DoCheckForRemovals",
[promiseHolder = std::move(aPromiseHolder), rv]() {
promiseHolder.get()->MaybeReject(rv);
}));
});
MOZ_ASSERT(mJumpListBackend);
if (!mJumpListBackend) {
return;
}
// Abort any ongoing list building that might not have been committed,
// otherwise BeginList will give us problems.
Unused << mJumpListBackend->AbortList();
nsTArray<nsString> urisToRemove;
RefPtr<IObjectArray> objArray;
uint32_t maxItems = 0;
HRESULT hr = mJumpListBackend->BeginList(
&maxItems,
IID_PPV_ARGS(static_cast<IObjectArray**>(getter_AddRefs(objArray))));
if (FAILED(hr)) {
rv = NS_ERROR_UNEXPECTED;
return;
}
RemoveIconCacheAndGetJumplistShortcutURIs(objArray, urisToRemove);
// We began a list in order to get the removals, which we can now abort.
Unused << mJumpListBackend->AbortList();
errorHandler.release();
NS_DispatchToMainThread(NS_NewRunnableFunction(
"DoCheckForRemovals", [urisToRemove = std::move(urisToRemove),
promiseHolder = std::move(aPromiseHolder)]() {
promiseHolder.get()->MaybeResolve(urisToRemove);
}));
}
void JumpListBuilder::DoPopulateJumpList(
const nsTArray<WindowsJumpListShortcutDescription>&& aTaskDescriptions,
const nsAString& aCustomTitle,
const nsTArray<WindowsJumpListShortcutDescription>&& aCustomDescriptions,
const nsMainThreadPtrHandle<Promise>& aPromiseHolder) {
MOZ_ASSERT(!NS_IsMainThread());
MOZ_ASSERT(aPromiseHolder);
nsresult rv = NS_ERROR_UNEXPECTED;
auto errorHandler = MakeScopeExit([&aPromiseHolder, &rv]() {
NS_DispatchToMainThread(NS_NewRunnableFunction(
"DoPopulateJumpList",
[promiseHolder = std::move(aPromiseHolder), rv]() {
promiseHolder.get()->MaybeReject(rv);
}));
});
MOZ_ASSERT(mJumpListBackend);
if (!mJumpListBackend) {
return;
}
// Abort any ongoing list building that might not have been committed,
// otherwise BeginList will give us problems.
Unused << mJumpListBackend->AbortList();
nsTArray<nsString> urisToRemove;
RefPtr<IObjectArray> objArray;
uint32_t maxItems = 0;
HRESULT hr = mJumpListBackend->BeginList(
&maxItems,
IID_PPV_ARGS(static_cast<IObjectArray**>(getter_AddRefs(objArray))));
if (FAILED(hr)) {
rv = NS_ERROR_UNEXPECTED;
return;
}
if (urisToRemove.Length()) {
// It'd be nice if we could return a more descriptive error here so that
// the caller knows that they should have called checkForRemovals first.
rv = NS_ERROR_UNEXPECTED;
return;
}
if (aTaskDescriptions.Length()) {
RefPtr<IObjectCollection> taskCollection;
hr = CoCreateInstance(CLSID_EnumerableObjectCollection, nullptr,
CLSCTX_INPROC_SERVER, IID_IObjectCollection,
getter_AddRefs(taskCollection));
if (FAILED(hr)) {
rv = NS_ERROR_UNEXPECTED;
return;
}
// Start by building the task list
for (auto& desc : aTaskDescriptions) {
// These should all be ShellLinks
RefPtr<IShellLinkW> link;
rv = GetShellLinkFromDescription(desc, link);
if (NS_FAILED(rv)) {
// Let the errorHandler deal with this.
return;
}
taskCollection->AddObject(link);
}
RefPtr<IObjectArray> pTaskArray;
hr = taskCollection->QueryInterface(IID_IObjectArray,
getter_AddRefs(pTaskArray));
if (FAILED(hr)) {
rv = NS_ERROR_UNEXPECTED;
return;
}
hr = mJumpListBackend->AddUserTasks(pTaskArray);
if (FAILED(hr)) {
rv = NS_ERROR_FAILURE;
return;
}
}
if (aCustomDescriptions.Length()) {
// Then build the custom list
RefPtr<IObjectCollection> customCollection;
hr = CoCreateInstance(CLSID_EnumerableObjectCollection, nullptr,
CLSCTX_INPROC_SERVER, IID_IObjectCollection,
getter_AddRefs(customCollection));
if (FAILED(hr)) {
rv = NS_ERROR_UNEXPECTED;
return;
}
for (auto& desc : aCustomDescriptions) {
// These should all be ShellLinks
RefPtr<IShellLinkW> link;
rv = GetShellLinkFromDescription(desc, link);
if (NS_FAILED(rv)) {
// Let the errorHandler deal with this.
return;
}
customCollection->AddObject(link);
}
RefPtr<IObjectArray> pCustomArray;
hr = customCollection->QueryInterface(IID_IObjectArray,
getter_AddRefs(pCustomArray));
if (FAILED(hr)) {
rv = NS_ERROR_UNEXPECTED;
return;
}
hr = mJumpListBackend->AppendCategory(
reinterpret_cast<const wchar_t*>(aCustomTitle.BeginReading()),
pCustomArray);
if (FAILED(hr)) {
rv = NS_ERROR_UNEXPECTED;
return;
}
}
hr = mJumpListBackend->CommitList();
if (FAILED(hr)) {
rv = NS_ERROR_FAILURE;
return;
}
errorHandler.release();
NS_DispatchToMainThread(NS_NewRunnableFunction(
"DoPopulateJumpList", [promiseHolder = std::move(aPromiseHolder)]() {
promiseHolder.get()->MaybeResolveWithUndefined();
}));
}
void JumpListBuilder::DoClearJumpList(
const nsMainThreadPtrHandle<Promise>& aPromiseHolder) {
MOZ_ASSERT(!NS_IsMainThread());
MOZ_ASSERT(aPromiseHolder);
if (!mJumpListBackend) {
NS_DispatchToMainThread(NS_NewRunnableFunction(
"DoClearJumpList", [promiseHolder = std::move(aPromiseHolder)]() {
promiseHolder.get()->MaybeReject(NS_ERROR_UNEXPECTED);
}));
return;
}
if (SUCCEEDED(mJumpListBackend->DeleteList(mAppUserModelId.get()))) {
NS_DispatchToMainThread(NS_NewRunnableFunction(
"DoClearJumpList", [promiseHolder = std::move(aPromiseHolder)]() {
promiseHolder.get()->MaybeResolveWithUndefined();
}));
} else {
NS_DispatchToMainThread(NS_NewRunnableFunction(
"DoClearJumpList", [promiseHolder = std::move(aPromiseHolder)]() {
promiseHolder.get()->MaybeReject(NS_ERROR_FAILURE);
}));
}
}
// RemoveIconCacheAndGetJumplistShortcutURIs - does multiple things to
// avoid unnecessary extra XPCOM incantations. For each object in the input
// array, if it's an IShellLinkW, this deletes the cached icon and adds the
// url param to a list of URLs to be removed from the places database.
void JumpListBuilder::RemoveIconCacheAndGetJumplistShortcutURIs(
IObjectArray* aObjArray, nsTArray<nsString>& aURISpecs) {
MOZ_ASSERT(!NS_IsMainThread());
// Early return here just in case some versions of Windows don't populate this
if (!aObjArray) {
return;
}
uint32_t count = 0;
aObjArray->GetCount(&count);
for (uint32_t idx = 0; idx < count; idx++) {
RefPtr<IShellLinkW> pLink;
if (FAILED(aObjArray->GetAt(idx, IID_IShellLinkW,
static_cast<void**>(getter_AddRefs(pLink))))) {
continue;
}
wchar_t buf[MAX_PATH];
HRESULT hres = pLink->GetArguments(buf, MAX_PATH);
if (SUCCEEDED(hres)) {
LPWSTR* arglist;
int32_t numArgs;
arglist = ::CommandLineToArgvW(buf, &numArgs);
if (arglist && numArgs > 0) {
nsString spec(arglist[0]);
aURISpecs.AppendElement(std::move(spec));
::LocalFree(arglist);
}
}
int iconIdx = 0;
hres = pLink->GetIconLocation(buf, MAX_PATH, &iconIdx);
if (SUCCEEDED(hres)) {
nsDependentString spec(buf);
DeleteIconFromDisk(spec);
}
}
}
void JumpListBuilder::DeleteIconFromDisk(const nsAString& aPath) {
MOZ_ASSERT(!NS_IsMainThread());
// Check that we aren't deleting some arbitrary file that is not an icon
if (StringTail(aPath, 4).LowerCaseEqualsASCII(".ico")) {
// Construct the parent path of the passed in path
nsCOMPtr<nsIFile> icoFile;
nsresult rv = NS_NewLocalFile(aPath, true, getter_AddRefs(icoFile));
if (NS_WARN_IF(NS_FAILED(rv))) {
return;
}
icoFile->Remove(false);
}
}
// Converts a WindowsJumpListShortcutDescription into a IShellLinkW
nsresult JumpListBuilder::GetShellLinkFromDescription(
const WindowsJumpListShortcutDescription& aDesc,
RefPtr<IShellLinkW>& aShellLink) {
MOZ_ASSERT(!NS_IsMainThread());
HRESULT hr;
IShellLinkW* psl;
// Shell links:
// http://msdn.microsoft.com/en-us/library/bb776891(VS.85).aspx
// http://msdn.microsoft.com/en-us/library/bb774950(VS.85).aspx
// Create a IShellLink
hr = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER,
IID_IShellLinkW, (LPVOID*)&psl);
if (FAILED(hr)) {
return NS_ERROR_UNEXPECTED;
}
// Store the title of the app
if (!aDesc.mTitle.IsEmpty()) {
IPropertyStore* pPropStore = nullptr;
hr = psl->QueryInterface(IID_IPropertyStore, (LPVOID*)&pPropStore);
if (FAILED(hr)) {
return NS_ERROR_UNEXPECTED;
}
PROPVARIANT pv;
::InitPropVariantFromString(aDesc.mTitle.get(), &pv);
pPropStore->SetValue(PKEY_Title, pv);
pPropStore->Commit();
pPropStore->Release();
PropVariantClear(&pv);
}
// Store the rest of the params
hr = psl->SetPath(aDesc.mPath.get());
hr = psl->SetDescription(aDesc.mDescription.get());
if (aDesc.mArguments.WasPassed() && !aDesc.mArguments.Value().IsEmpty()) {
hr = psl->SetArguments(aDesc.mArguments.Value().get());
} else {
hr = psl->SetArguments(L"");
}
// Set up the fallback icon in the event that a valid icon URI has
// not been supplied.
hr = psl->SetIconLocation(aDesc.mPath.get(), aDesc.mFallbackIconIndex);
if (aDesc.mIconPath.WasPassed() && !aDesc.mIconPath.Value().IsEmpty()) {
// Always use the first icon in the ICO file, as our encoded icon only has 1
// resource
hr = psl->SetIconLocation(aDesc.mIconPath.Value().get(), 0);
}
aShellLink = dont_AddRef(psl);
return NS_OK;
}
NS_IMETHODIMP
JumpListBuilder::Observe(nsISupports* aSubject, const char* aTopic,
const char16_t* aData) {
MOZ_ASSERT(NS_IsMainThread());
NS_ENSURE_ARG_POINTER(aTopic);
if (strcmp(aTopic, TOPIC_PROFILE_BEFORE_CHANGE) == 0) {
nsCOMPtr<nsIObserverService> observerService =
do_GetService("@mozilla.org/observer-service;1");
if (observerService) {
observerService->RemoveObserver(this, TOPIC_PROFILE_BEFORE_CHANGE);
observerService->RemoveObserver(this, TOPIC_CLEAR_PRIVATE_DATA);
}
mIOThread->Dispatch(NewRunnableMethod("ShutdownBackend", this,
&JumpListBuilder::DoShutdownBackend),
NS_DISPATCH_NORMAL);
mIOThread->Shutdown();
} else if (strcmp(aTopic, "nsPref:changed") == 0 &&
nsDependentString(aData).EqualsASCII(kPrefTaskbarEnabled)) {
bool enabled = Preferences::GetBool(kPrefTaskbarEnabled, true);
if (!enabled) {
nsCOMPtr<nsIRunnable> event =
new mozilla::widget::AsyncDeleteAllFaviconsFromDisk();
mIOThread->Dispatch(event, NS_DISPATCH_NORMAL);
}
} else if (strcmp(aTopic, TOPIC_CLEAR_PRIVATE_DATA) == 0) {
// Delete JumpListCache icons from Disk, if any.
nsCOMPtr<nsIRunnable> event =
new mozilla::widget::AsyncDeleteAllFaviconsFromDisk(false);
mIOThread->Dispatch(event, NS_DISPATCH_NORMAL);
}
return NS_OK;
}
} // namespace widget
} // namespace mozilla

Просмотреть файл

@ -0,0 +1,101 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifndef __JumpListBuilder_h__
#define __JumpListBuilder_h__
#include "nsIJumpListBuilder.h"
#include "nsIObserver.h"
#include "mozilla/LazyIdleThread.h"
namespace mozilla {
namespace dom {
struct WindowsJumpListShortcutDescription;
} // namespace dom
namespace widget {
/**
* This is an abstract class for a backend to write to the Windows Jump List.
*
* It has a 1-to-1 method mapping with ICustomDestinationList. The abtract
* class allows us to implement a "fake" backend for automated testing.
*/
class JumpListBackend {
NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING
virtual bool IsAvailable() = 0;
virtual HRESULT SetAppID(LPCWSTR pszAppID) = 0;
virtual HRESULT BeginList(UINT* pcMinSlots, REFIID riid, void** ppv) = 0;
virtual HRESULT AddUserTasks(IObjectArray* poa) = 0;
virtual HRESULT AppendCategory(LPCWSTR pszCategory, IObjectArray* poa) = 0;
virtual HRESULT CommitList() = 0;
virtual HRESULT AbortList() = 0;
virtual HRESULT DeleteList(LPCWSTR pszAppID) = 0;
virtual HRESULT AppendKnownCategory(KNOWNDESTCATEGORY category) = 0;
protected:
virtual ~JumpListBackend() {}
};
/**
* JumpListBuilder is a component that can be used to manage the Windows
* Jump List off of the main thread.
*/
class JumpListBuilder : public nsIJumpListBuilder, public nsIObserver {
virtual ~JumpListBuilder();
public:
NS_DECL_THREADSAFE_ISUPPORTS
NS_DECL_NSIJUMPLISTBUILDER
NS_DECL_NSIOBSERVER
explicit JumpListBuilder(const nsAString& aAppUserModelId,
RefPtr<JumpListBackend> aTestingBackend = nullptr);
private:
// These all run on the lazy helper thread.
void DoSetupBackend();
void DoSetupTestingBackend(RefPtr<JumpListBackend> aTestingBackend);
void DoShutdownBackend();
void DoSetAppID(nsString aAppUserModelID);
void DoIsAvailable(const nsMainThreadPtrHandle<dom::Promise>& aPromiseHolder);
void DoCheckForRemovals(
const nsMainThreadPtrHandle<dom::Promise>& aPromiseHolder);
void DoPopulateJumpList(
const nsTArray<dom::WindowsJumpListShortcutDescription>&&
aTaskDescriptions,
const nsAString& aCustomTitle,
const nsTArray<dom::WindowsJumpListShortcutDescription>&&
aCustomDescriptions,
const nsMainThreadPtrHandle<dom::Promise>& aPromiseHolder);
void DoClearJumpList(
const nsMainThreadPtrHandle<dom::Promise>& aPromiseHolder);
void RemoveIconCacheAndGetJumplistShortcutURIs(IObjectArray* aObjArray,
nsTArray<nsString>& aURISpecs);
void DeleteIconFromDisk(const nsAString& aPath);
nsresult GetShellLinkFromDescription(
const dom::WindowsJumpListShortcutDescription& aDesc,
RefPtr<IShellLinkW>& aShellLink);
// This is written to once during construction on the main thread before the
// lazy helper thread is created. After that, the lazy helper thread might
// read from it.
nsString mAppUserModelId;
// This is only accessed by the lazy helper thread.
RefPtr<JumpListBackend> mJumpListBackend;
// This is only accessed by the main thread.
RefPtr<LazyIdleThread> mIOThread;
};
} // namespace widget
} // namespace mozilla
#endif /* __JumpListBuilder_h__ */

Просмотреть файл

@ -11,6 +11,7 @@
#include "nsITaskbarPreviewController.h"
#include "mozilla/RefPtr.h"
#include "mozilla/widget/JumpListBuilder.h"
#include <nsError.h>
#include <nsCOMPtr.h>
#include <nsIWidget.h>
@ -36,7 +37,8 @@
#include <propkey.h>
#include <shellapi.h>
static NS_DEFINE_CID(kLegacyJumpListBuilderCID, NS_WIN_LEGACYJUMPLISTBUILDER_CID);
static NS_DEFINE_CID(kLegacyJumpListBuilderCID,
NS_WIN_LEGACYJUMPLISTBUILDER_CID);
namespace {
@ -404,8 +406,8 @@ WinTaskbar::GetOverlayIconController(
}
NS_IMETHODIMP
WinTaskbar::CreateLegacyJumpListBuilder(bool aPrivateBrowsing,
nsILegacyJumpListBuilder** aJumpListBuilder) {
WinTaskbar::CreateLegacyJumpListBuilder(
bool aPrivateBrowsing, nsILegacyJumpListBuilder** aJumpListBuilder) {
nsresult rv;
if (LegacyJumpListBuilder::sBuildingList) return NS_ERROR_ALREADY_INITIALIZED;
@ -423,6 +425,21 @@ WinTaskbar::CreateLegacyJumpListBuilder(bool aPrivateBrowsing,
return NS_OK;
}
NS_IMETHODIMP
WinTaskbar::CreateJumpListBuilder(bool aPrivateBrowsing,
nsIJumpListBuilder** aJumpListBuilder) {
nsAutoString aumid;
GenerateAppUserModelID(aumid, aPrivateBrowsing);
nsCOMPtr<nsIJumpListBuilder> builder = new JumpListBuilder(aumid);
if (!builder) {
return NS_ERROR_UNEXPECTED;
}
NS_IF_ADDREF(*aJumpListBuilder = builder);
return NS_OK;
}
NS_IMETHODIMP
WinTaskbar::SetGroupIdForWindow(mozIDOMWindow* aParent,
const nsAString& aIdentifier) {

Просмотреть файл

@ -52,6 +52,7 @@ EXPORTS.mozilla.widget += [
"CompositorWidgetChild.h",
"CompositorWidgetParent.h",
"InProcessWinCompositorWidget.h",
"JumpListBuilder.h",
"nsWindowLoggedMessages.h",
"WinCompositorWidget.h",
"WinCompositorWindowThread.h",
@ -72,6 +73,7 @@ UNIFIED_SOURCES += [
"GfxInfo.cpp",
"IEnumFE.cpp",
"IMMHandler.cpp",
"JumpListBuilder.cpp",
"KeyboardLayout.cpp",
"LegacyJumpListItem.cpp",
"LSPAnnotator.cpp",