File indexing completed on 2024-05-12 16:59:20
0001 /* 0002 * SPDX-FileCopyrightText: 2010-2016 Ivan Cukic <ivan.cukic@kde.org> 0003 * 0004 * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL 0005 */ 0006 0007 // Self 0008 #include "Activities.h" 0009 #include "Activities_p.h" 0010 #include <kactivities-features.h> 0011 0012 // Qt 0013 #include <QDBusConnection> 0014 #include <QFile> 0015 #include <QMetaObject> 0016 #include <QStandardPaths> 0017 #include <QUuid> 0018 0019 // KDE 0020 #include <kauthorized.h> 0021 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) 0022 #include <kdelibs4migration.h> 0023 #endif 0024 #include <klocalizedstring.h> 0025 #include <ksharedconfig.h> 0026 0027 // Utils 0028 #include <utils/d_ptr_implementation.h> 0029 #include <utils/range.h> 0030 0031 // Local 0032 #include "DebugActivities.h" 0033 #include "activitiesadaptor.h" 0034 #include "common/dbus/common.h" 0035 #include "ksmserver/KSMServer.h" 0036 0037 // Private 0038 #define ACTIVITY_MANAGER_CONFIG_FILE_NAME QStringLiteral("kactivitymanagerdrc") 0039 0040 namespace 0041 { 0042 inline bool nameBasedOrdering(const ActivityInfo &info, const ActivityInfo &other) 0043 { 0044 const auto comp = QString::compare(info.name, other.name, Qt::CaseInsensitive); 0045 return comp < 0 || (comp == 0 && info.id < other.id); 0046 } 0047 } 0048 0049 Activities::Private::KDE4ConfigurationTransitionChecker::KDE4ConfigurationTransitionChecker() 0050 { 0051 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) 0052 // Checking whether we need to transfer the KActivities/KDE4 0053 // configuration file to the new location. 0054 const QString newConfigLocation = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + QLatin1Char('/') + ACTIVITY_MANAGER_CONFIG_FILE_NAME; 0055 0056 if (QFile(newConfigLocation).exists()) { 0057 return; 0058 } 0059 // Testing for kdehome 0060 Kdelibs4Migration migration; 0061 if (!migration.kdeHomeFound()) { 0062 return; 0063 } 0064 0065 QString oldConfigFile(migration.locateLocal("config", QStringLiteral("activitymanagerrc"))); 0066 if (!oldConfigFile.isEmpty()) { 0067 QFile(oldConfigFile).copy(newConfigLocation); 0068 } 0069 #endif 0070 } 0071 0072 Activities::Private::Private(Activities *parent) 0073 : kde4ConfigurationTransitionChecker() 0074 , config(QStringLiteral("kactivitymanagerdrc")) 0075 , q(parent) 0076 { 0077 // qCDebug(KAMD_ACTIVITIES) << "Using this configuration file:" 0078 // << config.name() 0079 // << config.locationType() 0080 // << QStandardPaths::standardLocations(config.locationType()) 0081 // ; 0082 0083 // Reading activities from the config file. 0084 // Saving only the running activities means that if we have any 0085 // errors in the config, we might end up with all activities 0086 // stopped 0087 0088 const auto defaultState = !mainConfig().hasKey("runningActivities") ? Activities::Running 0089 : !mainConfig().hasKey("stoppedActivities") ? Activities::Stopped 0090 : Activities::Running; 0091 0092 const auto runningActivities = mainConfig().readEntry("runningActivities", QStringList()); 0093 const auto stoppedActivities = mainConfig().readEntry("stoppedActivities", QStringList()); 0094 0095 // Do we have a running activity? 0096 bool atLeastOneRunning = false; 0097 0098 for (const auto &activity : activityNameConfig().keyList()) { 0099 auto state = runningActivities.contains(activity) ? Activities::Running : stoppedActivities.contains(activity) ? Activities::Stopped : defaultState; 0100 0101 activities[activity] = state; 0102 0103 if (state == Activities::Running) { 0104 atLeastOneRunning = true; 0105 } 0106 } 0107 0108 // Is this our first start? 0109 if (activities.isEmpty()) { 0110 // We need to add this only after the service has been properly started 0111 KConfigGroup cg(KSharedConfig::openConfig(QStringLiteral("kdeglobals")), QStringLiteral("Activities")); 0112 // NOTE: config key still singular for retrocompatibility 0113 const QStringList names = cg.readEntry("defaultActivityName", QStringList{i18n("Default")}); 0114 0115 for (const auto &name : names) { 0116 QMetaObject::invokeMethod(q, "AddActivity", Qt::QueuedConnection, Q_ARG(QString, name)); 0117 } 0118 0119 } else if (!atLeastOneRunning) { 0120 // If we have no running activities, but we have activities, 0121 // we are in a problem. This should not happen unless the 0122 // configuration file is in a big problem and told us there 0123 // are no running activities, and enlists all of them as stopped. 0124 // In that case, we will pretend all of them are running 0125 qCWarning(KAMD_LOG_ACTIVITIES) << "The config file enlisted all activities as stopped"; 0126 for (const auto &keys : activities.keys()) { 0127 activities[keys] = Activities::Running; 0128 } 0129 } 0130 } 0131 0132 void Activities::Private::updateSortedActivityList() 0133 { 0134 QVector<ActivityInfo> a; 0135 for (const auto &activity : activities.keys()) { 0136 a.append(q->ActivityInformation(activity)); 0137 } 0138 0139 std::sort(a.begin(), a.end(), &nameBasedOrdering); 0140 0141 QWriteLocker lock(&activitiesLock); 0142 sortedActivities = a; 0143 } 0144 0145 void Activities::Private::loadLastActivity() 0146 { 0147 // This is called from constructor, no need for locking 0148 0149 // If there are no public activities, try to load the last used activity 0150 const auto lastUsedActivity = mainConfig().readEntry("currentActivity", QString()); 0151 0152 setCurrentActivity((lastUsedActivity.isEmpty() && activities.size() > 0) ? activities.keys().at(0) : lastUsedActivity); 0153 } 0154 0155 Activities::Private::~Private() 0156 { 0157 configSync(); 0158 } 0159 0160 bool Activities::Private::setCurrentActivity(const QString &activity) 0161 { 0162 { 0163 // There is nothing expensive in this block, not a problem to lock 0164 QWriteLocker lock(&activitiesLock); 0165 0166 // Should we change the activity at all? 0167 if (currentActivity == activity) { 0168 return true; 0169 } 0170 0171 // If the activity is empty, this means we are entering a limbo state 0172 if (activity.isEmpty()) { 0173 currentActivity.clear(); 0174 emit q->CurrentActivityChanged(currentActivity); 0175 return true; 0176 } 0177 0178 // Does the requested activity exist? 0179 if (!activities.contains(activity)) { 0180 return false; 0181 } 0182 } 0183 0184 // Start activity 0185 q->StartActivity(activity); 0186 0187 // Saving the current activity, and notifying 0188 // clients of the change 0189 currentActivity = activity; 0190 0191 mainConfig().writeEntry("currentActivity", activity); 0192 0193 scheduleConfigSync(); 0194 0195 emit q->CurrentActivityChanged(activity); 0196 0197 return true; 0198 } 0199 0200 bool Activities::Private::previousActivity() 0201 { 0202 const auto a = q->ListActivities(Activities::Running); 0203 0204 for (int i = 0; i < a.count(); ++i) { 0205 if (a[i] == currentActivity) { 0206 return setCurrentActivity(a[(i + a.size() - 1) % a.size()]); 0207 } 0208 } 0209 0210 return false; 0211 } 0212 0213 bool Activities::Private::nextActivity() 0214 { 0215 const auto a = q->ListActivities(Activities::Running); 0216 0217 for (int i = 0; i < a.count(); ++i) { 0218 if (a[i] == currentActivity) { 0219 return setCurrentActivity(a[(i + 1) % a.size()]); 0220 } 0221 } 0222 0223 return false; 0224 } 0225 0226 QString Activities::Private::addActivity(const QString &name) 0227 { 0228 QString activity; 0229 0230 if (name.isEmpty()) { 0231 Q_ASSERT(!name.isEmpty()); 0232 return activity; 0233 } 0234 0235 int activitiesCount = 0; 0236 0237 { 0238 QWriteLocker lock(&activitiesLock); 0239 0240 // Ensuring a new Uuid. The loop should usually end after only 0241 // one iteration 0242 while (activity.isEmpty() || activities.contains(activity)) { 0243 activity = QUuid::createUuid().toString().mid(1, 36); 0244 } 0245 0246 // Saves the activity info to the config 0247 0248 activities[activity] = Invalid; 0249 activitiesCount = activities.size(); 0250 } 0251 0252 setActivityState(activity, Running); 0253 0254 q->SetActivityName(activity, name); 0255 0256 updateSortedActivityList(); 0257 0258 emit q->ActivityAdded(activity); 0259 0260 scheduleConfigSync(); 0261 0262 if (activitiesCount == 1) { 0263 q->SetCurrentActivity(activity); 0264 } 0265 0266 return activity; 0267 } 0268 0269 void Activities::Private::removeActivity(const QString &activity) 0270 { 0271 Q_ASSERT(!activity.isEmpty()); 0272 0273 // Sanity checks 0274 { 0275 QWriteLocker lock(&activitiesLock); 0276 0277 if (!activities.contains(activity)) { 0278 return; 0279 } 0280 0281 // Is somebody trying to remove the last activity? 0282 if (activities.size() == 1) { 0283 return; 0284 } 0285 } 0286 0287 // If the activity is running, stash it 0288 q->StopActivity(activity); 0289 0290 setActivityState(activity, Activities::Invalid); 0291 0292 bool currentActivityDeleted = false; 0293 0294 { 0295 QWriteLocker lock(&activitiesLock); 0296 // Removing the activity 0297 activities.remove(activity); 0298 0299 for (int i = 0; i < sortedActivities.count(); ++i) { 0300 if (sortedActivities[i].id == activity) { 0301 sortedActivities.remove(i); 0302 break; 0303 } 0304 } 0305 0306 // If the removed activity was the current one, 0307 // set another activity as current 0308 currentActivityDeleted = (currentActivity == activity); 0309 } 0310 0311 activityNameConfig().deleteEntry(activity); 0312 activityDescriptionConfig().deleteEntry(activity); 0313 activityIconConfig().deleteEntry(activity); 0314 0315 if (currentActivityDeleted) { 0316 ensureCurrentActivityIsRunning(); 0317 } 0318 0319 emit q->ActivityRemoved(activity); 0320 0321 QMetaObject::invokeMethod(q, "ActivityRemoved", Qt::QueuedConnection, Q_ARG(QString, activity)); 0322 0323 QMetaObject::invokeMethod(this, "configSync", Qt::QueuedConnection); 0324 } 0325 0326 void Activities::Private::scheduleConfigSync() 0327 { 0328 static const auto shortInterval = 1000; 0329 0330 // If the timer is not running, or has a longer interval than we need, 0331 // start it 0332 // Note: If you want to add multiple different delays for different 0333 // events based on the importance of how quickly something needs 0334 // to be synced to the config, don't. Since the QTimer lives in a 0335 // separate thread, we have to communicate with it in via 0336 // queued connections, which means that we don't know whether 0337 // the timer was already started when this method was invoked, 0338 // we do not know whether the interval is properly set etc. 0339 if (!configSyncTimer.isActive()) { 0340 QMetaObject::invokeMethod(&configSyncTimer, "start", Qt::QueuedConnection, Q_ARG(int, shortInterval)); 0341 } 0342 } 0343 0344 void Activities::Private::configSync() 0345 { 0346 // Stop the timer and reset the interval to zero 0347 QMetaObject::invokeMethod(&configSyncTimer, "stop", Qt::QueuedConnection); 0348 config.sync(); 0349 } 0350 0351 void Activities::Private::setActivityState(const QString &activity, Activities::State state) 0352 { 0353 bool configNeedsUpdating = false; 0354 0355 { 0356 QWriteLocker lock(&activitiesLock); 0357 0358 Q_ASSERT(activities.contains(activity)); 0359 0360 if (activities.value(activity) == state) { 0361 return; 0362 } 0363 0364 // Treating 'Starting' as 'Running', and 'Stopping' as 'Stopped' 0365 // as far as the config file is concerned 0366 configNeedsUpdating = ((activities[activity] & 4) != (state & 4)); 0367 0368 activities[activity] = state; 0369 } 0370 0371 switch (state) { 0372 case Activities::Running: 0373 emit q->ActivityStarted(activity); 0374 break; 0375 0376 case Activities::Stopped: 0377 emit q->ActivityStopped(activity); 0378 break; 0379 0380 default: 0381 break; 0382 } 0383 0384 emit q->ActivityStateChanged(activity, state); 0385 0386 if (configNeedsUpdating) { 0387 QReadLocker lock(&activitiesLock); 0388 0389 mainConfig().writeEntry("runningActivities", activities.keys(Activities::Running) + activities.keys(Activities::Starting)); 0390 mainConfig().writeEntry("stoppedActivities", activities.keys(Activities::Stopped) + activities.keys(Activities::Stopping)); 0391 scheduleConfigSync(); 0392 } 0393 0394 updateSortedActivityList(); 0395 } 0396 0397 void Activities::Private::ensureCurrentActivityIsRunning() 0398 { 0399 // If the current activity is not running, 0400 // make some other activity current 0401 0402 const auto runningActivities = q->ListActivities(Activities::Running); 0403 0404 if (!runningActivities.contains(currentActivity) && runningActivities.size() > 0) { 0405 setCurrentActivity(runningActivities.first()); 0406 } 0407 } 0408 0409 void Activities::Private::activitySessionStateChanged(const QString &activity, int status) 0410 { 0411 QString currentActivity = this->currentActivity; 0412 0413 { 0414 QReadLocker lock(&activitiesLock); 0415 if (!activities.contains(activity)) { 0416 return; 0417 } 0418 } 0419 0420 switch (status) { 0421 case KSMServer::Started: 0422 case KSMServer::FailedToStop: 0423 setActivityState(activity, Activities::Running); 0424 break; 0425 0426 case KSMServer::Stopped: 0427 setActivityState(activity, Activities::Stopped); 0428 0429 if (currentActivity == activity) { 0430 ensureCurrentActivityIsRunning(); 0431 } 0432 0433 break; 0434 } 0435 0436 QMetaObject::invokeMethod(this, "configSync", Qt::QueuedConnection); 0437 } 0438 0439 // Main 0440 0441 Activities::Activities(QObject *parent) 0442 : Module(QStringLiteral("activities"), parent) 0443 , d(this) 0444 { 0445 qCDebug(KAMD_LOG_ACTIVITIES) << "Starting the KDE Activity Manager daemon" << QDateTime::currentDateTime(); 0446 0447 // Basic initialization //////////////////////////////////////////////////// 0448 0449 // Initializing D-Bus service 0450 0451 new ActivitiesAdaptor(this); 0452 QDBusConnection::sessionBus().registerObject(KAMD_DBUS_OBJECT_PATH("Activities"), this); 0453 0454 // Initializing config 0455 0456 qCDebug(KAMD_LOG_ACTIVITIES) << "Config timer connecting..."; 0457 d->connect(&d->configSyncTimer, SIGNAL(timeout()), SLOT(configSync()), Qt::QueuedConnection); 0458 0459 d->configSyncTimer.setSingleShot(true); 0460 0461 d->ksmserver = new KSMServer(this); 0462 d->connect(d->ksmserver, SIGNAL(activitySessionStateChanged(QString, int)), SLOT(activitySessionStateChanged(QString, int))); 0463 0464 d->updateSortedActivityList(); 0465 // Loading the last used activity, if possible 0466 d->loadLastActivity(); 0467 } 0468 0469 Activities::~Activities() 0470 { 0471 } 0472 0473 QString Activities::CurrentActivity() const 0474 { 0475 QReadLocker lock(&d->activitiesLock); 0476 return d->currentActivity; 0477 } 0478 0479 bool Activities::SetCurrentActivity(const QString &activity) 0480 { 0481 // Public method can not put us in a limbo state 0482 if (activity.isEmpty()) { 0483 return false; 0484 } 0485 0486 return d->setCurrentActivity(activity); 0487 } 0488 0489 bool Activities::PreviousActivity() 0490 { 0491 return d->previousActivity(); 0492 } 0493 0494 bool Activities::NextActivity() 0495 { 0496 return d->nextActivity(); 0497 } 0498 0499 QString Activities::AddActivity(const QString &name) 0500 { 0501 // We do not care about authorization if this is the first start 0502 if (!d->activities.isEmpty() && !KAuthorized::authorize(QStringLiteral("plasma-desktop/add_activities"))) { 0503 return QString(); 0504 } 0505 0506 return d->addActivity(name); 0507 } 0508 0509 void Activities::RemoveActivity(const QString &activity) 0510 { 0511 if (!KAuthorized::authorize(QStringLiteral("plasma-desktop/add_activities"))) { 0512 return; 0513 } 0514 0515 d->removeActivity(activity); 0516 } 0517 0518 QStringList Activities::ListActivities() const 0519 { 0520 QReadLocker lock(&d->activitiesLock); 0521 0522 QStringList s; 0523 for (const auto &a : d->sortedActivities) { 0524 s << a.id; 0525 } 0526 return s; 0527 } 0528 0529 QStringList Activities::ListActivities(int state) const 0530 { 0531 QReadLocker lock(&d->activitiesLock); 0532 0533 QStringList s; 0534 for (const auto &a : d->sortedActivities) { 0535 if (a.state == (State)state) { 0536 s << a.id; 0537 } 0538 } 0539 return s; 0540 } 0541 0542 QList<ActivityInfo> Activities::ListActivitiesWithInformation() const 0543 { 0544 using namespace kamd::utils; 0545 0546 // Mapping activity ids to info 0547 0548 return as_collection<QList<ActivityInfo>>(ListActivities() | transformed(&Activities::ActivityInformation, this)); 0549 } 0550 0551 ActivityInfo Activities::ActivityInformation(const QString &activity) const 0552 { 0553 return ActivityInfo{activity, ActivityName(activity), ActivityDescription(activity), ActivityIcon(activity), ActivityState(activity)}; 0554 } 0555 0556 #define CREATE_GETTER_AND_SETTER(What) \ 0557 QString Activities::Activity##What(const QString &activity) const \ 0558 { \ 0559 QReadLocker lock(&d->activitiesLock); \ 0560 return d->activities.contains(activity) ? d->activity##What(activity) : QString(); \ 0561 } \ 0562 \ 0563 void Activities::SetActivity##What(const QString &activity, const QString &value) \ 0564 { \ 0565 { \ 0566 QReadLocker lock(&d->activitiesLock); \ 0567 if (value == d->activity##What(activity) || !d->activities.contains(activity)) { \ 0568 return; \ 0569 } \ 0570 } \ 0571 \ 0572 d->activity##What##Config().writeEntry(activity, value); \ 0573 d->scheduleConfigSync(); \ 0574 \ 0575 emit Activity##What##Changed(activity, value); \ 0576 emit ActivityChanged(activity); \ 0577 } 0578 0579 CREATE_GETTER_AND_SETTER(Name) 0580 CREATE_GETTER_AND_SETTER(Description) 0581 CREATE_GETTER_AND_SETTER(Icon) 0582 0583 #undef CREATE_GETTER_AND_SETTER 0584 0585 // Main 0586 0587 void Activities::StartActivity(const QString &activity) 0588 { 0589 { 0590 QReadLocker lock(&d->activitiesLock); 0591 if (!d->activities.contains(activity) || d->activities[activity] != Stopped) { 0592 return; 0593 } 0594 } 0595 0596 d->setActivityState(activity, Starting); 0597 d->ksmserver->startActivitySession(activity); 0598 } 0599 0600 void Activities::StopActivity(const QString &activity) 0601 { 0602 { 0603 QReadLocker lock(&d->activitiesLock); 0604 if (!d->activities.contains(activity) // 0605 || d->activities[activity] == Stopped // 0606 || d->activities.size() == 1 // 0607 || d->activities.keys(Activities::Running).size() <= 1) { 0608 return; 0609 } 0610 } 0611 0612 d->setActivityState(activity, Stopping); 0613 d->ksmserver->stopActivitySession(activity); 0614 } 0615 0616 int Activities::ActivityState(const QString &activity) const 0617 { 0618 QReadLocker lock(&d->activitiesLock); 0619 return d->activities.contains(activity) ? d->activities[activity] : Invalid; 0620 }