gecko-dev/widget/windows/JumpListBuilder.cpp
Wei-Cheng Pan fafefc69a1 Bug 1354143 - Commit jump list on lazy idle thread. r=florian,jimm
Since committing will do IO on the main thread, it would be better to do it on
an idle thread instead. We have to change JavaScript code too because now the
API is asynchrous.

This patch also updates its xpcshell test.

Now mozilla::widget::AsyncDeleteAllFaviconsFromDisk will get profile directory
on the main thread to prevent it happens on off-main-threads, thus prevents
off-main-thread assertion.

MozReview-Commit-ID: CWcR0B2BC3n

--HG--
extra : rebase_source : 3685a07f9f4476bc94bdf92937734b78fb3fe309
2017-05-24 16:38:57 +08:00

620 lines
17 KiB
C++

/* -*- 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 "JumpListBuilder.h"
#include "nsError.h"
#include "nsCOMPtr.h"
#include "nsServiceManagerUtils.h"
#include "nsString.h"
#include "nsArrayUtils.h"
#include "nsIMutableArray.h"
#include "nsWidgetsCID.h"
#include "WinTaskbar.h"
#include "nsDirectoryServiceUtils.h"
#include "nsISimpleEnumerator.h"
#include "mozilla/Preferences.h"
#include "nsStringStream.h"
#include "nsThreadUtils.h"
#include "mozilla/LazyIdleThread.h"
#include "nsIObserverService.h"
#include "mozilla/Unused.h"
#include "WinUtils.h"
// 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
namespace mozilla {
namespace widget {
static NS_DEFINE_CID(kJumpListItemCID, NS_WIN_JUMPLISTITEM_CID);
static NS_DEFINE_CID(kJumpListLinkCID, NS_WIN_JUMPLISTLINK_CID);
static NS_DEFINE_CID(kJumpListShortcutCID, NS_WIN_JUMPLISTSHORTCUT_CID);
// defined in WinTaskbar.cpp
extern const wchar_t *gMozillaJumpListIDGeneric;
Atomic<bool> JumpListBuilder::sBuildingList(false);
const char kPrefTaskbarEnabled[] = "browser.taskbar.lists.enabled";
NS_IMPL_ISUPPORTS(JumpListBuilder, nsIJumpListBuilder, nsIObserver)
#define TOPIC_PROFILE_BEFORE_CHANGE "profile-before-change"
#define TOPIC_CLEAR_PRIVATE_DATA "clear-private-data"
namespace detail {
class DoneCommitListBuildCallback : public nsIRunnable
{
NS_DECL_THREADSAFE_ISUPPORTS
public:
DoneCommitListBuildCallback(nsIJumpListCommittedCallback* aCallback,
JumpListBuilder* aBuilder)
: mCallback(aCallback)
, mBuilder(aBuilder)
, mResult(false)
{
}
NS_IMETHOD Run() override
{
MOZ_ASSERT(NS_IsMainThread());
if (mCallback) {
Unused << mCallback->Done(mResult);
}
// Ensure we are releasing on the main thread.
Destroy();
return NS_OK;
}
void SetResult(bool aResult)
{
mResult = aResult;
}
private:
~DoneCommitListBuildCallback()
{
// Destructor does not always call on the main thread.
MOZ_ASSERT(!mCallback);
MOZ_ASSERT(!mBuilder);
}
void Destroy()
{
MOZ_ASSERT(NS_IsMainThread());
mCallback = nullptr;
mBuilder = nullptr;
}
// These two references MUST be released on the main thread.
RefPtr<nsIJumpListCommittedCallback> mCallback;
RefPtr<JumpListBuilder> mBuilder;
bool mResult;
};
NS_IMPL_ISUPPORTS(DoneCommitListBuildCallback, nsIRunnable);
} // namespace detail
JumpListBuilder::JumpListBuilder() :
mMaxItems(0),
mHasCommit(false)
{
::CoInitialize(nullptr);
CoCreateInstance(CLSID_DestinationList, nullptr, CLSCTX_INPROC_SERVER,
IID_ICustomDestinationList, getter_AddRefs(mJumpListMgr));
// Make a lazy thread for any IO
mIOThread = new LazyIdleThread(DEFAULT_THREAD_TIMEOUT_MS,
NS_LITERAL_CSTRING("Jump List"),
LazyIdleThread::ManualShutdown);
Preferences::AddStrongObserver(this, kPrefTaskbarEnabled);
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);
}
}
JumpListBuilder::~JumpListBuilder()
{
Preferences::RemoveObserver(this, kPrefTaskbarEnabled);
mJumpListMgr = nullptr;
::CoUninitialize();
}
NS_IMETHODIMP JumpListBuilder::GetAvailable(int16_t *aAvailable)
{
*aAvailable = false;
if (mJumpListMgr)
*aAvailable = true;
return NS_OK;
}
NS_IMETHODIMP JumpListBuilder::GetIsListCommitted(bool *aCommit)
{
*aCommit = mHasCommit;
return NS_OK;
}
NS_IMETHODIMP JumpListBuilder::GetMaxListItems(int16_t *aMaxItems)
{
if (!mJumpListMgr)
return NS_ERROR_NOT_AVAILABLE;
*aMaxItems = 0;
if (sBuildingList) {
*aMaxItems = mMaxItems;
return NS_OK;
}
IObjectArray *objArray;
if (SUCCEEDED(mJumpListMgr->BeginList(&mMaxItems, IID_PPV_ARGS(&objArray)))) {
*aMaxItems = mMaxItems;
if (objArray)
objArray->Release();
mJumpListMgr->AbortList();
}
return NS_OK;
}
NS_IMETHODIMP JumpListBuilder::InitListBuild(nsIMutableArray *removedItems, bool *_retval)
{
NS_ENSURE_ARG_POINTER(removedItems);
*_retval = false;
if (!mJumpListMgr)
return NS_ERROR_NOT_AVAILABLE;
if(sBuildingList)
AbortListBuild();
IObjectArray *objArray;
// The returned objArray of removed items are for manually removed items.
// This does not return items which are removed because they were previously
// part of the jump list but are no longer part of the jump list.
if (SUCCEEDED(mJumpListMgr->BeginList(&mMaxItems, IID_PPV_ARGS(&objArray)))) {
if (objArray) {
TransferIObjectArrayToIMutableArray(objArray, removedItems);
objArray->Release();
}
RemoveIconCacheForItems(removedItems);
sBuildingList = true;
*_retval = true;
return NS_OK;
}
return NS_OK;
}
// Ensures that we don't have old ICO files that aren't in our jump lists
// anymore left over in the cache.
nsresult JumpListBuilder::RemoveIconCacheForItems(nsIMutableArray *items)
{
NS_ENSURE_ARG_POINTER(items);
nsresult rv;
uint32_t length;
items->GetLength(&length);
for (uint32_t i = 0; i < length; ++i) {
//Obtain an IJumpListItem and get the type
nsCOMPtr<nsIJumpListItem> item = do_QueryElementAt(items, i);
if (!item) {
continue;
}
int16_t type;
if (NS_FAILED(item->GetType(&type))) {
continue;
}
// If the item is a shortcut, remove its associated icon if any
if (type == nsIJumpListItem::JUMPLIST_ITEM_SHORTCUT) {
nsCOMPtr<nsIJumpListShortcut> shortcut = do_QueryInterface(item);
if (shortcut) {
nsCOMPtr<nsIURI> uri;
rv = shortcut->GetFaviconPageUri(getter_AddRefs(uri));
if (NS_SUCCEEDED(rv) && uri) {
// The local file path is stored inside the nsIURI
// Get the nsIURI spec which stores the local path for the icon to remove
nsAutoCString spec;
nsresult rv = uri->GetSpec(spec);
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIRunnable> event
= new mozilla::widget::AsyncDeleteIconFromDisk(NS_ConvertUTF8toUTF16(spec));
mIOThread->Dispatch(event, NS_DISPATCH_NORMAL);
// The shortcut was generated from an IShellLinkW so IShellLinkW can
// only tell us what the original icon is and not the URI.
// So this field was used only temporarily as the actual icon file
// path. It should be cleared.
shortcut->SetFaviconPageUri(nullptr);
}
}
}
} // end for
return NS_OK;
}
// Ensures that we have no old ICO files left in the jump list cache
nsresult JumpListBuilder::RemoveIconCacheForAllItems()
{
// Construct the path of our jump list cache
nsCOMPtr<nsIFile> jumpListCacheDir;
nsresult rv = NS_GetSpecialDirectory("ProfLDS",
getter_AddRefs(jumpListCacheDir));
NS_ENSURE_SUCCESS(rv, rv);
rv = jumpListCacheDir->AppendNative(nsDependentCString(
mozilla::widget::FaviconHelper::kJumpListCacheDir));
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsISimpleEnumerator> entries;
rv = jumpListCacheDir->GetDirectoryEntries(getter_AddRefs(entries));
NS_ENSURE_SUCCESS(rv, rv);
// Loop through each directory entry and remove all ICO files found
do {
bool hasMore = false;
if (NS_FAILED(entries->HasMoreElements(&hasMore)) || !hasMore)
break;
nsCOMPtr<nsISupports> supp;
if (NS_FAILED(entries->GetNext(getter_AddRefs(supp))))
break;
nsCOMPtr<nsIFile> currFile(do_QueryInterface(supp));
nsAutoString path;
if (NS_FAILED(currFile->GetPath(path)))
continue;
if (StringTail(path, 4).LowerCaseEqualsASCII(".ico")) {
// Check if the cached ICO file exists
bool exists;
if (NS_FAILED(currFile->Exists(&exists)) || !exists)
continue;
// We found an ICO file that exists, so we should remove it
currFile->Remove(false);
}
} while(true);
return NS_OK;
}
NS_IMETHODIMP JumpListBuilder::AddListToBuild(int16_t aCatType, nsIArray *items, const nsAString &catName, bool *_retval)
{
nsresult rv;
*_retval = false;
if (!mJumpListMgr)
return NS_ERROR_NOT_AVAILABLE;
switch(aCatType) {
case nsIJumpListBuilder::JUMPLIST_CATEGORY_TASKS:
{
NS_ENSURE_ARG_POINTER(items);
HRESULT hr;
RefPtr<IObjectCollection> collection;
hr = CoCreateInstance(CLSID_EnumerableObjectCollection, nullptr,
CLSCTX_INPROC_SERVER, IID_IObjectCollection,
getter_AddRefs(collection));
if (FAILED(hr))
return NS_ERROR_UNEXPECTED;
// Build the list
uint32_t length;
items->GetLength(&length);
for (uint32_t i = 0; i < length; ++i) {
nsCOMPtr<nsIJumpListItem> item = do_QueryElementAt(items, i);
if (!item)
continue;
// Check for separators
if (IsSeparator(item)) {
RefPtr<IShellLinkW> link;
rv = JumpListSeparator::GetSeparator(link);
if (NS_FAILED(rv))
return rv;
collection->AddObject(link);
continue;
}
// These should all be ShellLinks
RefPtr<IShellLinkW> link;
rv = JumpListShortcut::GetShellLink(item, link, mIOThread);
if (NS_FAILED(rv))
return rv;
collection->AddObject(link);
}
// We need IObjectArray to submit
RefPtr<IObjectArray> pArray;
hr = collection->QueryInterface(IID_IObjectArray, getter_AddRefs(pArray));
if (FAILED(hr))
return NS_ERROR_UNEXPECTED;
// Add the tasks
hr = mJumpListMgr->AddUserTasks(pArray);
if (SUCCEEDED(hr))
*_retval = true;
return NS_OK;
}
break;
case nsIJumpListBuilder::JUMPLIST_CATEGORY_RECENT:
{
if (SUCCEEDED(mJumpListMgr->AppendKnownCategory(KDC_RECENT)))
*_retval = true;
return NS_OK;
}
break;
case nsIJumpListBuilder::JUMPLIST_CATEGORY_FREQUENT:
{
if (SUCCEEDED(mJumpListMgr->AppendKnownCategory(KDC_FREQUENT)))
*_retval = true;
return NS_OK;
}
break;
case nsIJumpListBuilder::JUMPLIST_CATEGORY_CUSTOMLIST:
{
NS_ENSURE_ARG_POINTER(items);
if (catName.IsEmpty())
return NS_ERROR_INVALID_ARG;
HRESULT hr;
RefPtr<IObjectCollection> collection;
hr = CoCreateInstance(CLSID_EnumerableObjectCollection, nullptr,
CLSCTX_INPROC_SERVER, IID_IObjectCollection,
getter_AddRefs(collection));
if (FAILED(hr))
return NS_ERROR_UNEXPECTED;
uint32_t length;
items->GetLength(&length);
for (uint32_t i = 0; i < length; ++i) {
nsCOMPtr<nsIJumpListItem> item = do_QueryElementAt(items, i);
if (!item)
continue;
int16_t type;
if (NS_FAILED(item->GetType(&type)))
continue;
switch(type) {
case nsIJumpListItem::JUMPLIST_ITEM_SEPARATOR:
{
RefPtr<IShellLinkW> shellItem;
rv = JumpListSeparator::GetSeparator(shellItem);
if (NS_FAILED(rv))
return rv;
collection->AddObject(shellItem);
}
break;
case nsIJumpListItem::JUMPLIST_ITEM_LINK:
{
RefPtr<IShellItem2> shellItem;
rv = JumpListLink::GetShellItem(item, shellItem);
if (NS_FAILED(rv))
return rv;
collection->AddObject(shellItem);
}
break;
case nsIJumpListItem::JUMPLIST_ITEM_SHORTCUT:
{
RefPtr<IShellLinkW> shellItem;
rv = JumpListShortcut::GetShellLink(item, shellItem, mIOThread);
if (NS_FAILED(rv))
return rv;
collection->AddObject(shellItem);
}
break;
}
}
// We need IObjectArray to submit
RefPtr<IObjectArray> pArray;
hr = collection->QueryInterface(IID_IObjectArray, (LPVOID*)&pArray);
if (FAILED(hr))
return NS_ERROR_UNEXPECTED;
// Add the tasks
hr = mJumpListMgr->AppendCategory(reinterpret_cast<const wchar_t*>(catName.BeginReading()), pArray);
if (SUCCEEDED(hr))
*_retval = true;
// Get rid of the old icons
nsCOMPtr<nsIRunnable> event =
new mozilla::widget::AsyncDeleteAllFaviconsFromDisk(true);
mIOThread->Dispatch(event, NS_DISPATCH_NORMAL);
return NS_OK;
}
break;
}
return NS_OK;
}
NS_IMETHODIMP JumpListBuilder::AbortListBuild()
{
if (!mJumpListMgr)
return NS_ERROR_NOT_AVAILABLE;
mJumpListMgr->AbortList();
sBuildingList = false;
return NS_OK;
}
NS_IMETHODIMP JumpListBuilder::CommitListBuild(nsIJumpListCommittedCallback* aCallback)
{
if (!mJumpListMgr)
return NS_ERROR_NOT_AVAILABLE;
// Also holds a strong reference to this to prevent use-after-free.
RefPtr<detail::DoneCommitListBuildCallback> callback =
new detail::DoneCommitListBuildCallback(aCallback, this);
// The builder has a strong reference in the callback already, so we do not
// need to do it for this runnable again.
RefPtr<nsIRunnable> event =
NewNonOwningRunnableMethod<RefPtr<detail::DoneCommitListBuildCallback>>
("JumpListBuilder::DoCommitListBuild", this,
&JumpListBuilder::DoCommitListBuild, Move(callback));
Unused << mIOThread->Dispatch(event, NS_DISPATCH_NORMAL);
return NS_OK;
}
void JumpListBuilder::DoCommitListBuild(RefPtr<detail::DoneCommitListBuildCallback> aCallback)
{
MOZ_ASSERT(mJumpListMgr);
MOZ_ASSERT(aCallback);
HRESULT hr = mJumpListMgr->CommitList();
sBuildingList = false;
if (SUCCEEDED(hr)) {
mHasCommit = true;
}
// XXX We might want some specific error data here.
aCallback->SetResult(SUCCEEDED(hr));
Unused << NS_DispatchToMainThread(aCallback);
}
NS_IMETHODIMP JumpListBuilder::DeleteActiveList(bool *_retval)
{
*_retval = false;
if (!mJumpListMgr)
return NS_ERROR_NOT_AVAILABLE;
if(sBuildingList)
AbortListBuild();
nsAutoString uid;
if (!WinTaskbar::GetAppUserModelID(uid))
return NS_OK;
if (SUCCEEDED(mJumpListMgr->DeleteList(uid.get())))
*_retval = true;
return NS_OK;
}
/* internal */
bool JumpListBuilder::IsSeparator(nsCOMPtr<nsIJumpListItem>& item)
{
int16_t type;
item->GetType(&type);
if (NS_FAILED(item->GetType(&type)))
return false;
if (type == nsIJumpListItem::JUMPLIST_ITEM_SEPARATOR)
return true;
return false;
}
// TransferIObjectArrayToIMutableArray - used in converting removed items
// to our objects.
nsresult JumpListBuilder::TransferIObjectArrayToIMutableArray(IObjectArray *objArray, nsIMutableArray *removedItems)
{
NS_ENSURE_ARG_POINTER(objArray);
NS_ENSURE_ARG_POINTER(removedItems);
nsresult rv;
uint32_t count = 0;
objArray->GetCount(&count);
nsCOMPtr<nsIJumpListItem> item;
for (uint32_t idx = 0; idx < count; idx++) {
IShellLinkW * pLink = nullptr;
IShellItem * pItem = nullptr;
if (SUCCEEDED(objArray->GetAt(idx, IID_IShellLinkW, (LPVOID*)&pLink))) {
nsCOMPtr<nsIJumpListShortcut> shortcut =
do_CreateInstance(kJumpListShortcutCID, &rv);
if (NS_FAILED(rv))
return NS_ERROR_UNEXPECTED;
rv = JumpListShortcut::GetJumpListShortcut(pLink, shortcut);
item = do_QueryInterface(shortcut);
}
else if (SUCCEEDED(objArray->GetAt(idx, IID_IShellItem, (LPVOID*)&pItem))) {
nsCOMPtr<nsIJumpListLink> link =
do_CreateInstance(kJumpListLinkCID, &rv);
if (NS_FAILED(rv))
return NS_ERROR_UNEXPECTED;
rv = JumpListLink::GetJumpListLink(pItem, link);
item = do_QueryInterface(link);
}
if (pLink)
pLink->Release();
if (pItem)
pItem->Release();
if (NS_SUCCEEDED(rv)) {
removedItems->AppendElement(item, false);
}
}
return NS_OK;
}
NS_IMETHODIMP JumpListBuilder::Observe(nsISupports* aSubject,
const char* aTopic,
const char16_t* aData)
{
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);
}
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