summaryrefslogtreecommitdiffstats
path: root/widget/windows/JumpListBuilder.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'widget/windows/JumpListBuilder.cpp')
-rw-r--r--widget/windows/JumpListBuilder.cpp818
1 files changed, 818 insertions, 0 deletions
diff --git a/widget/windows/JumpListBuilder.cpp b/widget/windows/JumpListBuilder.cpp
new file mode 100644
index 0000000000..80b1c29aa7
--- /dev/null
+++ b/widget/windows/JumpListBuilder.cpp
@@ -0,0 +1,818 @@
+/* -*- 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 <windows.h>
+#include <shobjidl.h>
+#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 "nsIFile.h"
+#include "nsIObserverService.h"
+#include "nsServiceManagerUtils.h"
+#include "WinUtils.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());
+
+ // According to the documentation at [1], the maximum description length is
+ // INFOTIPSIZE, so we copy the const string from the description into a buffer
+ // of that maximum size. However, testing reveals that INFOTIPSIZE is still
+ // sometimes too large. MAX_PATH seems to work instead.
+ //
+ // We truncate to MAX_PATH - 1, since nsAutoString's don't include the null
+ // character in their capacity calculations, but the string for IShellLinkW
+ // description does. So by truncating to MAX_PATH - 1, the full contents of
+ // the truncated nsAutoString will be copied into the IShellLink description
+ // buffer.
+ //
+ // [1]:
+ // https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-ishelllinka-setdescription
+ nsAutoString descriptionCopy(aDesc.mDescription.get());
+ if (descriptionCopy.Length() >= MAX_PATH) {
+ descriptionCopy.Truncate(MAX_PATH - 1);
+ }
+
+ hr = psl->SetDescription(descriptionCopy.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