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 }