1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 #include "LegacyJumpListBuilder.h"
10 #include "nsServiceManagerUtils.h"
12 #include "nsArrayUtils.h"
13 #include "nsWidgetsCID.h"
14 #include "WinTaskbar.h"
15 #include "nsDirectoryServiceUtils.h"
16 #include "mozilla/Preferences.h"
17 #include "nsStringStream.h"
18 #include "nsThreadUtils.h"
19 #include "mozilla/LazyIdleThread.h"
20 #include "nsIObserverService.h"
21 #include "mozilla/ScopeExit.h"
22 #include "mozilla/Unused.h"
23 #include "mozilla/dom/Promise.h"
24 #include "mozilla/mscom/ApartmentRegion.h"
25 #include "mozilla/mscom/EnsureMTA.h"
30 using mozilla::dom::Promise
;
32 // The amount of time, in milliseconds, that our IO thread will stay alive after
33 // the last event it processes.
34 #define DEFAULT_THREAD_TIMEOUT_MS 30000
39 // defined in WinTaskbar.cpp
40 extern const wchar_t* gMozillaJumpListIDGeneric
;
42 Atomic
<bool> LegacyJumpListBuilder::sBuildingList(false);
43 const char kPrefTaskbarEnabled
[] = "browser.taskbar.lists.enabled";
45 NS_IMPL_ISUPPORTS(LegacyJumpListBuilder
, nsILegacyJumpListBuilder
, nsIObserver
)
46 #define TOPIC_PROFILE_BEFORE_CHANGE "profile-before-change"
47 #define TOPIC_CLEAR_PRIVATE_DATA "clear-private-data"
51 class DoneCommitListBuildCallback final
: public nsIRunnable
{
52 NS_DECL_THREADSAFE_ISUPPORTS
55 DoneCommitListBuildCallback(nsILegacyJumpListCommittedCallback
* aCallback
,
56 LegacyJumpListBuilder
* aBuilder
)
57 : mCallback(aCallback
), mBuilder(aBuilder
), mResult(false) {}
59 NS_IMETHOD
Run() override
{
60 MOZ_ASSERT(NS_IsMainThread());
62 Unused
<< mCallback
->Done(mResult
);
64 // Ensure we are releasing on the main thread.
69 void SetResult(bool aResult
) { mResult
= aResult
; }
72 ~DoneCommitListBuildCallback() {
73 // Destructor does not always call on the main thread.
74 MOZ_ASSERT(!mCallback
);
75 MOZ_ASSERT(!mBuilder
);
79 MOZ_ASSERT(NS_IsMainThread());
84 // These two references MUST be released on the main thread.
85 RefPtr
<nsILegacyJumpListCommittedCallback
> mCallback
;
86 RefPtr
<LegacyJumpListBuilder
> mBuilder
;
90 NS_IMPL_ISUPPORTS(DoneCommitListBuildCallback
, nsIRunnable
);
94 LegacyJumpListBuilder::LegacyJumpListBuilder()
97 mMonitor("LegacyJumpListBuilderMonitor") {
98 MOZ_ASSERT(NS_IsMainThread());
100 // Instantiate mJumpListMgr in the multithreaded apartment so that proxied
101 // calls on that object do not need to interact with the main thread's message
103 mscom::EnsureMTA([&]() {
104 RefPtr
<ICustomDestinationList
> jumpListMgr
;
105 HRESULT hr
= ::CoCreateInstance(
106 CLSID_DestinationList
, nullptr, CLSCTX_INPROC_SERVER
,
107 IID_ICustomDestinationList
, getter_AddRefs(jumpListMgr
));
112 ReentrantMonitorAutoEnter
lock(mMonitor
);
113 // Since we are accessing mJumpListMgr across different threads
114 // (ie, different apartments), mJumpListMgr must be an agile reference.
115 mJumpListMgr
= mscom::AgileReference(jumpListMgr
);
122 // Make a lazy thread for any IO
123 mIOThread
= new LazyIdleThread(DEFAULT_THREAD_TIMEOUT_MS
, "Jump List",
124 LazyIdleThread::ManualShutdown
);
125 Preferences::AddStrongObserver(this, kPrefTaskbarEnabled
);
127 nsCOMPtr
<nsIObserverService
> observerService
=
128 do_GetService("@mozilla.org/observer-service;1");
129 if (observerService
) {
130 observerService
->AddObserver(this, TOPIC_PROFILE_BEFORE_CHANGE
, false);
131 observerService
->AddObserver(this, TOPIC_CLEAR_PRIVATE_DATA
, false);
135 LegacyJumpListBuilder::~LegacyJumpListBuilder() {
136 Preferences::RemoveObserver(this, kPrefTaskbarEnabled
);
139 NS_IMETHODIMP
LegacyJumpListBuilder::SetAppUserModelID(
140 const nsAString
& aAppUserModelId
) {
141 ReentrantMonitorAutoEnter
lock(mMonitor
);
142 if (!mJumpListMgr
) return NS_ERROR_NOT_AVAILABLE
;
144 RefPtr
<ICustomDestinationList
> jumpListMgr
= mJumpListMgr
.Resolve();
146 return NS_ERROR_NOT_AVAILABLE
;
149 mAppUserModelId
.Assign(aAppUserModelId
);
150 // MSIX packages explicitly do not support setting the appid from within
151 // the app, as it is set in the package manifest instead.
152 if (!mozilla::widget::WinUtils::HasPackageIdentity()) {
153 jumpListMgr
->SetAppID(mAppUserModelId
.get());
159 NS_IMETHODIMP
LegacyJumpListBuilder::GetAvailable(int16_t* aAvailable
) {
162 ReentrantMonitorAutoEnter
lock(mMonitor
);
163 if (mJumpListMgr
) *aAvailable
= true;
168 NS_IMETHODIMP
LegacyJumpListBuilder::GetIsListCommitted(bool* aCommit
) {
169 *aCommit
= mHasCommit
;
174 NS_IMETHODIMP
LegacyJumpListBuilder::GetMaxListItems(int16_t* aMaxItems
) {
175 ReentrantMonitorAutoEnter
lock(mMonitor
);
176 if (!mJumpListMgr
) return NS_ERROR_NOT_AVAILABLE
;
181 *aMaxItems
= mMaxItems
;
185 RefPtr
<ICustomDestinationList
> jumpListMgr
= mJumpListMgr
.Resolve();
187 return NS_ERROR_UNEXPECTED
;
190 IObjectArray
* objArray
;
191 if (SUCCEEDED(jumpListMgr
->BeginList(&mMaxItems
, IID_PPV_ARGS(&objArray
)))) {
192 *aMaxItems
= mMaxItems
;
194 if (objArray
) objArray
->Release();
196 jumpListMgr
->AbortList();
202 NS_IMETHODIMP
LegacyJumpListBuilder::InitListBuild(JSContext
* aCx
,
203 Promise
** aPromise
) {
204 ReentrantMonitorAutoEnter
lock(mMonitor
);
206 return NS_ERROR_NOT_AVAILABLE
;
209 nsIGlobalObject
* globalObject
= xpc::CurrentNativeGlobal(aCx
);
210 if (NS_WARN_IF(!globalObject
)) {
211 return NS_ERROR_FAILURE
;
215 RefPtr
<Promise
> promise
= Promise::Create(globalObject
, result
);
216 if (NS_WARN_IF(result
.Failed())) {
217 return result
.StealNSResult();
220 nsCOMPtr
<nsIRunnable
> runnable
=
221 NewRunnableMethod
<StoreCopyPassByRRef
<RefPtr
<Promise
>>>(
222 "InitListBuild", this, &LegacyJumpListBuilder::DoInitListBuild
,
224 nsresult rv
= mIOThread
->Dispatch(runnable
, NS_DISPATCH_NORMAL
);
225 if (NS_WARN_IF(NS_FAILED(rv
))) {
229 promise
.forget(aPromise
);
233 void LegacyJumpListBuilder::DoInitListBuild(RefPtr
<Promise
>&& aPromise
) {
234 // Since we're invoking COM interfaces to talk to the shell on a background
235 // thread, we need to be running inside a multithreaded apartment.
236 mscom::MTARegion mta
;
237 MOZ_ASSERT(mta
.IsValid());
239 ReentrantMonitorAutoEnter
lock(mMonitor
);
240 MOZ_ASSERT(mJumpListMgr
);
246 HRESULT hr
= E_UNEXPECTED
;
247 auto errorHandler
= MakeScopeExit([&aPromise
, &hr
]() {
252 NS_DispatchToMainThread(NS_NewRunnableFunction(
253 "InitListBuildReject", [promise
= std::move(aPromise
)]() {
254 promise
->MaybeReject(NS_ERROR_FAILURE
);
258 RefPtr
<ICustomDestinationList
> jumpListMgr
= mJumpListMgr
.Resolve();
263 nsTArray
<nsString
> urisToRemove
;
264 RefPtr
<IObjectArray
> objArray
;
265 hr
= jumpListMgr
->BeginList(
267 IID_PPV_ARGS(static_cast<IObjectArray
**>(getter_AddRefs(objArray
))));
272 // The returned objArray of removed items are for manually removed items.
273 // This does not return items which are removed because they were previously
274 // part of the jump list but are no longer part of the jump list.
275 sBuildingList
= true;
276 RemoveIconCacheAndGetJumplistShortcutURIs(objArray
, urisToRemove
);
278 NS_DispatchToMainThread(NS_NewRunnableFunction(
279 "InitListBuildResolve", [urisToRemove
= std::move(urisToRemove
),
280 promise
= std::move(aPromise
)]() {
281 promise
->MaybeResolve(urisToRemove
);
285 // Ensures that we have no old ICO files left in the jump list cache
286 nsresult
LegacyJumpListBuilder::RemoveIconCacheForAllItems() {
287 // Construct the path of our jump list cache
288 nsCOMPtr
<nsIFile
> jumpListCacheDir
;
290 NS_GetSpecialDirectory("ProfLDS", getter_AddRefs(jumpListCacheDir
));
291 NS_ENSURE_SUCCESS(rv
, rv
);
292 rv
= jumpListCacheDir
->AppendNative(
293 nsDependentCString(mozilla::widget::FaviconHelper::kJumpListCacheDir
));
294 NS_ENSURE_SUCCESS(rv
, rv
);
296 nsCOMPtr
<nsIDirectoryEnumerator
> entries
;
297 rv
= jumpListCacheDir
->GetDirectoryEntries(getter_AddRefs(entries
));
298 NS_ENSURE_SUCCESS(rv
, rv
);
300 // Loop through each directory entry and remove all ICO files found
302 nsCOMPtr
<nsIFile
> currFile
;
303 if (NS_FAILED(entries
->GetNextFile(getter_AddRefs(currFile
))) || !currFile
)
307 if (NS_FAILED(currFile
->GetPath(path
))) continue;
309 if (StringTail(path
, 4).LowerCaseEqualsASCII(".ico")) {
310 // Check if the cached ICO file exists
312 if (NS_FAILED(currFile
->Exists(&exists
)) || !exists
) continue;
314 // We found an ICO file that exists, so we should remove it
315 currFile
->Remove(false);
322 NS_IMETHODIMP
LegacyJumpListBuilder::AddListToBuild(int16_t aCatType
,
324 const nsAString
& catName
,
330 ReentrantMonitorAutoEnter
lock(mMonitor
);
331 if (!mJumpListMgr
) return NS_ERROR_NOT_AVAILABLE
;
333 RefPtr
<ICustomDestinationList
> jumpListMgr
= mJumpListMgr
.Resolve();
335 return NS_ERROR_UNEXPECTED
;
339 case nsILegacyJumpListBuilder::JUMPLIST_CATEGORY_TASKS
: {
340 NS_ENSURE_ARG_POINTER(items
);
343 RefPtr
<IObjectCollection
> collection
;
344 hr
= CoCreateInstance(CLSID_EnumerableObjectCollection
, nullptr,
345 CLSCTX_INPROC_SERVER
, IID_IObjectCollection
,
346 getter_AddRefs(collection
));
347 if (FAILED(hr
)) return NS_ERROR_UNEXPECTED
;
351 items
->GetLength(&length
);
352 for (uint32_t i
= 0; i
< length
; ++i
) {
353 nsCOMPtr
<nsILegacyJumpListItem
> item
= do_QueryElementAt(items
, i
);
355 // Check for separators
356 if (IsSeparator(item
)) {
357 RefPtr
<IShellLinkW
> link
;
358 rv
= LegacyJumpListSeparator::GetSeparator(link
);
359 if (NS_FAILED(rv
)) return rv
;
360 collection
->AddObject(link
);
363 // These should all be ShellLinks
364 RefPtr
<IShellLinkW
> link
;
365 rv
= LegacyJumpListShortcut::GetShellLink(item
, link
, mIOThread
);
366 if (NS_FAILED(rv
)) return rv
;
367 collection
->AddObject(link
);
370 // We need IObjectArray to submit
371 RefPtr
<IObjectArray
> pArray
;
372 hr
= collection
->QueryInterface(IID_IObjectArray
, getter_AddRefs(pArray
));
373 if (FAILED(hr
)) return NS_ERROR_UNEXPECTED
;
376 hr
= jumpListMgr
->AddUserTasks(pArray
);
377 if (SUCCEEDED(hr
)) *_retval
= true;
380 case nsILegacyJumpListBuilder::JUMPLIST_CATEGORY_RECENT
: {
381 if (SUCCEEDED(jumpListMgr
->AppendKnownCategory(KDC_RECENT
)))
385 case nsILegacyJumpListBuilder::JUMPLIST_CATEGORY_FREQUENT
: {
386 if (SUCCEEDED(jumpListMgr
->AppendKnownCategory(KDC_FREQUENT
)))
390 case nsILegacyJumpListBuilder::JUMPLIST_CATEGORY_CUSTOMLIST
: {
391 NS_ENSURE_ARG_POINTER(items
);
393 if (catName
.IsEmpty()) return NS_ERROR_INVALID_ARG
;
396 RefPtr
<IObjectCollection
> collection
;
397 hr
= CoCreateInstance(CLSID_EnumerableObjectCollection
, nullptr,
398 CLSCTX_INPROC_SERVER
, IID_IObjectCollection
,
399 getter_AddRefs(collection
));
400 if (FAILED(hr
)) return NS_ERROR_UNEXPECTED
;
403 items
->GetLength(&length
);
404 for (uint32_t i
= 0; i
< length
; ++i
) {
405 nsCOMPtr
<nsILegacyJumpListItem
> item
= do_QueryElementAt(items
, i
);
408 if (NS_FAILED(item
->GetType(&type
))) continue;
410 case nsILegacyJumpListItem::JUMPLIST_ITEM_SEPARATOR
: {
411 RefPtr
<IShellLinkW
> shellItem
;
412 rv
= LegacyJumpListSeparator::GetSeparator(shellItem
);
413 if (NS_FAILED(rv
)) return rv
;
414 collection
->AddObject(shellItem
);
416 case nsILegacyJumpListItem::JUMPLIST_ITEM_LINK
: {
417 RefPtr
<IShellItem2
> shellItem
;
418 rv
= LegacyJumpListLink::GetShellItem(item
, shellItem
);
419 if (NS_FAILED(rv
)) return rv
;
420 collection
->AddObject(shellItem
);
422 case nsILegacyJumpListItem::JUMPLIST_ITEM_SHORTCUT
: {
423 RefPtr
<IShellLinkW
> shellItem
;
424 rv
= LegacyJumpListShortcut::GetShellLink(item
, shellItem
,
426 if (NS_FAILED(rv
)) return rv
;
427 collection
->AddObject(shellItem
);
432 // We need IObjectArray to submit
433 RefPtr
<IObjectArray
> pArray
;
434 hr
= collection
->QueryInterface(IID_IObjectArray
, (LPVOID
*)&pArray
);
435 if (FAILED(hr
)) return NS_ERROR_UNEXPECTED
;
438 hr
= jumpListMgr
->AppendCategory(
439 reinterpret_cast<const wchar_t*>(catName
.BeginReading()), pArray
);
440 if (SUCCEEDED(hr
)) *_retval
= true;
442 // Get rid of the old icons
443 nsCOMPtr
<nsIRunnable
> event
=
444 new mozilla::widget::AsyncDeleteAllFaviconsFromDisk(true);
445 mIOThread
->Dispatch(event
, NS_DISPATCH_NORMAL
);
453 NS_IMETHODIMP
LegacyJumpListBuilder::AbortListBuild() {
454 ReentrantMonitorAutoEnter
lock(mMonitor
);
455 if (!mJumpListMgr
) return NS_ERROR_NOT_AVAILABLE
;
457 RefPtr
<ICustomDestinationList
> jumpListMgr
= mJumpListMgr
.Resolve();
459 return NS_ERROR_UNEXPECTED
;
462 jumpListMgr
->AbortList();
463 sBuildingList
= false;
468 NS_IMETHODIMP
LegacyJumpListBuilder::CommitListBuild(
469 nsILegacyJumpListCommittedCallback
* aCallback
) {
470 ReentrantMonitorAutoEnter
lock(mMonitor
);
471 if (!mJumpListMgr
) return NS_ERROR_NOT_AVAILABLE
;
473 // Also holds a strong reference to this to prevent use-after-free.
474 RefPtr
<detail::DoneCommitListBuildCallback
> callback
=
475 new detail::DoneCommitListBuildCallback(aCallback
, this);
477 // The builder has a strong reference in the callback already, so we do not
478 // need to do it for this runnable again.
479 RefPtr
<nsIRunnable
> event
=
480 NewNonOwningRunnableMethod
<RefPtr
<detail::DoneCommitListBuildCallback
>>(
481 "LegacyJumpListBuilder::DoCommitListBuild", this,
482 &LegacyJumpListBuilder::DoCommitListBuild
, std::move(callback
));
483 Unused
<< mIOThread
->Dispatch(event
, NS_DISPATCH_NORMAL
);
488 void LegacyJumpListBuilder::DoCommitListBuild(
489 RefPtr
<detail::DoneCommitListBuildCallback
> aCallback
) {
490 // Since we're invoking COM interfaces to talk to the shell on a background
491 // thread, we need to be running inside a multithreaded apartment.
492 mscom::MTARegion mta
;
493 MOZ_ASSERT(mta
.IsValid());
495 ReentrantMonitorAutoEnter
lock(mMonitor
);
496 MOZ_ASSERT(mJumpListMgr
);
497 MOZ_ASSERT(aCallback
);
499 HRESULT hr
= E_UNEXPECTED
;
500 auto onExit
= MakeScopeExit([&hr
, &aCallback
]() {
501 // XXX We might want some specific error data here.
502 aCallback
->SetResult(SUCCEEDED(hr
));
503 Unused
<< NS_DispatchToMainThread(aCallback
);
506 RefPtr
<ICustomDestinationList
> jumpListMgr
= mJumpListMgr
.Resolve();
511 hr
= jumpListMgr
->CommitList();
512 sBuildingList
= false;
519 NS_IMETHODIMP
LegacyJumpListBuilder::DeleteActiveList(bool* _retval
) {
522 ReentrantMonitorAutoEnter
lock(mMonitor
);
523 if (!mJumpListMgr
) return NS_ERROR_NOT_AVAILABLE
;
529 RefPtr
<ICustomDestinationList
> jumpListMgr
= mJumpListMgr
.Resolve();
531 return NS_ERROR_UNEXPECTED
;
534 if (SUCCEEDED(jumpListMgr
->DeleteList(mAppUserModelId
.get()))) {
543 bool LegacyJumpListBuilder::IsSeparator(nsCOMPtr
<nsILegacyJumpListItem
>& item
) {
545 item
->GetType(&type
);
546 if (NS_FAILED(item
->GetType(&type
))) return false;
548 if (type
== nsILegacyJumpListItem::JUMPLIST_ITEM_SEPARATOR
) return true;
552 // RemoveIconCacheAndGetJumplistShortcutURIs - does multiple things to
553 // avoid unnecessary extra XPCOM incantations. For each object in the input
554 // array, if it's an IShellLinkW, this deletes the cached icon and adds the
555 // url param to a list of URLs to be removed from the places database.
556 void LegacyJumpListBuilder::RemoveIconCacheAndGetJumplistShortcutURIs(
557 IObjectArray
* aObjArray
, nsTArray
<nsString
>& aURISpecs
) {
558 MOZ_ASSERT(!NS_IsMainThread());
560 // Early return here just in case some versions of Windows don't populate this
566 aObjArray
->GetCount(&count
);
568 for (uint32_t idx
= 0; idx
< count
; idx
++) {
569 RefPtr
<IShellLinkW
> pLink
;
571 if (FAILED(aObjArray
->GetAt(idx
, IID_IShellLinkW
,
572 static_cast<void**>(getter_AddRefs(pLink
))))) {
576 wchar_t buf
[MAX_PATH
];
577 HRESULT hres
= pLink
->GetArguments(buf
, MAX_PATH
);
578 if (SUCCEEDED(hres
)) {
582 arglist
= ::CommandLineToArgvW(buf
, &numArgs
);
583 if (arglist
&& numArgs
> 0) {
584 nsString
spec(arglist
[0]);
585 aURISpecs
.AppendElement(std::move(spec
));
586 ::LocalFree(arglist
);
591 hres
= pLink
->GetIconLocation(buf
, MAX_PATH
, &iconIdx
);
592 if (SUCCEEDED(hres
)) {
593 nsDependentString
spec(buf
);
594 DeleteIconFromDisk(spec
);
599 void LegacyJumpListBuilder::DeleteIconFromDisk(const nsAString
& aPath
) {
600 MOZ_ASSERT(!NS_IsMainThread());
602 // Check that we aren't deleting some arbitrary file that is not an icon
603 if (StringTail(aPath
, 4).LowerCaseEqualsASCII(".ico")) {
604 // Construct the parent path of the passed in path
605 nsCOMPtr
<nsIFile
> icoFile
;
606 nsresult rv
= NS_NewLocalFile(aPath
, true, getter_AddRefs(icoFile
));
607 if (NS_WARN_IF(NS_FAILED(rv
))) {
611 icoFile
->Remove(false);
615 NS_IMETHODIMP
LegacyJumpListBuilder::Observe(nsISupports
* aSubject
,
617 const char16_t
* aData
) {
618 NS_ENSURE_ARG_POINTER(aTopic
);
619 if (strcmp(aTopic
, TOPIC_PROFILE_BEFORE_CHANGE
) == 0) {
620 nsCOMPtr
<nsIObserverService
> observerService
=
621 do_GetService("@mozilla.org/observer-service;1");
622 if (observerService
) {
623 observerService
->RemoveObserver(this, TOPIC_PROFILE_BEFORE_CHANGE
);
625 mIOThread
->Shutdown();
626 // Clear out mJumpListMgr, as MSCOM services won't be available soon.
627 ReentrantMonitorAutoEnter
lock(mMonitor
);
628 mJumpListMgr
= nullptr;
629 } else if (strcmp(aTopic
, "nsPref:changed") == 0 &&
630 nsDependentString(aData
).EqualsASCII(kPrefTaskbarEnabled
)) {
631 bool enabled
= Preferences::GetBool(kPrefTaskbarEnabled
, true);
633 nsCOMPtr
<nsIRunnable
> event
=
634 new mozilla::widget::AsyncDeleteAllFaviconsFromDisk();
635 mIOThread
->Dispatch(event
, NS_DISPATCH_NORMAL
);
637 } else if (strcmp(aTopic
, TOPIC_CLEAR_PRIVATE_DATA
) == 0) {
638 // Delete JumpListCache icons from Disk, if any.
639 nsCOMPtr
<nsIRunnable
> event
=
640 new mozilla::widget::AsyncDeleteAllFaviconsFromDisk(false);
641 mIOThread
->Dispatch(event
, NS_DISPATCH_NORMAL
);
646 } // namespace widget
647 } // namespace mozilla