/* * 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.h" #include "application_manager.h" #include "desktopfilereader.h" #include "session.h" #include "sharedwakelock.h" #include "taskcontroller.h" // common #include // QPA mirserver #include "logging.h" // mir #include #include namespace ms = mir::scene; namespace qtmir { QStringList Application::lifecycleExceptions; Application::Application(const QSharedPointer& sharedWakelock, DesktopFileReader *desktopFileReader, const QStringList &arguments, ApplicationManager *parent) : ApplicationInfoInterface(desktopFileReader->appId(), parent) , m_sharedWakelock(sharedWakelock) , m_desktopData(desktopFileReader) , m_pid(0) , m_stage((m_desktopData->stageHint() == "SideStage") ? Application::SideStage : Application::MainStage) , m_state(InternalState::Starting) , m_focused(false) , m_arguments(arguments) , m_session(nullptr) , m_requestedState(RequestedRunning) , m_processState(ProcessUnknown) { qCDebug(QTMIR_APPLICATIONS) << "Application::Application - appId=" << desktopFileReader->appId(); // Because m_state is InternalState::Starting acquireWakelock(); // FIXME(greyback) need to save long appId internally until ubuntu-app-launch can hide it from us m_longAppId = desktopFileReader->file().remove(QRegExp(".desktop$")).split('/').last(); m_supportedOrientations = m_desktopData->supportedOrientations(); m_rotatesWindowContents = m_desktopData->rotatesWindowContents(); } Application::~Application() { qCDebug(QTMIR_APPLICATIONS) << "Application::~Application"; // (ricmm) -- To be on the safe side, better wipe the application QML compile cache if it crashes on startup if (m_processState == Application::ProcessUnknown || state() == Application::Starting || state() == Application::Running) { wipeQMLCache(); } if (m_session) { m_session->setApplication(nullptr); delete m_session; } delete m_desktopData; } void Application::wipeQMLCache() { QString path(QDir::homePath() + QStringLiteral("/.cache/QML/Apps/")); QDir dir(path); QStringList apps = dir.entryList(); for (int i = 0; i < apps.size(); i++) { if (apps.at(i).contains(appId())) { qCDebug(QTMIR_APPLICATIONS) << "Application appId=" << apps.at(i) << " Wiping QML Cache"; dir.cd(apps.at(i)); dir.removeRecursively(); break; } } } bool Application::isValid() const { return m_desktopData->loaded(); } QString Application::desktopFile() const { return m_desktopData->file(); } QString Application::appId() const { return m_desktopData->appId(); } QString Application::name() const { return m_desktopData->name(); } QString Application::comment() const { return m_desktopData->comment(); } QUrl Application::icon() const { QString iconString = m_desktopData->icon(); QString pathString = m_desktopData->path(); if (QFileInfo(iconString).exists()) { return QUrl(iconString); } else if (QFileInfo(pathString + '/' + iconString).exists()) { return QUrl(pathString + '/' + iconString); } else { return QUrl("image://theme/" + iconString); } } QString Application::splashTitle() const { return m_desktopData->splashTitle(); } QUrl Application::splashImage() const { if (m_desktopData->splashImage().isEmpty()) { return QUrl(); } else { QFileInfo imageFileInfo(m_desktopData->path(), m_desktopData->splashImage()); if (imageFileInfo.exists()) { return QUrl::fromLocalFile(imageFileInfo.canonicalFilePath()); } else { qCWarning(QTMIR_APPLICATIONS) << QString("Application(%1).splashImage file does not exist: \"%2\". Ignoring it.") .arg(appId()).arg(imageFileInfo.absoluteFilePath()); return QUrl(); } } } QColor Application::colorFromString(const QString &colorString, const char *colorName) const { // NB: If a colour which is not fully opaque is specified in the desktop file, it will // be ignored and the default colour will be used instead. QColor color; if (colorString.isEmpty()) { color.setRgba(qRgba(0, 0, 0, 0)); } else { color.setNamedColor(colorString); if (color.isValid()) { // Force a fully opaque color. color.setAlpha(255); } else { color.setRgba(qRgba(0, 0, 0, 0)); qCWarning(QTMIR_APPLICATIONS) << QString("Invalid %1: \"%2\"") .arg(colorName).arg(colorString); } } return color; } const char* Application::internalStateToStr(InternalState state) { switch (state) { case InternalState::Starting: return "Starting"; case InternalState::Running: return "Running"; case InternalState::RunningInBackground: return "RunningInBackground"; case InternalState::SuspendingWaitSession: return "SuspendingWaitSession"; case InternalState::SuspendingWaitProcess: return "SuspendingWaitProcess"; case InternalState::Suspended: return "Suspended"; case InternalState::StoppedResumable: return "StoppedResumable"; case InternalState::Stopped: return "Stopped"; default: return "???"; } } bool Application::splashShowHeader() const { QString showHeader = m_desktopData->splashShowHeader(); if (showHeader.toLower() == "true") { return true; } else { return false; } } QColor Application::splashColor() const { QString colorStr = m_desktopData->splashColor(); return colorFromString(colorStr, "splashColor"); } QColor Application::splashColorHeader() const { QString colorStr = m_desktopData->splashColorHeader(); return colorFromString(colorStr, "splashColorHeader"); } QColor Application::splashColorFooter() const { QString colorStr = m_desktopData->splashColorFooter(); return colorFromString(colorStr, "splashColorFooter"); } QString Application::exec() const { return m_desktopData->exec(); } Application::Stage Application::stage() const { return m_stage; } Application::Stages Application::supportedStages() const { return m_supportedStages; } Application::State Application::state() const { // The public state is a simplified version of the internal one as our consumers // don't have to know or care about all the nasty details. switch (m_state) { case InternalState::Starting: return Starting; case InternalState::Running: case InternalState::RunningInBackground: case InternalState::SuspendingWaitSession: case InternalState::SuspendingWaitProcess: return Running; case InternalState::Suspended: return Suspended; case InternalState::Stopped: default: return Stopped; } } Application::RequestedState Application::requestedState() const { return m_requestedState; } void Application::setRequestedState(RequestedState value) { if (m_requestedState == value) { // nothing to do return; } qCDebug(QTMIR_APPLICATIONS) << "Application::setRequestedState - appId=" << appId() << "requestedState=" << applicationStateToStr(value); m_requestedState = value; Q_EMIT requestedStateChanged(m_requestedState); applyRequestedState(); } void Application::applyRequestedState() { if (m_requestedState == RequestedRunning) { applyRequestedRunning(); } else { applyRequestedSuspended(); } } void Application::applyRequestedRunning() { switch (m_state) { case InternalState::Starting: // should leave the app alone until it reaches Running state break; case InternalState::Running: // already where it's wanted to be break; case InternalState::RunningInBackground: case InternalState::SuspendingWaitSession: case InternalState::Suspended: resume(); break; case InternalState::SuspendingWaitProcess: // should leave the app alone until it reaches Suspended state break; case InternalState::StoppedResumable: respawn(); break; case InternalState::Stopped: // dead end. break; } } void Application::applyRequestedSuspended() { switch (m_state) { case InternalState::Starting: // should leave the app alone until it reaches Running state break; case InternalState::Running: if (m_processState == ProcessRunning) { suspend(); } else { // we can't suspend it since we have no information on the app process Q_ASSERT(m_processState == ProcessUnknown); } break; case InternalState::RunningInBackground: case InternalState::SuspendingWaitSession: case InternalState::SuspendingWaitProcess: case InternalState::Suspended: // it's already going where we it's wanted break; case InternalState::StoppedResumable: case InternalState::Stopped: // the app doesn't have a process in the first place, so there's nothing to suspend break; } } bool Application::focused() const { return m_focused; } bool Application::fullscreen() const { return m_session ? m_session->fullscreen() : false; } bool Application::canBeResumed() const { return m_processState != ProcessUnknown; } pid_t Application::pid() const { return m_pid; } void Application::setPid(pid_t pid) { m_pid = pid; } void Application::setArguments(const QStringList arguments) { m_arguments = arguments; } void Application::setSession(SessionInterface *newSession) { qCDebug(QTMIR_APPLICATIONS) << "Application::setSession - appId=" << appId() << "session=" << newSession; if (newSession == m_session) return; if (m_session) { m_session->disconnect(this); m_session->setApplication(nullptr); m_session->setParent(nullptr); } bool oldFullscreen = fullscreen(); m_session = newSession; if (m_session) { m_session->setParent(this); m_session->setApplication(this); switch (m_state) { case InternalState::Starting: case InternalState::Running: case InternalState::RunningInBackground: m_session->resume(); break; case InternalState::SuspendingWaitSession: case InternalState::SuspendingWaitProcess: case InternalState::Suspended: m_session->suspend(); break; case InternalState::Stopped: default: m_session->stop(); break; } connect(m_session, &SessionInterface::stateChanged, this, &Application::onSessionStateChanged); connect(m_session, &SessionInterface::fullscreenChanged, this, &Application::fullscreenChanged); if (oldFullscreen != fullscreen()) Q_EMIT fullscreenChanged(fullscreen()); } else { // this can only happen after the session has stopped and QML code called Session::release() Q_ASSERT(m_state == InternalState::Stopped || m_state == InternalState::StoppedResumable); } Q_EMIT sessionChanged(m_session); } void Application::setStage(Application::Stage stage) { qCDebug(QTMIR_APPLICATIONS) << "Application::setStage - appId=" << appId() << "stage=" << stage; if (m_stage != stage) { if (stage | m_supportedStages) { return; } m_stage = stage; Q_EMIT stageChanged(stage); } } void Application::setInternalState(Application::InternalState state) { if (m_state == state) { return; } qCDebug(QTMIR_APPLICATIONS) << "Application::setInternalState - appId=" << appId() << "state=" << internalStateToStr(state); auto oldPublicState = this->state(); m_state = state; switch (m_state) { case InternalState::Starting: case InternalState::Running: acquireWakelock(); break; case InternalState::RunningInBackground: releaseWakelock(); break; case InternalState::Suspended: releaseWakelock(); break; case InternalState::StoppedResumable: releaseWakelock(); break; case InternalState::Stopped: Q_EMIT stopped(); releaseWakelock(); break; case InternalState::SuspendingWaitSession: case InternalState::SuspendingWaitProcess: // transitory states. leave as it is default: break; }; if (this->state() != oldPublicState) { Q_EMIT stateChanged(this->state()); } applyRequestedState(); } void Application::setFocused(bool focused) { qCDebug(QTMIR_APPLICATIONS) << "Application::setFocused - appId=" << appId() << "focused=" << focused; if (m_focused != focused) { m_focused = focused; Q_EMIT focusedChanged(focused); } } void Application::setProcessState(ProcessState newProcessState) { if (m_processState == newProcessState) { return; } m_processState = newProcessState; switch (m_processState) { case ProcessUnknown: // it would be a coding error Q_ASSERT(false); break; case ProcessRunning: if (m_state == InternalState::StoppedResumable) { setInternalState(InternalState::Starting); } break; case ProcessSuspended: Q_ASSERT(m_state == InternalState::SuspendingWaitProcess); setInternalState(InternalState::Suspended); break; case ProcessFailed: // we assume the session always stop before the process Q_ASSERT(!m_session || m_session->state() == Session::Stopped); if (m_state == InternalState::Starting) { // that was way too soon. let it go away setInternalState(InternalState::Stopped); } else { Q_ASSERT(m_state == InternalState::Stopped || m_state == InternalState::StoppedResumable); } break; case ProcessStopped: // we assume the session always stop before the process Q_ASSERT(!m_session || m_session->state() == Session::Stopped); if (m_state == InternalState::Starting) { // that was way too soon. let it go away setInternalState(InternalState::Stopped); } else if (m_state == InternalState::StoppedResumable) { // The application stopped nicely, likely closed itself. Thus not meant to be resumed later. setInternalState(InternalState::Stopped); } else { Q_ASSERT(m_state == InternalState::Stopped); } break; } applyRequestedState(); } void Application::suspend() { Q_ASSERT(m_state == InternalState::Running); Q_ASSERT(m_session != nullptr); if (!lifecycleExceptions.filter(appId().section('_',0,0)).empty()) { // Present in exceptions list. // There's no need to keep the wakelock as the process is never suspended // and thus has no cleanup to perform when (for example) the display is // blanked. setInternalState(InternalState::RunningInBackground); } else { setInternalState(InternalState::SuspendingWaitSession); m_session->suspend(); } } void Application::resume() { if (m_state == InternalState::Suspended) { setInternalState(InternalState::Running); Q_EMIT resumeProcessRequested(); if (m_processState == ProcessSuspended) { setProcessState(ProcessRunning); // should we wait for a resumed() signal? } m_session->resume(); } else if (m_state == InternalState::SuspendingWaitSession) { setInternalState(InternalState::Running); m_session->resume(); } else if (m_state == InternalState::RunningInBackground) { setInternalState(InternalState::Running); } } void Application::respawn() { qCDebug(QTMIR_APPLICATIONS) << "Application::respawn - appId=" << appId(); setInternalState(InternalState::Starting); Q_EMIT startProcessRequested(); } QString Application::longAppId() const { return m_longAppId; } Qt::ScreenOrientations Application::supportedOrientations() const { return m_supportedOrientations; } bool Application::rotatesWindowContents() const { return m_rotatesWindowContents; } SessionInterface* Application::session() const { return m_session; } void Application::acquireWakelock() const { if (appId() == "unity8-dash") return; m_sharedWakelock->acquire(this); } void Application::releaseWakelock() const { if (appId() == "unity8-dash") return; m_sharedWakelock->release(this); } void Application::onSessionStateChanged(Session::State sessionState) { switch (sessionState) { case Session::Starting: break; case Session::Running: if (m_state == InternalState::Starting) { setInternalState(InternalState::Running); } break; case Session::Suspending: break; case Session::Suspended: Q_ASSERT(m_state == InternalState::SuspendingWaitSession); setInternalState(InternalState::SuspendingWaitProcess); Q_EMIT suspendProcessRequested(); break; case Session::Stopped: if (!canBeResumed() || m_state == InternalState::Starting || m_state == InternalState::Running) { /* 1. application is not managed by upstart * 2. application is managed by upstart, but has stopped before it managed * to create a surface, we can assume it crashed on startup, and thus * cannot be resumed * 3. application is managed by upstart and is in foreground (i.e. has * Running state), if Mir reports the application disconnects, it * either crashed or stopped itself. */ setInternalState(InternalState::Stopped); } else { setInternalState(InternalState::StoppedResumable); } } } } // namespace qtmir