/* * Copyright (C) 2013-2015 Canonical, Ltd. * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License version 3, as published by * the Free Software Foundation. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ // local #include "application_manager.h" #include "application.h" #include "desktopfilereader.h" #include "dbuswindowstack.h" #include "session.h" #include "sharedwakelock.h" #include "proc_info.h" #include "taskcontroller.h" #include "upstart/applicationcontroller.h" #include "maybe_tracepoints.h" #include "settings.h" // mirserver #include "mirserver.h" #include "nativeinterface.h" #include "sessionlistener.h" #include "sessionauthorizer.h" #include "taskcontroller.h" #include "logging.h" // mir #include #include #include #include #include // Qt #include #include #include #include // std #include namespace ms = mir::scene; Q_LOGGING_CATEGORY(QTMIR_APPLICATIONS, "qtmir.applications") using namespace unity::shell::application; namespace qtmir { namespace { // FIXME: To be removed once shell has fully adopted short appIds!! QString toShortAppIdIfPossible(const QString &appId) { QRegExp longAppIdMask("[a-z0-9][a-z0-9+.-]+_[a-zA-Z0-9+.-]+_[0-9][a-zA-Z0-9.+:~-]*"); if (longAppIdMask.exactMatch(appId)) { qWarning() << "WARNING: long App ID encountered:" << appId; // input string a long AppId, chop the version string off the end QStringList parts = appId.split("_"); return QString("%1_%2").arg(parts.first()).arg(parts.at(1)); } return appId; } void connectToSessionListener(ApplicationManager *manager, SessionListener *listener) { QObject::connect(listener, &SessionListener::sessionStarting, manager, &ApplicationManager::onSessionStarting); QObject::connect(listener, &SessionListener::sessionStopping, manager, &ApplicationManager::onSessionStopping); QObject::connect(listener, &SessionListener::sessionCreatedSurface, manager, &ApplicationManager::onSessionCreatedSurface); QObject::connect(listener, &SessionListener::sessionDestroyingSurface, manager, &ApplicationManager::onSessionDestroyingSurface); } void connectToSessionAuthorizer(ApplicationManager *manager, SessionAuthorizer *authorizer) { QObject::connect(authorizer, &SessionAuthorizer::requestAuthorizationForSession, manager, &ApplicationManager::authorizeSession, Qt::BlockingQueuedConnection); } void connectToTaskController(ApplicationManager *manager, TaskController *controller) { QObject::connect(controller, &TaskController::processStarting, manager, &ApplicationManager::onProcessStarting); QObject::connect(controller, &TaskController::processStopped, manager, &ApplicationManager::onProcessStopped); QObject::connect(controller, &TaskController::processSuspended, manager, &ApplicationManager::onProcessSuspended); QObject::connect(controller, &TaskController::processFailed, manager, &ApplicationManager::onProcessFailed); QObject::connect(controller, &TaskController::focusRequested, manager, &ApplicationManager::onFocusRequested); QObject::connect(controller, &TaskController::resumeRequested, manager, &ApplicationManager::onResumeRequested); } } // namespace ApplicationManager* ApplicationManager::Factory::Factory::create() { NativeInterface *nativeInterface = dynamic_cast(QGuiApplication::platformNativeInterface()); if (!nativeInterface) { qCritical() << "ERROR: Unity.Application QML plugin requires use of the 'mirserver' QPA plugin"; QGuiApplication::quit(); return nullptr; } auto mirServer = nativeInterface->m_mirServer; SessionListener *sessionListener = static_cast(nativeInterface->nativeResourceForIntegration("SessionListener")); SessionAuthorizer *sessionAuthorizer = static_cast(nativeInterface->nativeResourceForIntegration("SessionAuthorizer")); QSharedPointer appController(new upstart::ApplicationController()); QSharedPointer taskController(new TaskController(nullptr, appController)); QSharedPointer fileReaderFactory(new DesktopFileReader::Factory()); QSharedPointer procInfo(new ProcInfo()); QSharedPointer sharedWakelock(new SharedWakelock); QSharedPointer settings(new Settings()); // FIXME: We should use a QSharedPointer to wrap this ApplicationManager object, which requires us // to use the data() method to pass the raw pointer to the QML engine. However the QML engine appears // to take ownership of the object, and deletes it when it wants to. This conflicts with the purpose // of the QSharedPointer, and a double-delete results. Trying QQmlEngine::setObjectOwnership on the // object no effect, which it should. Need to investigate why. ApplicationManager* appManager = new ApplicationManager( mirServer, taskController, sharedWakelock, fileReaderFactory, procInfo, settings ); connectToSessionListener(appManager, sessionListener); connectToSessionAuthorizer(appManager, sessionAuthorizer); connectToTaskController(appManager, taskController.data()); // Emit signal to notify Upstart that Mir is ready to receive client connections // see http://upstart.ubuntu.com/cookbook/#expect-stop // FIXME: should not be qtmir's job, instead should notify the user of this library // that they should emit this signal, perhaps by posting an event to the // QMirServerApplication event loop when it comes up if (qgetenv("UNITY_MIR_EMITS_SIGSTOP") == "1") { raise(SIGSTOP); } return appManager; } ApplicationManager* ApplicationManager::singleton() { static ApplicationManager* instance; if (!instance) { Factory appFactory; instance = appFactory.create(); } return instance; } ApplicationManager::ApplicationManager( const QSharedPointer& mirServer, const QSharedPointer& taskController, const QSharedPointer& sharedWakelock, const QSharedPointer& desktopFileReaderFactory, const QSharedPointer& procInfo, const QSharedPointer& settings, QObject *parent) : ApplicationManagerInterface(parent) , m_mirServer(mirServer) , m_focusedApplication(nullptr) , m_dbusWindowStack(new DBusWindowStack(this)) , m_taskController(taskController) , m_desktopFileReaderFactory(desktopFileReaderFactory) , m_procInfo(procInfo) , m_sharedWakelock(sharedWakelock) , m_settings(settings) { qCDebug(QTMIR_APPLICATIONS) << "ApplicationManager::ApplicationManager (this=%p)" << this; setObjectName("qtmir::ApplicationManager"); m_roleNames.insert(RoleSession, "session"); m_roleNames.insert(RoleFullscreen, "fullscreen"); if (settings.data()) { Application::lifecycleExceptions = m_settings->get("lifecycleExemptAppids").toStringList(); connect(m_settings.data(), &Settings::changed, this, &ApplicationManager::onSettingsChanged); } } ApplicationManager::~ApplicationManager() { qCDebug(QTMIR_APPLICATIONS) << "ApplicationManager::~ApplicationManager"; } int ApplicationManager::rowCount(const QModelIndex &parent) const { return !parent.isValid() ? m_applications.size() : 0; } QVariant ApplicationManager::data(const QModelIndex &index, int role) const { if (index.row() >= 0 && index.row() < m_applications.size()) { Application *application = m_applications.at(index.row()); switch (role) { case RoleAppId: return QVariant::fromValue(application->appId()); case RoleName: return QVariant::fromValue(application->name()); case RoleComment: return QVariant::fromValue(application->comment()); case RoleIcon: return QVariant::fromValue(application->icon()); case RoleStage: return QVariant::fromValue((int)application->stage()); case RoleState: return QVariant::fromValue((int)application->state()); case RoleFocused: return QVariant::fromValue(application->focused()); case RoleSession: return QVariant::fromValue(application->session()); case RoleFullscreen: return QVariant::fromValue(application->fullscreen()); default: return QVariant(); } } else { return QVariant(); } } Application* ApplicationManager::get(int index) const { if (index < 0 || index >= m_applications.count()) { return nullptr; } return m_applications.at(index); } Application* ApplicationManager::findApplication(const QString &inputAppId) const { const QString appId = toShortAppIdIfPossible(inputAppId); for (Application *app : m_applications) { if (app->appId() == appId) { return app; } } return nullptr; } bool ApplicationManager::requestFocusApplication(const QString &inputAppId) { const QString appId = toShortAppIdIfPossible(inputAppId); qCDebug(QTMIR_APPLICATIONS) << "ApplicationManager::requestFocusApplication - appId=" << appId; Application *application = findApplication(appId); if (!application) { qDebug() << "No such running application with appId=" << appId; return false; } Q_EMIT focusRequested(appId); return true; } QString ApplicationManager::focusedApplicationId() const { if (m_focusedApplication) { return m_focusedApplication->appId(); } else { return QString(); } } bool ApplicationManager::focusApplication(const QString &inputAppId) { const QString appId = toShortAppIdIfPossible(inputAppId); qCDebug(QTMIR_APPLICATIONS) << "ApplicationManager::focusApplication - appId=" << appId; Application *application = findApplication(appId); if (!application) { qDebug() << "No such running application with appId=" << appId; return false; } if (m_focusedApplication) { m_focusedApplication->setFocused(false); } m_focusedApplication = application; m_focusedApplication->setFocused(true); move(m_applications.indexOf(application), 0); Q_EMIT focusedApplicationIdChanged(); m_dbusWindowStack->FocusedWindowChanged(0, application->appId(), application->stage()); return true; } void ApplicationManager::unfocusCurrentApplication() { qCDebug(QTMIR_APPLICATIONS) << "ApplicationManager::unfocusCurrentApplication"; m_focusedApplication = nullptr; Q_EMIT focusedApplicationIdChanged(); } /** * @brief ApplicationManager::startApplication launches an application identified by an "application id" or appId. * * Note: due to an implementation detail, appIds come in two forms: * * long appId: $(click_package)_$(application)_$(version) * * short appId: $(click_package)_$(application) * It is expected that the shell uses _only_ short appIds (but long appIds are accepted by this method for legacy * reasons - but be warned, this ability will be removed) * * Unless stated otherwise, we always use short appIds in this API. * * @param inputAppId AppId of application to launch (long appId supported) * @param arguments Command line arguments to pass to the application to be launched * @return Pointer to Application object representing the launched process. If process already running, return nullptr */ Application* ApplicationManager::startApplication(const QString &appId, const QStringList &arguments) { return startApplication(appId, NoFlag, arguments); } Application *ApplicationManager::startApplication(const QString &inputAppId, ExecFlags flags, const QStringList &arguments) { tracepoint(qtmir, startApplication); QString appId = toShortAppIdIfPossible(inputAppId); qCDebug(QTMIR_APPLICATIONS) << "ApplicationManager::startApplication - this=" << this << "appId" << qPrintable(appId); Application *application = findApplication(appId); if (application) { qWarning() << "ApplicationManager::startApplication - application appId=" << appId << " already exists"; return nullptr; } if (!m_taskController->start(appId, arguments)) { qWarning() << "Upstart failed to start application with appId" << appId; return nullptr; } // The TaskController may synchroneously callback onProcessStarting, so check if application already added application = findApplication(appId); if (application) { application->setArguments(arguments); } else { application = new Application( m_sharedWakelock, m_desktopFileReaderFactory->createInstance(appId, m_taskController->findDesktopFileForAppId(appId)), arguments, this); if (!application->isValid()) { qWarning() << "Unable to instantiate application with appId" << appId; return nullptr; } // override stage if necessary if (application->stage() == Application::SideStage && flags.testFlag(ApplicationManager::ForceMainStage)) { application->setStage(Application::MainStage); } add(application); } return application; } void ApplicationManager::onProcessStarting(const QString &appId) { tracepoint(qtmir, onProcessStarting); qCDebug(QTMIR_APPLICATIONS) << "ApplicationManager::onProcessStarting - appId=" << appId; Application *application = findApplication(appId); if (!application) { // then shell did not start this application, so ubuntu-app-launch must have - add to list application = new Application( m_sharedWakelock, m_desktopFileReaderFactory->createInstance(appId, m_taskController->findDesktopFileForAppId(appId)), QStringList(), this); if (!application->isValid()) { qWarning() << "Unable to instantiate application with appId" << appId; return; } add(application); Q_EMIT focusRequested(appId); } else { if (application->state() == Application::Stopped) { // url-dispatcher can relaunch apps which have been OOM-killed - AppMan must accept the newly spawned // application and focus it immediately (as user expects app to still be running). qCDebug(QTMIR_APPLICATIONS) << "Stopped application appId=" << appId << "is being resumed externally"; Q_EMIT focusRequested(appId); } else { qCDebug(QTMIR_APPLICATIONS) << "ApplicationManager::onProcessStarting application already found with appId" << appId; } } application->setProcessState(Application::ProcessRunning); } /** * @brief ApplicationManager::stopApplication - stop a running application and remove from list * @param inputAppId * @return True if running application was stopped, false if application did not exist or could not be stopped */ bool ApplicationManager::stopApplication(const QString &inputAppId) { const QString appId = toShortAppIdIfPossible(inputAppId); qCDebug(QTMIR_APPLICATIONS) << "ApplicationManager::stopApplication - appId=" << appId; Application *application = findApplication(appId); if (!application) { qCritical() << "No such running application with appId" << appId; return false; } remove(application); bool result = m_taskController->stop(application->longAppId()); if (!result && application->pid() > 0) { qWarning() << "FAILED to ask Upstart to stop application with appId" << appId << "Sending SIGTERM to process:" << application->pid(); kill(application->pid(), SIGTERM); result = true; } delete application; return result; } void ApplicationManager::onProcessFailed(const QString &appId, const bool duringStartup) { // Applications fail if they fail to launch, crash or are killed. qCDebug(QTMIR_APPLICATIONS) << "ApplicationManager::onProcessFailed - appId=" << appId << "duringStartup=" << duringStartup; Application *application = findApplication(appId); if (!application) { qWarning() << "ApplicationManager::onProcessFailed - upstart reports failure of application" << appId << "that AppManager is not managing"; return; } Q_UNUSED(duringStartup); // FIXME(greyback) upstart reports app that fully started up & crashes as failing during startup?? application->setProcessState(Application::ProcessFailed); application->setPid(0); } void ApplicationManager::onProcessStopped(const QString &appId) { tracepoint(qtmir, onProcessStopped); qCDebug(QTMIR_APPLICATIONS) << "ApplicationManager::onProcessStopped - appId=" << appId; Application *application = findApplication(appId); if (!application) { qDebug() << "ApplicationManager::onProcessStopped reports stop of appId=" << appId << "which AppMan is not managing, ignoring the event"; return; } // if an application gets killed, onProcessFailed is called first, followed by onProcessStopped. // we don't want to override what onProcessFailed already set. if (application->processState() != Application::ProcessFailed) { application->setProcessState(Application::ProcessStopped); application->setPid(0); } } void ApplicationManager::onProcessSuspended(const QString &appId) { Application *application = findApplication(appId); if (application) { application->setProcessState(Application::ProcessSuspended); } } void ApplicationManager::onFocusRequested(const QString& appId) { qCDebug(QTMIR_APPLICATIONS) << "ApplicationManager::onFocusRequested - appId=" << appId; Q_EMIT focusRequested(appId); } void ApplicationManager::onResumeRequested(const QString& appId) { qCDebug(QTMIR_APPLICATIONS) << "ApplicationManager::onResumeRequested - appId=" << appId; Application *application = findApplication(appId); if (!application) { qCritical() << "ApplicationManager::onResumeRequested: No such running application" << appId; return; } // We interpret this as a focus request for a suspended app. // Shell will have this app resumed if it complies with the focus request if (application->state() == Application::Suspended) { Q_EMIT focusRequested(appId); } } void ApplicationManager::onAppDataChanged(const int role) { if (sender()) { Application *application = static_cast(sender()); QModelIndex appIndex = findIndex(application); Q_EMIT dataChanged(appIndex, appIndex, QVector() << role); qCDebug(QTMIR_APPLICATIONS) << "ApplicationManager::onAppDataChanged: Received " << m_roleNames[role] << " update" << application->appId(); } else { qCDebug(QTMIR_APPLICATIONS) << "ApplicationManager::onAppDataChanged: Received " << m_roleNames[role] << " signal but application has disappeard."; } } void ApplicationManager::onSettingsChanged(const QString &key) { if (key == "lifecycleExemptAppids") { Application::lifecycleExceptions = m_settings->get("lifecycleExemptAppids").toStringList(); } } void ApplicationManager::authorizeSession(const quint64 pid, bool &authorized) { tracepoint(qtmir, authorizeSession); authorized = false; //to be proven wrong qCDebug(QTMIR_APPLICATIONS) << "ApplicationManager::authorizeSession - pid=" << pid; for (Application *app : m_applications) { if (app->state() == Application::Starting) { tracepoint(qtmir, appIdHasProcessId_start); if (m_taskController->appIdHasProcessId(app->appId(), pid)) { app->setPid(pid); authorized = true; tracepoint(qtmir, appIdHasProcessId_end, 1); //found return; } tracepoint(qtmir, appIdHasProcessId_end, 0); // not found } } /* * Hack: Allow applications to be launched without being managed by upstart, where AppManager * itself manages processes executed with a "--desktop_file_hint=/path/to/desktopFile.desktop" * parameter attached. This exists until ubuntu-app-launch can notify shell any application is * and so shell should allow it. Also reads the --stage parameter to determine the desired stage */ std::unique_ptr info = m_procInfo->commandLine(pid); if (!info) { qWarning() << "ApplicationManager REJECTED connection from app with pid" << pid << "as unable to read the process command line"; return; } if (info->startsWith("maliit-server") || info->contains("qt5/libexec/QtWebProcess")) { authorized = true; return; } const QString desktopFileName = info->getParameter("--desktop_file_hint="); if (desktopFileName.isNull()) { qCritical() << "ApplicationManager REJECTED connection from app with pid" << pid << "as it was not launched by upstart, and no desktop_file_hint is specified"; return; } qCDebug(QTMIR_APPLICATIONS) << "Process supplied desktop_file_hint, loading:" << desktopFileName; // Guess appId from the desktop file hint const QString appId = toShortAppIdIfPossible(desktopFileName.split('/').last().remove(QRegExp(".desktop$"))); // FIXME: right now we support --desktop_file_hint=appId for historical reasons. So let's try that in // case we didn't get an existing .desktop file path DesktopFileReader* desktopData; if (QFileInfo::exists(desktopFileName)) { desktopData = m_desktopFileReaderFactory->createInstance(appId, QFileInfo(desktopFileName)); } else { qCDebug(QTMIR_APPLICATIONS) << "Unable to find file:" << desktopFileName << "so will search standard paths for one named" << appId << ".desktop"; desktopData = m_desktopFileReaderFactory->createInstance(appId, m_taskController->findDesktopFileForAppId(appId)); } if (!desktopData->loaded()) { delete desktopData; qCritical() << "ApplicationManager REJECTED connection from app with pid" << pid << "as the file specified by the desktop_file_hint argument could not be opened"; return; } // some naughty applications use a script to launch the actual application. Check for the // case where shell actually launched the script. Application *application = findApplication(desktopData->appId()); if (application && application->state() == Application::Starting) { qCDebug(QTMIR_APPLICATIONS) << "Process with pid" << pid << "appeared, attaching to existing entry" << "in application list with appId:" << application->appId(); delete desktopData; application->setPid(pid); authorized = true; return; } // if stage supplied in CLI, fetch that Application::Stage stage = Application::MainStage; QString stageParam = info->getParameter("--stage_hint="); if (stageParam == "side_stage") { stage = Application::SideStage; } qCDebug(QTMIR_APPLICATIONS) << "New process with pid" << pid << "appeared, adding new application to the" << "application list with appId:" << desktopData->appId(); QStringList arguments(info->asStringList()); application = new Application( m_sharedWakelock, desktopData, arguments, this); application->setPid(pid); application->setStage(stage); add(application); authorized = true; } void ApplicationManager::onSessionStarting(std::shared_ptr const& session) { Q_UNUSED(session); } void ApplicationManager::onSessionStopping(std::shared_ptr const& session) { Application* application = findApplicationWithSession(session); if (application) { m_dbusWindowStack->WindowDestroyed(0, application->appId()); } } void ApplicationManager::onSessionCreatedSurface(ms::Session const* session, std::shared_ptr const& surface) { qCDebug(QTMIR_APPLICATIONS) << "ApplicationManager::onSessionCreatedSurface - sessionName=" << session->name().c_str(); Q_UNUSED(surface); Application* application = findApplicationWithSession(session); if (application) { m_dbusWindowStack->WindowCreated(0, application->appId()); } } void ApplicationManager::onSessionDestroyingSurface(ms::Session const* session, std::shared_ptr const& surface) { qCDebug(QTMIR_APPLICATIONS) << "ApplicationManager::onSessionDestroyingSurface - sessionName=" << session->name().c_str(); Q_UNUSED(surface); Application* application = findApplicationWithSession(session); if (application && application->state() == Application::Running) { // If app in Running state but it destroys its surface, it's probably shutting down, // in which case, we can preempt it and remove it from the App list immediately. // FIXME: this is not desktop application friendly, but resolves issue where trust-prompt // helpers take a long time to shut down, but destroys their surface quickly. remove(application); application->deleteLater(); } } Application* ApplicationManager::findApplicationWithSession(const std::shared_ptr &session) { return findApplicationWithSession(session.get()); } Application* ApplicationManager::findApplicationWithSession(const ms::Session *session) { if (!session) return nullptr; return findApplicationWithPid(session->process_id()); } Application* ApplicationManager::findApplicationWithPid(const qint64 pid) { if (pid <= 0) return nullptr; for (Application *app : m_applications) { if (app->m_pid == pid) { return app; } } return nullptr; } void ApplicationManager::add(Application* application) { Q_ASSERT(application != nullptr); qCDebug(QTMIR_APPLICATIONS) << "ApplicationManager::add - appId=" << application->appId(); connect(application, &Application::fullscreenChanged, this, [this](bool) { onAppDataChanged(RoleFullscreen); }); connect(application, &Application::focusedChanged, this, [this](bool) { onAppDataChanged(RoleFocused); }); connect(application, &Application::stateChanged, this, [this](Application::State) { onAppDataChanged(RoleState); }); connect(application, &Application::stageChanged, this, [this](Application::Stage) { onAppDataChanged(RoleStage); }); QString appId = application->appId(); QString longAppId = application->longAppId(); QStringList arguments = application->arguments(); // The connection is queued as a workaround an issue in the PhoneStage animation that // happens when you tap on a killed app in the spread to bring it to foreground, causing // a Application::respawn() to take place. // In any case, it seems like in general QML works better when don't do too many things // in the same event loop iteration. connect(application, &Application::startProcessRequested, this, [=]() { m_taskController->start(appId, arguments); }, Qt::QueuedConnection); connect(application, &Application::suspendProcessRequested, this, [=]() { m_taskController->suspend(longAppId); } ); connect(application, &Application::resumeProcessRequested, this, [=]() { m_taskController->resume(longAppId); } ); connect(application, &Application::stopped, this, [=]() { remove(application); application->deleteLater(); }); beginInsertRows(QModelIndex(), m_applications.count(), m_applications.count()); m_applications.append(application); endInsertRows(); Q_EMIT countChanged(); Q_EMIT applicationAdded(application->appId()); if (m_applications.size() == 1) { Q_EMIT emptyChanged(); } } void ApplicationManager::remove(Application *application) { Q_ASSERT(application != nullptr); qCDebug(QTMIR_APPLICATIONS) << "ApplicationManager::remove - appId=" << application->appId(); application->disconnect(this); int i = m_applications.indexOf(application); if (i != -1) { beginRemoveRows(QModelIndex(), i, i); m_applications.removeAt(i); endRemoveRows(); Q_EMIT applicationRemoved(application->appId()); Q_EMIT countChanged(); if (i == 0) { Q_EMIT emptyChanged(); } } if (application == m_focusedApplication) { m_focusedApplication = nullptr; Q_EMIT focusedApplicationIdChanged(); } } void ApplicationManager::move(int from, int to) { qCDebug(QTMIR_APPLICATIONS) << "ApplicationManager::move - from=" << from << "to=" << to; if (from == to) return; if (from >= 0 && from < m_applications.size() && to >= 0 && to < m_applications.size()) { QModelIndex parent; /* When moving an item down, the destination index needs to be incremented by one, as explained in the documentation: http://qt-project.org/doc/qt-5.0/qtcore/qabstractitemmodel.html#beginMoveRows */ beginMoveRows(parent, from, from, parent, to + (to > from ? 1 : 0)); m_applications.move(from, to); endMoveRows(); } qCDebug(QTMIR_APPLICATIONS) << "ApplicationManager::move after " << toString(); } QModelIndex ApplicationManager::findIndex(Application* application) { for (int i = 0; i < m_applications.size(); ++i) { if (m_applications.at(i) == application) { return index(i); } } return QModelIndex(); } QString ApplicationManager::toString() const { QString result; for (int i = 0; i < m_applications.count(); ++i) { if (i > 0) { result.append(","); } result.append(m_applications.at(i)->appId()); } return result; } } // namespace qtmir