File indexing completed on 2024-05-12 05:29:22

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