File indexing completed on 2024-11-10 04:40:31
0001 /* 0002 SPDX-FileCopyrightText: 2009 Constantin Berzan <exit3219@gmail.com> 0003 0004 SPDX-License-Identifier: LGPL-2.0-or-later 0005 */ 0006 0007 #include "specialcollectionshelperjobs_p.h" 0008 0009 #include "servermanager.h" 0010 #include "specialcollectionattribute.h" 0011 #include <QDBusConnection> 0012 0013 #include "agentinstance.h" 0014 #include "agentinstancecreatejob.h" 0015 #include "agentmanager.h" 0016 #include "collectionfetchjob.h" 0017 #include "collectionfetchscope.h" 0018 #include "collectionmodifyjob.h" 0019 #include "entitydisplayattribute.h" 0020 #include "resourcesynchronizationjob.h" 0021 0022 #include "akonadicore_debug.h" 0023 0024 #include <KCoreConfigSkeleton> 0025 #include <KLocalizedString> 0026 0027 #include <QDBusConnectionInterface> 0028 #include <QDBusInterface> 0029 #include <QDBusServiceWatcher> 0030 #include <QMetaMethod> 0031 #include <QTime> 0032 #include <QTimer> 0033 0034 #define LOCK_WAIT_TIMEOUT_SECONDS 30 0035 0036 using namespace Akonadi; 0037 0038 // convenient methods to get/set the default resource id 0039 static void setDefaultResourceId(KCoreConfigSkeleton *settings, const QString &value) 0040 { 0041 KConfigSkeletonItem *item = settings->findItem(QStringLiteral("DefaultResourceId")); 0042 Q_ASSERT(item); 0043 item->setProperty(value); 0044 } 0045 0046 static QString defaultResourceId(KCoreConfigSkeleton *settings) 0047 { 0048 const KConfigSkeletonItem *item = settings->findItem(QStringLiteral("DefaultResourceId")); 0049 Q_ASSERT(item); 0050 return item->property().toString(); 0051 } 0052 0053 static QString dbusServiceName() 0054 { 0055 const QString service = QStringLiteral("org.kde.pim.SpecialCollections"); 0056 if (ServerManager::hasInstanceIdentifier()) { 0057 return service + ServerManager::instanceIdentifier(); 0058 } 0059 return service; 0060 } 0061 0062 static QMetaType::Type argumentType(const QMetaObject *metaObject, const QString &method) 0063 { 0064 QMetaMethod metaMethod; 0065 for (int i = 0; i < metaObject->methodCount(); ++i) { 0066 const QString signature = QString::fromLatin1(metaObject->method(i).methodSignature()); 0067 if (signature.startsWith(method)) { 0068 metaMethod = metaObject->method(i); 0069 } 0070 } 0071 0072 if (metaMethod.methodSignature().isEmpty()) { 0073 return QMetaType::UnknownType; 0074 } 0075 0076 const QList<QByteArray> argTypes = metaMethod.parameterTypes(); 0077 if (argTypes.count() != 1) { 0078 return QMetaType::UnknownType; 0079 } 0080 0081 return static_cast<QMetaType::Type>(QMetaType::fromName(argTypes.first().constData()).id()); 0082 } 0083 0084 // ===================== ResourceScanJob ============================ 0085 0086 /** 0087 @internal 0088 */ 0089 class Akonadi::ResourceScanJobPrivate 0090 { 0091 public: 0092 ResourceScanJobPrivate(KCoreConfigSkeleton *settings, ResourceScanJob *qq); 0093 0094 void fetchResult(KJob *job); // slot 0095 0096 ResourceScanJob *const q; 0097 0098 // Input: 0099 QString mResourceId; 0100 KCoreConfigSkeleton *mSettings = nullptr; 0101 0102 // Output: 0103 Collection mRootCollection; 0104 Collection::List mSpecialCollections; 0105 }; 0106 0107 ResourceScanJobPrivate::ResourceScanJobPrivate(KCoreConfigSkeleton *settings, ResourceScanJob *qq) 0108 : q(qq) 0109 , mSettings(settings) 0110 { 0111 } 0112 0113 void ResourceScanJobPrivate::fetchResult(KJob *job) 0114 { 0115 if (job->error()) { 0116 qCWarning(AKONADICORE_LOG) << job->errorText(); 0117 return; 0118 } 0119 0120 auto fetchJob = qobject_cast<CollectionFetchJob *>(job); 0121 Q_ASSERT(fetchJob); 0122 0123 Q_ASSERT(!mRootCollection.isValid()); 0124 Q_ASSERT(mSpecialCollections.isEmpty()); 0125 const Akonadi::Collection::List lstCols = fetchJob->collections(); 0126 for (const Collection &collection : lstCols) { 0127 if (collection.parentCollection() == Collection::root()) { 0128 if (mRootCollection.isValid()) { 0129 qCWarning(AKONADICORE_LOG) << "Resource has more than one root collection. I don't know what to do."; 0130 } else { 0131 mRootCollection = collection; 0132 } 0133 } 0134 0135 if (collection.hasAttribute<SpecialCollectionAttribute>()) { 0136 mSpecialCollections.append(collection); 0137 } 0138 } 0139 0140 qCDebug(AKONADICORE_LOG) << "Fetched root collection" << mRootCollection.id() << "and" << mSpecialCollections.count() << "local folders" 0141 << "(total" << fetchJob->collections().count() << "collections)."; 0142 0143 if (!mRootCollection.isValid()) { 0144 q->setError(ResourceScanJob::Unknown); 0145 q->setErrorText(i18n("Could not fetch root collection of resource %1.", mResourceId)); 0146 q->emitResult(); 0147 return; 0148 } 0149 0150 // We are done! 0151 q->emitResult(); 0152 } 0153 0154 ResourceScanJob::ResourceScanJob(const QString &resourceId, KCoreConfigSkeleton *settings, QObject *parent) 0155 : Job(parent) 0156 , d(new ResourceScanJobPrivate(settings, this)) 0157 { 0158 setResourceId(resourceId); 0159 } 0160 0161 ResourceScanJob::~ResourceScanJob() = default; 0162 0163 QString ResourceScanJob::resourceId() const 0164 { 0165 return d->mResourceId; 0166 } 0167 0168 void ResourceScanJob::setResourceId(const QString &resourceId) 0169 { 0170 d->mResourceId = resourceId; 0171 } 0172 0173 Akonadi::Collection ResourceScanJob::rootResourceCollection() const 0174 { 0175 return d->mRootCollection; 0176 } 0177 0178 Akonadi::Collection::List ResourceScanJob::specialCollections() const 0179 { 0180 return d->mSpecialCollections; 0181 } 0182 0183 void ResourceScanJob::doStart() 0184 { 0185 if (d->mResourceId.isEmpty()) { 0186 if (!qobject_cast<DefaultResourceJob *>(this)) { 0187 qCCritical(AKONADICORE_LOG) << "No resource ID given."; 0188 setError(Job::Unknown); 0189 setErrorText(i18n("No resource ID given.")); 0190 } 0191 emitResult(); 0192 return; 0193 } 0194 0195 auto fetchJob = new CollectionFetchJob(Collection::root(), CollectionFetchJob::Recursive, this); 0196 fetchJob->fetchScope().setResource(d->mResourceId); 0197 fetchJob->fetchScope().setIncludeStatistics(true); 0198 fetchJob->fetchScope().setListFilter(CollectionFetchScope::Display); 0199 connect(fetchJob, &CollectionFetchJob::result, this, [this](KJob *job) { 0200 d->fetchResult(job); 0201 }); 0202 } 0203 0204 // ===================== DefaultResourceJob ============================ 0205 0206 /** 0207 @internal 0208 */ 0209 class Akonadi::DefaultResourceJobPrivate 0210 { 0211 public: 0212 DefaultResourceJobPrivate(KCoreConfigSkeleton *settings, DefaultResourceJob *qq); 0213 0214 void tryFetchResource(); 0215 void resourceCreateResult(KJob *job); // slot 0216 void resourceSyncResult(KJob *job); // slot 0217 void collectionFetchResult(KJob *job); // slot 0218 void collectionModifyResult(KJob *job); // slot 0219 0220 DefaultResourceJob *const q; 0221 KCoreConfigSkeleton *mSettings = nullptr; 0222 QVariantMap mDefaultResourceOptions; 0223 QList<QByteArray> mKnownTypes; 0224 QMap<QByteArray, QString> mNameForTypeMap; 0225 QMap<QByteArray, QString> mIconForTypeMap; 0226 QString mDefaultResourceType; 0227 int mPendingModifyJobs = 0; 0228 bool mResourceWasPreexisting = true; 0229 }; 0230 0231 DefaultResourceJobPrivate::DefaultResourceJobPrivate(KCoreConfigSkeleton *settings, DefaultResourceJob *qq) 0232 : q(qq) 0233 , mSettings(settings) 0234 , mPendingModifyJobs(0) 0235 , mResourceWasPreexisting(true /* for safety, so as not to accidentally delete data */) 0236 { 0237 } 0238 0239 void DefaultResourceJobPrivate::tryFetchResource() 0240 { 0241 // Get the resourceId from config. Another instance might have changed it in the meantime. 0242 mSettings->load(); 0243 0244 const QString resourceId = defaultResourceId(mSettings); 0245 0246 qCDebug(AKONADICORE_LOG) << "Read defaultResourceId" << resourceId << "from config."; 0247 0248 const AgentInstance resource = AgentManager::self()->instance(resourceId); 0249 if (resource.isValid()) { 0250 // The resource exists; scan it. 0251 mResourceWasPreexisting = true; 0252 qCDebug(AKONADICORE_LOG) << "Found resource" << resourceId; 0253 q->setResourceId(resourceId); 0254 0255 auto fetchJob = new CollectionFetchJob(Collection::root(), CollectionFetchJob::Recursive, q); 0256 fetchJob->fetchScope().setResource(resourceId); 0257 fetchJob->fetchScope().setIncludeStatistics(true); 0258 q->connect(fetchJob, &CollectionFetchJob::result, q, [this](KJob *job) { 0259 collectionFetchResult(job); 0260 }); 0261 } else { 0262 // Try harder: maybe the default resource has been removed and another one added 0263 // without updating the config file, in this case search for a resource 0264 // of the same type and the default name 0265 const AgentInstance::List resources = AgentManager::self()->instances(); 0266 for (const AgentInstance &resourceInstance : resources) { 0267 if (resourceInstance.type().identifier() == mDefaultResourceType) { 0268 if (resourceInstance.name() == mDefaultResourceOptions.value(QStringLiteral("Name")).toString()) { 0269 // found a matching one... 0270 setDefaultResourceId(mSettings, resourceInstance.identifier()); 0271 mSettings->save(); 0272 mResourceWasPreexisting = true; 0273 qCDebug(AKONADICORE_LOG) << "Found resource" << resourceInstance.identifier(); 0274 q->setResourceId(resourceInstance.identifier()); 0275 q->ResourceScanJob::doStart(); 0276 return; 0277 } 0278 } 0279 } 0280 0281 // Create the resource. 0282 mResourceWasPreexisting = false; 0283 qCDebug(AKONADICORE_LOG) << "Creating maildir resource."; 0284 const AgentType type = AgentManager::self()->type(mDefaultResourceType); 0285 auto job = new AgentInstanceCreateJob(type, q); 0286 QObject::connect(job, &AgentInstanceCreateJob::result, q, [this](KJob *job) { 0287 resourceCreateResult(job); 0288 }); 0289 job->start(); // non-Akonadi::Job 0290 } 0291 } 0292 0293 void DefaultResourceJobPrivate::resourceCreateResult(KJob *job) 0294 { 0295 if (job->error()) { 0296 qCWarning(AKONADICORE_LOG) << job->errorText(); 0297 // fail( i18n( "Failed to create the default resource (%1).", job->errorString() ) ); 0298 q->setError(job->error()); 0299 q->setErrorText(job->errorText()); 0300 q->emitResult(); 0301 return; 0302 } 0303 0304 AgentInstance agent; 0305 0306 // Get the resource instance. 0307 { 0308 auto createJob = qobject_cast<AgentInstanceCreateJob *>(job); 0309 Q_ASSERT(createJob); 0310 agent = createJob->instance(); 0311 setDefaultResourceId(mSettings, agent.identifier()); 0312 qCDebug(AKONADICORE_LOG) << "Created maildir resource with id" << defaultResourceId(mSettings); 0313 } 0314 0315 const QString defaultId = defaultResourceId(mSettings); 0316 0317 // Configure the resource. 0318 { 0319 agent.setName(mDefaultResourceOptions.value(QStringLiteral("Name")).toString()); 0320 0321 const auto service = ServerManager::agentServiceName(ServerManager::Resource, defaultId); 0322 QDBusInterface conf(service, QStringLiteral("/Settings"), QString()); 0323 0324 if (!conf.isValid()) { 0325 q->setError(-1); 0326 q->setErrorText(i18n("Invalid resource identifier '%1'", defaultId)); 0327 q->emitResult(); 0328 return; 0329 } 0330 0331 QMap<QString, QVariant>::const_iterator it = mDefaultResourceOptions.cbegin(); 0332 const QMap<QString, QVariant>::const_iterator itEnd = mDefaultResourceOptions.cend(); 0333 for (; it != itEnd; ++it) { 0334 if (it.key() == QLatin1StringView("Name")) { 0335 continue; 0336 } 0337 0338 const QString methodName = QStringLiteral("set%1").arg(it.key()); 0339 const QMetaType::Type argType = argumentType(conf.metaObject(), methodName); 0340 if (argType == QMetaType::UnknownType) { 0341 q->setError(Job::Unknown); 0342 q->setErrorText(i18n("Failed to configure default resource via D-Bus.")); 0343 q->emitResult(); 0344 return; 0345 } 0346 0347 QDBusReply<void> reply = conf.call(methodName, it.value()); 0348 if (!reply.isValid()) { 0349 q->setError(Job::Unknown); 0350 q->setErrorText(i18n("Failed to configure default resource via D-Bus.")); 0351 q->emitResult(); 0352 return; 0353 } 0354 } 0355 0356 conf.call(QStringLiteral("save")); 0357 0358 agent.reconfigure(); 0359 } 0360 0361 // Sync the resource. 0362 { 0363 auto syncJob = new ResourceSynchronizationJob(agent, q); 0364 QObject::connect(syncJob, &ResourceSynchronizationJob::result, q, [this](KJob *job) { 0365 resourceSyncResult(job); 0366 }); 0367 syncJob->start(); // non-Akonadi 0368 } 0369 } 0370 0371 void DefaultResourceJobPrivate::resourceSyncResult(KJob *job) 0372 { 0373 if (job->error()) { 0374 qCWarning(AKONADICORE_LOG) << job->errorText(); 0375 // fail( i18n( "ResourceSynchronizationJob failed (%1).", job->errorString() ) ); 0376 return; 0377 } 0378 0379 // Fetch the collections of the resource. 0380 qCDebug(AKONADICORE_LOG) << "Fetching maildir collections."; 0381 auto fetchJob = new CollectionFetchJob(Collection::root(), CollectionFetchJob::Recursive, q); 0382 fetchJob->fetchScope().setResource(defaultResourceId(mSettings)); 0383 QObject::connect(fetchJob, &CollectionFetchJob::result, q, [this](KJob *job) { 0384 collectionFetchResult(job); 0385 }); 0386 } 0387 0388 void DefaultResourceJobPrivate::collectionFetchResult(KJob *job) 0389 { 0390 if (job->error()) { 0391 qCWarning(AKONADICORE_LOG) << job->errorText(); 0392 // fail( i18n( "Failed to fetch the root maildir collection (%1).", job->errorString() ) ); 0393 return; 0394 } 0395 0396 auto fetchJob = qobject_cast<CollectionFetchJob *>(job); 0397 Q_ASSERT(fetchJob); 0398 0399 const Collection::List collections = fetchJob->collections(); 0400 qCDebug(AKONADICORE_LOG) << "Fetched" << collections.count() << "collections."; 0401 0402 // Find the root maildir collection. 0403 Collection::List toRecover; 0404 Collection resourceCollection; 0405 for (const Collection &collection : collections) { 0406 if (collection.parentCollection() == Collection::root()) { 0407 resourceCollection = collection; 0408 toRecover.append(collection); 0409 break; 0410 } 0411 } 0412 0413 if (!resourceCollection.isValid()) { 0414 q->setError(Job::Unknown); 0415 q->setErrorText(i18n("Failed to fetch the resource collection.")); 0416 q->emitResult(); 0417 return; 0418 } 0419 0420 // Find all children of the resource collection. 0421 for (const Collection &collection : std::as_const(collections)) { 0422 if (collection.parentCollection() == resourceCollection) { 0423 toRecover.append(collection); 0424 } 0425 } 0426 0427 QHash<QString, QByteArray> typeForName; 0428 for (const QByteArray &type : std::as_const(mKnownTypes)) { 0429 const QString displayName = mNameForTypeMap.value(type); 0430 typeForName[displayName] = type; 0431 } 0432 0433 // These collections have been created by the maildir resource, when it 0434 // found the folders on disk. So give them the necessary attributes now. 0435 Q_ASSERT(mPendingModifyJobs == 0); 0436 for (Collection collection : std::as_const(toRecover)) { 0437 if (collection.hasAttribute<SpecialCollectionAttribute>()) { 0438 continue; 0439 } 0440 0441 // Find the type for the collection. 0442 const QString name = collection.displayName(); 0443 const QByteArray type = typeForName.value(name); 0444 0445 if (!type.isEmpty()) { 0446 qCDebug(AKONADICORE_LOG) << "Recovering collection" << name; 0447 setCollectionAttributes(collection, type, mNameForTypeMap, mIconForTypeMap); 0448 0449 auto modifyJob = new CollectionModifyJob(collection, q); 0450 QObject::connect(modifyJob, &CollectionModifyJob::result, q, [this](KJob *job) { 0451 collectionModifyResult(job); 0452 }); 0453 mPendingModifyJobs++; 0454 } else { 0455 qCDebug(AKONADICORE_LOG) << "Searching for names: " << typeForName.keys(); 0456 qCDebug(AKONADICORE_LOG) << "Unknown collection name" << name << "-- not recovering."; 0457 } 0458 } 0459 0460 if (mPendingModifyJobs == 0) { 0461 // Scan the resource. 0462 q->setResourceId(defaultResourceId(mSettings)); 0463 q->ResourceScanJob::doStart(); 0464 } 0465 } 0466 0467 void DefaultResourceJobPrivate::collectionModifyResult(KJob *job) 0468 { 0469 if (job->error()) { 0470 qCWarning(AKONADICORE_LOG) << job->errorText(); 0471 // fail( i18n( "Failed to modify the root maildir collection (%1).", job->errorString() ) ); 0472 return; 0473 } 0474 0475 Q_ASSERT(mPendingModifyJobs > 0); 0476 mPendingModifyJobs--; 0477 qCDebug(AKONADICORE_LOG) << "pendingModifyJobs now" << mPendingModifyJobs; 0478 if (mPendingModifyJobs == 0) { 0479 // Write the updated config. 0480 qCDebug(AKONADICORE_LOG) << "Writing defaultResourceId" << defaultResourceId(mSettings) << "to config."; 0481 mSettings->save(); 0482 0483 // Scan the resource. 0484 q->setResourceId(defaultResourceId(mSettings)); 0485 q->ResourceScanJob::doStart(); 0486 } 0487 } 0488 0489 DefaultResourceJob::DefaultResourceJob(KCoreConfigSkeleton *settings, QObject *parent) 0490 : ResourceScanJob(QString(), settings, parent) 0491 , d(new DefaultResourceJobPrivate(settings, this)) 0492 { 0493 } 0494 0495 DefaultResourceJob::~DefaultResourceJob() = default; 0496 0497 void DefaultResourceJob::setDefaultResourceType(const QString &type) 0498 { 0499 d->mDefaultResourceType = type; 0500 } 0501 0502 void DefaultResourceJob::setDefaultResourceOptions(const QVariantMap &options) 0503 { 0504 d->mDefaultResourceOptions = options; 0505 } 0506 0507 void DefaultResourceJob::setTypes(const QList<QByteArray> &types) 0508 { 0509 d->mKnownTypes = types; 0510 } 0511 0512 void DefaultResourceJob::setNameForTypeMap(const QMap<QByteArray, QString> &map) 0513 { 0514 d->mNameForTypeMap = map; 0515 } 0516 0517 void DefaultResourceJob::setIconForTypeMap(const QMap<QByteArray, QString> &map) 0518 { 0519 d->mIconForTypeMap = map; 0520 } 0521 0522 void DefaultResourceJob::doStart() 0523 { 0524 d->tryFetchResource(); 0525 } 0526 0527 void DefaultResourceJob::slotResult(KJob *job) 0528 { 0529 if (job->error()) { 0530 qCWarning(AKONADICORE_LOG) << job->errorText(); 0531 // Do some cleanup. 0532 if (!d->mResourceWasPreexisting) { 0533 // We only removed the resource instance if we have created it. 0534 // Otherwise we might lose the user's data. 0535 const AgentInstance resource = AgentManager::self()->instance(defaultResourceId(d->mSettings)); 0536 qCDebug(AKONADICORE_LOG) << "Removing resource" << resource.identifier(); 0537 AgentManager::self()->removeInstance(resource); 0538 } 0539 } 0540 0541 Job::slotResult(job); 0542 } 0543 0544 // ===================== GetLockJob ============================ 0545 0546 class Akonadi::GetLockJobPrivate 0547 { 0548 public: 0549 explicit GetLockJobPrivate(GetLockJob *qq); 0550 0551 void doStart(); // slot 0552 void timeout(); // slot 0553 0554 GetLockJob *const q; 0555 QTimer *mSafetyTimer = nullptr; 0556 }; 0557 0558 GetLockJobPrivate::GetLockJobPrivate(GetLockJob *qq) 0559 : q(qq) 0560 , mSafetyTimer(nullptr) 0561 { 0562 } 0563 0564 void GetLockJobPrivate::doStart() 0565 { 0566 // Just doing registerService() and checking its return value is not sufficient, 0567 // since we may *already* own the name, and then registerService() returns true. 0568 0569 QDBusConnection bus = QDBusConnection::sessionBus(); 0570 const bool alreadyLocked = bus.interface()->isServiceRegistered(dbusServiceName()); 0571 const bool gotIt = bus.registerService(dbusServiceName()); 0572 0573 if (gotIt && !alreadyLocked) { 0574 // qCDebug(AKONADICORE_LOG) << "Got lock immediately."; 0575 q->emitResult(); 0576 } else { 0577 auto watcher = new QDBusServiceWatcher(dbusServiceName(), QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForUnregistration, q); 0578 QObject::connect(watcher, &QDBusServiceWatcher::serviceUnregistered, q, [this]() { 0579 if (QDBusConnection::sessionBus().registerService(dbusServiceName())) { 0580 mSafetyTimer->stop(); 0581 q->emitResult(); 0582 } 0583 }); 0584 0585 mSafetyTimer = new QTimer(q); 0586 mSafetyTimer->setSingleShot(true); 0587 mSafetyTimer->setInterval(LOCK_WAIT_TIMEOUT_SECONDS * 1000); 0588 mSafetyTimer->start(); 0589 QObject::connect(mSafetyTimer, &QTimer::timeout, q, [this]() { 0590 timeout(); 0591 }); 0592 } 0593 } 0594 0595 void GetLockJobPrivate::timeout() 0596 { 0597 qCWarning(AKONADICORE_LOG) << "Timeout trying to get lock. Check who has acquired the name" << dbusServiceName() << "on DBus, using qdbus or qdbusviewer."; 0598 q->setError(Job::Unknown); 0599 q->setErrorText(i18n("Timeout trying to get lock.")); 0600 q->emitResult(); 0601 } 0602 0603 GetLockJob::GetLockJob(QObject *parent) 0604 : KJob(parent) 0605 , d(new GetLockJobPrivate(this)) 0606 { 0607 } 0608 0609 GetLockJob::~GetLockJob() = default; 0610 0611 void GetLockJob::start() 0612 { 0613 QTimer::singleShot(0, this, [this]() { 0614 d->doStart(); 0615 }); 0616 } 0617 0618 void Akonadi::setCollectionAttributes(Akonadi::Collection &collection, 0619 const QByteArray &type, 0620 const QMap<QByteArray, QString> &nameForType, 0621 const QMap<QByteArray, QString> &iconForType) 0622 { 0623 { 0624 auto attr = new EntityDisplayAttribute; 0625 attr->setIconName(iconForType.value(type)); 0626 attr->setDisplayName(nameForType.value(type)); 0627 collection.addAttribute(attr); 0628 } 0629 0630 { 0631 auto attr = new SpecialCollectionAttribute; 0632 attr->setCollectionType(type); 0633 collection.addAttribute(attr); 0634 } 0635 } 0636 0637 bool Akonadi::releaseLock() 0638 { 0639 return QDBusConnection::sessionBus().unregisterService(dbusServiceName()); 0640 } 0641 0642 #include "moc_specialcollectionshelperjobs_p.cpp"