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 "JumpListBuilder.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> JumpListBuilder::sBuildingList(false);
43 const char kPrefTaskbarEnabled
[] = "browser.taskbar.lists.enabled";
45 NS_IMPL_ISUPPORTS(JumpListBuilder
, nsIJumpListBuilder
, 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(nsIJumpListCommittedCallback
* aCallback
,
56 JumpListBuilder
* 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
<nsIJumpListCommittedCallback
> mCallback
;
86 RefPtr
<JumpListBuilder
> mBuilder
;
90 NS_IMPL_ISUPPORTS(DoneCommitListBuildCallback
, nsIRunnable
);
94 JumpListBuilder::JumpListBuilder()
95 : mMaxItems(0), mHasCommit(false), mMonitor("JumpListBuilderMonitor") {
96 MOZ_ASSERT(NS_IsMainThread());
98 // Instantiate mJumpListMgr in the multithreaded apartment so that proxied
99 // calls on that object do not need to interact with the main thread's message
101 mscom::EnsureMTA([&]() {
102 RefPtr
<ICustomDestinationList
> jumpListMgr
;
103 HRESULT hr
= ::CoCreateInstance(
104 CLSID_DestinationList
, nullptr, CLSCTX_INPROC_SERVER
,
105 IID_ICustomDestinationList
, getter_AddRefs(jumpListMgr
));
110 ReentrantMonitorAutoEnter
lock(mMonitor
);
111 // Since we are accessing mJumpListMgr across different threads
112 // (ie, different apartments), mJumpListMgr must be an agile reference.
113 mJumpListMgr
= jumpListMgr
;
120 // Make a lazy thread for any IO
121 mIOThread
= new LazyIdleThread(DEFAULT_THREAD_TIMEOUT_MS
, "Jump List",
122 LazyIdleThread::ManualShutdown
);
123 Preferences::AddStrongObserver(this, kPrefTaskbarEnabled
);
125 nsCOMPtr
<nsIObserverService
> observerService
=
126 do_GetService("@mozilla.org/observer-service;1");
127 if (observerService
) {
128 observerService
->AddObserver(this, TOPIC_PROFILE_BEFORE_CHANGE
, false);
129 observerService
->AddObserver(this, TOPIC_CLEAR_PRIVATE_DATA
, false);
132 RefPtr
<ICustomDestinationList
> jumpListMgr
= mJumpListMgr
;
138 JumpListBuilder::~JumpListBuilder() {
139 Preferences::RemoveObserver(this, kPrefTaskbarEnabled
);
142 NS_IMETHODIMP
JumpListBuilder::SetAppUserModelID(
143 const nsAString
& aAppUserModelId
) {
144 ReentrantMonitorAutoEnter
lock(mMonitor
);
145 if (!mJumpListMgr
) return NS_ERROR_NOT_AVAILABLE
;
147 RefPtr
<ICustomDestinationList
> jumpListMgr
= mJumpListMgr
;
149 return NS_ERROR_NOT_AVAILABLE
;
152 mAppUserModelId
.Assign(aAppUserModelId
);
153 // MSIX packages explicitly do not support setting the appid from within
154 // the app, as it is set in the package manifest instead.
155 if (!mozilla::widget::WinUtils::HasPackageIdentity()) {
156 jumpListMgr
->SetAppID(mAppUserModelId
.get());
162 NS_IMETHODIMP
JumpListBuilder::GetAvailable(int16_t* aAvailable
) {
165 ReentrantMonitorAutoEnter
lock(mMonitor
);
166 if (mJumpListMgr
) *aAvailable
= true;
171 NS_IMETHODIMP
JumpListBuilder::GetIsListCommitted(bool* aCommit
) {
172 *aCommit
= mHasCommit
;
177 NS_IMETHODIMP
JumpListBuilder::GetMaxListItems(int16_t* aMaxItems
) {
178 ReentrantMonitorAutoEnter
lock(mMonitor
);
179 if (!mJumpListMgr
) return NS_ERROR_NOT_AVAILABLE
;
184 *aMaxItems
= mMaxItems
;
188 RefPtr
<ICustomDestinationList
> jumpListMgr
= mJumpListMgr
;
190 return NS_ERROR_UNEXPECTED
;
193 IObjectArray
* objArray
;
194 if (SUCCEEDED(jumpListMgr
->BeginList(&mMaxItems
, IID_PPV_ARGS(&objArray
)))) {
195 *aMaxItems
= mMaxItems
;
197 if (objArray
) objArray
->Release();
199 jumpListMgr
->AbortList();
205 NS_IMETHODIMP
JumpListBuilder::InitListBuild(JSContext
* aCx
,
206 Promise
** aPromise
) {
207 ReentrantMonitorAutoEnter
lock(mMonitor
);
209 return NS_ERROR_NOT_AVAILABLE
;
212 nsIGlobalObject
* globalObject
= xpc::CurrentNativeGlobal(aCx
);
213 if (NS_WARN_IF(!globalObject
)) {
214 return NS_ERROR_FAILURE
;
218 RefPtr
<Promise
> promise
= Promise::Create(globalObject
, result
);
219 if (NS_WARN_IF(result
.Failed())) {
220 return result
.StealNSResult();
223 nsCOMPtr
<nsIRunnable
> runnable
=
224 NewRunnableMethod
<StoreCopyPassByRRef
<RefPtr
<Promise
>>>(
225 "InitListBuild", this, &JumpListBuilder::DoInitListBuild
, promise
);
226 nsresult rv
= mIOThread
->Dispatch(runnable
, NS_DISPATCH_NORMAL
);
227 if (NS_WARN_IF(NS_FAILED(rv
))) {
231 promise
.forget(aPromise
);
235 void JumpListBuilder::DoInitListBuild(RefPtr
<Promise
>&& aPromise
) {
236 // Since we're invoking COM interfaces to talk to the shell on a background
237 // thread, we need to be running inside a multithreaded apartment.
238 mscom::MTARegion mta
;
239 MOZ_ASSERT(mta
.IsValid());
241 ReentrantMonitorAutoEnter
lock(mMonitor
);
242 MOZ_ASSERT(mJumpListMgr
);
248 HRESULT hr
= E_UNEXPECTED
;
249 auto errorHandler
= MakeScopeExit([&aPromise
, &hr
]() {
254 NS_DispatchToMainThread(NS_NewRunnableFunction(
255 "InitListBuildReject", [promise
= std::move(aPromise
)]() {
256 promise
->MaybeReject(NS_ERROR_FAILURE
);
260 RefPtr
<ICustomDestinationList
> jumpListMgr
= mJumpListMgr
;
265 nsTArray
<nsString
> urisToRemove
;
266 RefPtr
<IObjectArray
> objArray
;
267 hr
= jumpListMgr
->BeginList(
269 IID_PPV_ARGS(static_cast<IObjectArray
**>(getter_AddRefs(objArray
))));
274 // The returned objArray of removed items are for manually removed items.
275 // This does not return items which are removed because they were previously
276 // part of the jump list but are no longer part of the jump list.
277 sBuildingList
= true;
278 RemoveIconCacheAndGetJumplistShortcutURIs(objArray
, urisToRemove
);
280 NS_DispatchToMainThread(NS_NewRunnableFunction(
281 "InitListBuildResolve", [urisToRemove
= std::move(urisToRemove
),
282 promise
= std::move(aPromise
)]() {
283 promise
->MaybeResolve(urisToRemove
);
287 // Ensures that we have no old ICO files left in the jump list cache
288 nsresult
JumpListBuilder::RemoveIconCacheForAllItems() {
289 // Construct the path of our jump list cache
290 nsCOMPtr
<nsIFile
> jumpListCacheDir
;
292 NS_GetSpecialDirectory("ProfLDS", getter_AddRefs(jumpListCacheDir
));
293 NS_ENSURE_SUCCESS(rv
, rv
);
294 rv
= jumpListCacheDir
->AppendNative(
295 nsDependentCString(mozilla::widget::FaviconHelper::kJumpListCacheDir
));
296 NS_ENSURE_SUCCESS(rv
, rv
);
298 nsCOMPtr
<nsIDirectoryEnumerator
> entries
;
299 rv
= jumpListCacheDir
->GetDirectoryEntries(getter_AddRefs(entries
));
300 NS_ENSURE_SUCCESS(rv
, rv
);
302 // Loop through each directory entry and remove all ICO files found
304 nsCOMPtr
<nsIFile
> currFile
;
305 if (NS_FAILED(entries
->GetNextFile(getter_AddRefs(currFile
))) || !currFile
)
309 if (NS_FAILED(currFile
->GetPath(path
))) continue;
311 if (StringTail(path
, 4).LowerCaseEqualsASCII(".ico")) {
312 // Check if the cached ICO file exists
314 if (NS_FAILED(currFile
->Exists(&exists
)) || !exists
) continue;
316 // We found an ICO file that exists, so we should remove it
317 currFile
->Remove(false);
324 NS_IMETHODIMP
JumpListBuilder::AddListToBuild(int16_t aCatType
, nsIArray
* items
,
325 const nsAString
& catName
,
331 ReentrantMonitorAutoEnter
lock(mMonitor
);
332 if (!mJumpListMgr
) return NS_ERROR_NOT_AVAILABLE
;
334 RefPtr
<ICustomDestinationList
> jumpListMgr
= mJumpListMgr
;
336 return NS_ERROR_UNEXPECTED
;
340 case nsIJumpListBuilder::JUMPLIST_CATEGORY_TASKS
: {
341 NS_ENSURE_ARG_POINTER(items
);
344 RefPtr
<IObjectCollection
> collection
;
345 hr
= CoCreateInstance(CLSID_EnumerableObjectCollection
, nullptr,
346 CLSCTX_INPROC_SERVER
, IID_IObjectCollection
,
347 getter_AddRefs(collection
));
348 if (FAILED(hr
)) return NS_ERROR_UNEXPECTED
;
352 items
->GetLength(&length
);
353 for (uint32_t i
= 0; i
< length
; ++i
) {
354 nsCOMPtr
<nsIJumpListItem
> item
= do_QueryElementAt(items
, i
);
356 // Check for separators
357 if (IsSeparator(item
)) {
358 RefPtr
<IShellLinkW
> link
;
359 rv
= JumpListSeparator::GetSeparator(link
);
360 if (NS_FAILED(rv
)) return rv
;
361 collection
->AddObject(link
);
364 // These should all be ShellLinks
365 RefPtr
<IShellLinkW
> link
;
366 rv
= JumpListShortcut::GetShellLink(item
, link
, mIOThread
);
367 if (NS_FAILED(rv
)) return rv
;
368 collection
->AddObject(link
);
371 // We need IObjectArray to submit
372 RefPtr
<IObjectArray
> pArray
;
373 hr
= collection
->QueryInterface(IID_IObjectArray
, getter_AddRefs(pArray
));
374 if (FAILED(hr
)) return NS_ERROR_UNEXPECTED
;
377 hr
= jumpListMgr
->AddUserTasks(pArray
);
378 if (SUCCEEDED(hr
)) *_retval
= true;
381 case nsIJumpListBuilder::JUMPLIST_CATEGORY_RECENT
: {
382 if (SUCCEEDED(jumpListMgr
->AppendKnownCategory(KDC_RECENT
)))
386 case nsIJumpListBuilder::JUMPLIST_CATEGORY_FREQUENT
: {
387 if (SUCCEEDED(jumpListMgr
->AppendKnownCategory(KDC_FREQUENT
)))
391 case nsIJumpListBuilder::JUMPLIST_CATEGORY_CUSTOMLIST
: {
392 NS_ENSURE_ARG_POINTER(items
);
394 if (catName
.IsEmpty()) return NS_ERROR_INVALID_ARG
;
397 RefPtr
<IObjectCollection
> collection
;
398 hr
= CoCreateInstance(CLSID_EnumerableObjectCollection
, nullptr,
399 CLSCTX_INPROC_SERVER
, IID_IObjectCollection
,
400 getter_AddRefs(collection
));
401 if (FAILED(hr
)) return NS_ERROR_UNEXPECTED
;
404 items
->GetLength(&length
);
405 for (uint32_t i
= 0; i
< length
; ++i
) {
406 nsCOMPtr
<nsIJumpListItem
> item
= do_QueryElementAt(items
, i
);
409 if (NS_FAILED(item
->GetType(&type
))) continue;
411 case nsIJumpListItem::JUMPLIST_ITEM_SEPARATOR
: {
412 RefPtr
<IShellLinkW
> shellItem
;
413 rv
= JumpListSeparator::GetSeparator(shellItem
);
414 if (NS_FAILED(rv
)) return rv
;
415 collection
->AddObject(shellItem
);
417 case nsIJumpListItem::JUMPLIST_ITEM_LINK
: {
418 RefPtr
<IShellItem2
> shellItem
;
419 rv
= JumpListLink::GetShellItem(item
, shellItem
);
420 if (NS_FAILED(rv
)) return rv
;
421 collection
->AddObject(shellItem
);
423 case nsIJumpListItem::JUMPLIST_ITEM_SHORTCUT
: {
424 RefPtr
<IShellLinkW
> shellItem
;
425 rv
= JumpListShortcut::GetShellLink(item
, shellItem
, mIOThread
);
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
JumpListBuilder::AbortListBuild() {
454 ReentrantMonitorAutoEnter
lock(mMonitor
);
455 if (!mJumpListMgr
) return NS_ERROR_NOT_AVAILABLE
;
457 RefPtr
<ICustomDestinationList
> jumpListMgr
= mJumpListMgr
;
459 return NS_ERROR_UNEXPECTED
;
462 jumpListMgr
->AbortList();
463 sBuildingList
= false;
468 NS_IMETHODIMP
JumpListBuilder::CommitListBuild(
469 nsIJumpListCommittedCallback
* 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 "JumpListBuilder::DoCommitListBuild", this,
482 &JumpListBuilder::DoCommitListBuild
, std::move(callback
));
483 Unused
<< mIOThread
->Dispatch(event
, NS_DISPATCH_NORMAL
);
488 void JumpListBuilder::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
;
511 hr
= jumpListMgr
->CommitList();
512 sBuildingList
= false;
519 NS_IMETHODIMP
JumpListBuilder::DeleteActiveList(bool* _retval
) {
522 ReentrantMonitorAutoEnter
lock(mMonitor
);
523 if (!mJumpListMgr
) return NS_ERROR_NOT_AVAILABLE
;
529 RefPtr
<ICustomDestinationList
> jumpListMgr
= mJumpListMgr
;
531 return NS_ERROR_UNEXPECTED
;
534 if (SUCCEEDED(jumpListMgr
->DeleteList(mAppUserModelId
.get()))) {
543 bool JumpListBuilder::IsSeparator(nsCOMPtr
<nsIJumpListItem
>& item
) {
545 item
->GetType(&type
);
546 if (NS_FAILED(item
->GetType(&type
))) return false;
548 if (type
== nsIJumpListItem::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 JumpListBuilder::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 JumpListBuilder::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
JumpListBuilder::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