diff --git a/discover/qml/ApplicationScreenshots.qml b/discover/qml/ApplicationScreenshots.qml index 12e30440..2a2699cd 100644 --- a/discover/qml/ApplicationScreenshots.qml +++ b/discover/qml/ApplicationScreenshots.qml @@ -1,174 +1,180 @@ /* * Copyright (C) 2012 Aleix Pol Gonzalez * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library/Lesser General Public License * version 2, or (at your option) any later version, 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 warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details * * You should have received a copy of the GNU Library/Lesser General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import QtQuick 2.1 import QtQuick.Layouts 1.1 import QtQuick.Controls 1.1 import QtQuick.Controls 2.1 as QQC2 import QtGraphicalEffects 1.0 import org.kde.discover 2.0 import org.kde.kirigami 2.0 as Kirigami Flickable { id: root readonly property alias count: screenshotsModel.count property alias resource: screenshotsModel.application property var resource property int currentIndex: -1 property Item currentItem: screenshotsRep.itemAt(currentIndex) Layout.preferredHeight: Kirigami.Units.gridUnit * 13 contentHeight: height contentWidth: screenshotsLayout.width QQC2.Popup { id: overlay parent: applicationWindow().overlay modal: true x: (parent.width - width)/2 y: (parent.height - height)/2 readonly property real proportion: overlayImage.sourceSize.width>1 ? overlayImage.sourceSize.height/overlayImage.sourceSize.width : 1 - height: Math.min(parent.height * 0.9, (parent.width * 0.9) * proportion, overlayImage.sourceSize.height) + height: overlayImage.status == Image.Loading ? Kirigami.Units.gridUnit * 5 : Math.min(parent.height * 0.9, (parent.width * 0.9) * proportion, overlayImage.sourceSize.height) width: height/proportion + BusyIndicator { + visible: running + running: overlayImage.status == Image.Loading + anchors.fill: parent + } + Image { id: overlayImage anchors.fill: parent source: root.currentItem ? root.currentItem.imageSource : "" fillMode: Image.PreserveAspectFit smooth: true } Button { anchors { right: parent.left verticalCenter: parent.verticalCenter } visible: leftAction.visible iconName: leftAction.iconName onClicked: leftAction.triggered(null) } Button { anchors { left: parent.right verticalCenter: parent.verticalCenter } visible: rightAction.visible iconName: rightAction.iconName onClicked: rightAction.triggered(null) } Kirigami.Action { id: leftAction iconName: "arrow-left" enabled: overlay.visible && visible visible: root.currentIndex >= 1 onTriggered: root.currentIndex = (root.currentIndex - 1) % screenshotsModel.count } Kirigami.Action { id: rightAction iconName: "arrow-right" enabled: overlay.visible && visible visible: root.currentIndex < (root.count - 1) onTriggered: root.currentIndex = (root.currentIndex + 1) % screenshotsModel.count } } Row { id: screenshotsLayout height: root.contentHeight spacing: Kirigami.Units.largeSpacing focus: overlay.visible Keys.onLeftPressed: if (leftAction.visible) leftAction.trigger() Keys.onRightPressed: if (rightAction.visible) rightAction.trigger() Repeater { id: screenshotsRep model: ScreenshotsModel { id: screenshotsModel } delegate: MouseArea { readonly property url imageSource: large_image_url readonly property real proportion: thumbnail.sourceSize.width>1 ? thumbnail.sourceSize.height/thumbnail.sourceSize.width : 1 width: Math.max(50, height/proportion) height: screenshotsLayout.height hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: { root.currentIndex = index overlay.open() } DropShadow { source: thumbnail anchors.fill: thumbnail verticalOffset: 3 horizontalOffset: 0 radius: 12.0 samples: 25 color: Kirigami.Theme.disabledTextColor cached: true } BusyIndicator { visible: running running: thumbnail.status == Image.Loading anchors.centerIn: parent } Image { id: thumbnail source: small_image_url height: parent.height fillMode: Image.PreserveAspectFit smooth: true } } } } clip: true readonly property var leftShadow: Shadow { parent: root anchors { left: parent.left top: parent.top bottom: parent.bottom } edge: Qt.LeftEdge width: Math.max(0, Math.min(root.width/5, root.contentX)) } readonly property var rightShadow: Shadow { parent: root anchors { right: parent.right top: parent.top bottom: parent.bottom } edge: Qt.RightEdge width: Math.max(0, Math.min(root.contentWidth - root.contentX - root.width)/5) } } diff --git a/discover/qml/SourcesPage.qml b/discover/qml/SourcesPage.qml index 5c00ad70..9506d40d 100644 --- a/discover/qml/SourcesPage.qml +++ b/discover/qml/SourcesPage.qml @@ -1,206 +1,206 @@ import QtQuick 2.4 import QtQuick.Controls 1.1 import QtQuick.Controls 2.1 as QQC2 import QtQuick.Layouts 1.1 import org.kde.discover 2.0 import org.kde.discover.app 1.0 import org.kde.kquickcontrolsaddons 2.0 import org.kde.kirigami 2.2 as Kirigami import "navigation.js" as Navigation DiscoverPage { id: page clip: true title: i18n("Settings") property string search: "" Kirigami.Theme.inherit: false Kirigami.Theme.colorSet: Kirigami.Theme.View background: Rectangle { color: Kirigami.Theme.backgroundColor } contextualActions: [ KirigamiActionBridge { action: app.action("help_about_app") }, KirigamiActionBridge { action: app.action("help_report_bug") } ] mainItem: ListView { id: sourcesView model: QSortFilterProxyModel { filterRegExp: new RegExp(page.search, 'i') sortRole: SourcesModelClass.SourceNameRole sourceModel: SourcesModel } currentIndex: -1 Component { id: sourceBackendDelegate Kirigami.AbstractListItem { id: backendItem hoverEnabled: false supportsMouseEvents: false readonly property QtObject backend: sourcesBackend readonly property bool isDefault: ResourcesModel.currentApplicationBackend == resourcesBackend RowLayout { Connections { target: backendItem.backend onPassiveMessage: window.showPassiveNotification(message) } anchors { right: parent.right left: parent.left rightMargin: parent.rightPadding leftMargin: parent.leftPadding } Kirigami.Heading { Layout.fillWidth: true text: backendItem.isDefault ? i18n("%1 (Default)", resourcesBackend.displayName) : resourcesBackend.displayName } Button { Layout.rightMargin: Kirigami.Units.smallSpacing iconName: "preferences-other" visible: resourcesBackend && resourcesBackend.hasApplications Component { id: dialogComponent AddSourceDialog { source: backendItem.backend onVisibleChanged: if (!visible) { destroy() } } } menu: Menu { id: settingsMenu MenuItem { enabled: !backendItem.isDefault text: i18n("Make default") onTriggered: ResourcesModel.currentApplicationBackend = backendItem.backend.resourcesBackend } MenuItem { text: i18n("Add Source...") visible: backendItem.backend && backendItem.backend.supportsAdding onTriggered: { var addSourceDialog = dialogComponent.createObject(null, {displayName: backendItem.backend.resourcesBackend.displayName }) addSourceDialog.open() } } MenuSeparator { visible: backendActionsInst.count>0 } Instantiator { id: backendActionsInst model: ActionsModel { actions: backendItem.backend ? backendItem.backend.actions : undefined } delegate: MenuItem { action: ActionBridge { action: modelData.action } } onObjectAdded: { settingsMenu.insertItem(index, object) } onObjectRemoved: { object.destroy() } } } } } } } delegate: ConditionalLoader { anchors { right: parent.right left: parent.left } readonly property variant resourcesBackend: model.resourcesBackend readonly property variant sourcesBackend: model.sourcesBackend readonly property variant display: model.display readonly property variant checked: model.checked readonly property variant statusTip: model.statusTip readonly property variant toolTip: model.toolTip readonly property variant sourceId: model.sourceId readonly property variant modelIndex: sourcesView.model.index(index, 0) condition: resourcesBackend != null componentTrue: sourceBackendDelegate componentFalse: sourceDelegate } Component { id: sourceDelegate Kirigami.SwipeListItem { Layout.fillWidth: true enabled: display.length>0 highlighted: ListView.isCurrentItem - onClicked: Navigation.openApplicationListSource(display) + onClicked: Navigation.openApplicationListSource(sourceId) Keys.onReturnPressed: clicked() actions: [ Kirigami.Action { iconName: "edit-delete" tooltip: i18n("Delete the origin") onTriggered: { var backend = sourcesBackend if (!backend.removeSource(sourceId)) { window.showPassiveNotification(i18n("Failed to remove the source '%1'", display)) } } } ] RowLayout { CheckBox { id: enabledBox readonly property variant modelChecked: sourcesView.model.data(modelIndex, Qt.CheckStateRole) checked: modelChecked != Qt.Unchecked enabled: modelChecked !== undefined onClicked: { sourcesView.model.setData(modelIndex, checkedState, Qt.CheckStateRole) } } QQC2.Label { text: display + " - " + toolTip + "" elide: Text.ElideRight Layout.fillWidth: true } } } } footer: ColumnLayout { id: foot anchors { right: parent.right left: parent.left margins: Kirigami.Units.smallSpacing } Repeater { id: back model: ResourcesProxyModel { extending: "org.kde.discover.desktop" } delegate: RowLayout { visible: !model.application.isInstalled QQC2.Label { Layout.fillWidth: true text: name } InstallApplicationButton { application: model.application } } } } } } diff --git a/libdiscover/backends/FlatpakBackend/FlatpakBackend.cpp b/libdiscover/backends/FlatpakBackend/FlatpakBackend.cpp index 096c2b26..3d071e0b 100644 --- a/libdiscover/backends/FlatpakBackend/FlatpakBackend.cpp +++ b/libdiscover/backends/FlatpakBackend/FlatpakBackend.cpp @@ -1,1252 +1,1252 @@ /*************************************************************************** * Copyright © 2013 Aleix Pol Gonzalez * * Copyright © 2017 Jan Grulich * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #include "FlatpakBackend.h" #include "FlatpakFetchDataJob.h" #include "FlatpakResource.h" #include "FlatpakSourcesBackend.h" #include "FlatpakTransaction.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include DISCOVER_BACKEND_PLUGIN(FlatpakBackend) static QString idForInstalledRef(FlatpakInstallation *installation, FlatpakInstalledRef *ref) { const FlatpakResource::ResourceType appType = flatpak_ref_get_kind(FLATPAK_REF(ref)) == FLATPAK_REF_KIND_APP ? FlatpakResource::DesktopApp : FlatpakResource::Runtime; const QString name = QLatin1String(flatpak_ref_get_name(FLATPAK_REF(ref))); const QString appId = appType == FlatpakResource::DesktopApp ? QLatin1String(flatpak_ref_get_name(FLATPAK_REF(ref))) + QStringLiteral(".desktop") : name; const QString arch = QString::fromUtf8(flatpak_ref_get_arch(FLATPAK_REF(ref))); const QString branch = QString::fromUtf8(flatpak_ref_get_branch(FLATPAK_REF(ref))); return QStringLiteral("%1/%2/%3/%4/%5/%6").arg(FlatpakResource::installationPath(installation), QLatin1String("flatpak"), QString::fromUtf8(flatpak_installed_ref_get_origin(ref)), FlatpakResource::typeAsString(appType), appId, branch); } FlatpakBackend::FlatpakBackend(QObject* parent) : AbstractResourcesBackend(parent) , m_updater(new StandardBackendUpdater(this)) , m_reviews(AppStreamIntegration::global()->reviews()) , m_fetching(false) , m_refreshAppstreamMetadataJobs(0) , m_threadPool(new QThreadPool(this)) { g_autoptr(GError) error = nullptr; m_cancellable = g_cancellable_new(); connect(m_updater, &StandardBackendUpdater::updatesCountChanged, this, &FlatpakBackend::updatesCountChanged); // Load flatpak installation if (!setupFlatpakInstallations(&error)) { qWarning() << "Failed to setup flatpak installations:" << error->message; } else { loadAppsFromAppstreamData(); m_sources = new FlatpakSourcesBackend(m_installations, this); SourcesModel::global()->addSourcesBackend(m_sources); } connect(m_reviews.data(), &OdrsReviewsBackend::ratingsReady, this, &FlatpakBackend::announceRatingsReady); } FlatpakBackend::~FlatpakBackend() { m_threadPool.clear(); for(auto inst : m_installations) g_object_unref(inst); g_object_unref(m_cancellable); } bool FlatpakBackend::isValid() const { return m_sources && !m_installations.isEmpty(); } void FlatpakBackend::announceRatingsReady() { emitRatingsReady(); const auto ids = m_reviews->appstreamIds().toSet(); foreach(AbstractResource* res, m_resources) { if (ids.contains(res->appstreamId())) { res->ratingFetched(); } } } class FlatpakFetchRemoteResourceJob : public QNetworkAccessManager { Q_OBJECT public: FlatpakFetchRemoteResourceJob(const QUrl &url, FlatpakBackend *backend) : QNetworkAccessManager(backend) , m_backend(backend) , m_url(url) { } void start() { auto replyGet = get(QNetworkRequest(m_url)); connect(replyGet, &QNetworkReply::finished, this, [this, replyGet] { const QUrl originalUrl = replyGet->request().url(); if (replyGet->error() != QNetworkReply::NoError) { qWarning() << "couldn't download" << originalUrl << replyGet->errorString(); Q_EMIT jobFinished(false, nullptr); return; } const QUrl fileUrl = QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::TempLocation) + QLatin1Char('/') + originalUrl.fileName()); auto replyPut = put(QNetworkRequest(fileUrl), replyGet->readAll()); connect(replyPut, &QNetworkReply::finished, this, [this, originalUrl, fileUrl, replyPut]() { if (replyPut->error() == QNetworkReply::NoError) { auto res = m_backend->resourceForFile(fileUrl); if (res) { FlatpakResource *resource = qobject_cast(res); resource->setResourceFile(originalUrl); Q_EMIT jobFinished(true, resource); } else { qWarning() << "couldn't download" << originalUrl << "into" << fileUrl << replyPut->errorString(); Q_EMIT jobFinished(false, nullptr); } } }); }); } Q_SIGNALS: void jobFinished(bool success, FlatpakResource *resource); private: FlatpakBackend *m_backend; QUrl m_url; }; FlatpakRemote * FlatpakBackend::getFlatpakRemoteByUrl(const QString &url, FlatpakInstallation *installation) const { auto remotes = flatpak_installation_list_remotes(installation, m_cancellable, nullptr); if (!remotes) { return nullptr; } const QByteArray comparableUrl = url.toUtf8(); for (uint i = 0; i < remotes->len; i++) { FlatpakRemote *remote = FLATPAK_REMOTE(g_ptr_array_index(remotes, i)); if (comparableUrl == flatpak_remote_get_url(remote)) { return remote; } } return nullptr; } FlatpakInstalledRef * FlatpakBackend::getInstalledRefForApp(FlatpakInstallation *flatpakInstallation, FlatpakResource *resource) const { FlatpakInstalledRef *ref = nullptr; g_autoptr(GError) localError = nullptr; if (!flatpakInstallation) { return ref; } const auto type = resource->type() == FlatpakResource::DesktopApp ? FLATPAK_REF_KIND_APP : FLATPAK_REF_KIND_RUNTIME; return flatpak_installation_get_installed_ref(flatpakInstallation, type, resource->flatpakName().toUtf8().constData(), resource->arch().toUtf8().constData(), resource->branch().toUtf8().constData(), m_cancellable, &localError); } FlatpakResource * FlatpakBackend::getAppForInstalledRef(FlatpakInstallation *flatpakInstallation, FlatpakInstalledRef *ref) const { return m_resources.value(idForInstalledRef(flatpakInstallation, ref)); } FlatpakResource * FlatpakBackend::getRuntimeForApp(FlatpakResource *resource) const { FlatpakResource *runtime = nullptr; const auto runtimeInfo = resource->runtime().split(QLatin1Char('/')); if (runtimeInfo.count() != 3) { return runtime; } const QString runtimeId = QStringLiteral("runtime/") + runtimeInfo.at(0) + QLatin1Char('/') + runtimeInfo.at(2); for(auto it = m_resources.constBegin(), itEnd = m_resources.constEnd(); it!=itEnd; ++it) { if (it.key().endsWith(runtimeId)) { runtime = *it; break; } } // TODO if runtime wasn't found, create a new one from available info return runtime; } FlatpakResource * FlatpakBackend::addAppFromFlatpakBundle(const QUrl &url) { g_autoptr(GBytes) appstreamGz = nullptr; g_autoptr(GError) localError = nullptr; g_autoptr(GFile) file = nullptr; g_autoptr(FlatpakBundleRef) bundleRef = nullptr; AppStream::Component asComponent; file = g_file_new_for_path(url.toLocalFile().toUtf8().constData()); bundleRef = flatpak_bundle_ref_new(file, &localError); if (!bundleRef) { qWarning() << "Failed to load bundle:" << localError->message; return nullptr; } g_autoptr(GBytes) metadata = flatpak_bundle_ref_get_metadata(bundleRef); appstreamGz = flatpak_bundle_ref_get_appstream(bundleRef); if (appstreamGz) { g_autoptr(GZlibDecompressor) decompressor = nullptr; g_autoptr(GInputStream) streamGz = nullptr; g_autoptr(GInputStream) streamData = nullptr; g_autoptr(GBytes) appstream = nullptr; /* decompress data */ decompressor = g_zlib_decompressor_new (G_ZLIB_COMPRESSOR_FORMAT_GZIP); streamGz = g_memory_input_stream_new_from_bytes (appstreamGz); if (!streamGz) { return nullptr; } streamData = g_converter_input_stream_new (streamGz, G_CONVERTER (decompressor)); appstream = g_input_stream_read_bytes (streamData, 0x100000, m_cancellable, &localError); if (!appstream) { qWarning() << "Failed to extract appstream metadata from bundle:" << localError->message; return nullptr; } gsize len = 0; gconstpointer data = g_bytes_get_data(appstream, &len); AppStream::Metadata metadata; metadata.setFormatStyle(AppStream::Metadata::FormatStyleCollection); AppStream::Metadata::MetadataError error = metadata.parse(QString::fromUtf8((char*)data, len), AppStream::Metadata::FormatKindXml); if (error != AppStream::Metadata::MetadataErrorNoError) { qWarning() << "Failed to parse appstream metadata: " << error; return nullptr; } const QList components = metadata.components(); if (components.size()) { asComponent = AppStream::Component(components.first()); } else { qWarning() << "Failed to parse appstream metadata"; return nullptr; } } else { qWarning() << "No appstream metadata in bundle"; QTemporaryFile tempFile; tempFile.setAutoRemove(false); if (!tempFile.open()) { qWarning() << "Failed to get metadata file"; return nullptr; } gsize len = 0; QByteArray metadataContent = QByteArray((char *)g_bytes_get_data(metadata, &len)); tempFile.write(metadataContent); tempFile.close(); // Parse the temporary file QSettings setting(tempFile.fileName(), QSettings::NativeFormat); setting.beginGroup(QLatin1String("Application")); asComponent.setName(setting.value(QLatin1String("name")).toString()); tempFile.remove(); } FlatpakResource *resource = new FlatpakResource(asComponent, preferredInstallation(), this); gsize len = 0; QByteArray metadataContent = QByteArray((char *)g_bytes_get_data(metadata, &len)); if (!updateAppMetadata(resource, metadataContent)) { delete resource; qWarning() << "Failed to update metadata from app bundle"; return nullptr; } g_autoptr(GBytes) iconData = flatpak_bundle_ref_get_icon(bundleRef, 128); if (!iconData) { iconData = flatpak_bundle_ref_get_icon(bundleRef, 64); } if (iconData) { gsize len = 0; char * data = (char *)g_bytes_get_data(iconData, &len); QPixmap pixmap; pixmap.loadFromData(QByteArray(data, len), "PNG"); resource->setBundledIcon(pixmap); } const QString origin = QString::fromUtf8(flatpak_bundle_ref_get_origin(bundleRef)); resource->setDownloadSize(0); resource->setInstalledSize(flatpak_bundle_ref_get_installed_size(bundleRef)); resource->setPropertyState(FlatpakResource::DownloadSize, FlatpakResource::AlreadyKnown); resource->setPropertyState(FlatpakResource::InstalledSize, FlatpakResource::AlreadyKnown); resource->setFlatpakFileType(QStringLiteral("flatpak")); resource->setOrigin(origin.isEmpty() ? i18n("Local bundle") : origin); resource->setResourceFile(url); resource->setState(FlatpakResource::None); resource->setType(FlatpakResource::DesktopApp); addResource(resource); return resource; } FlatpakResource * FlatpakBackend::addAppFromFlatpakRef(const QUrl &url) { QSettings settings(url.toLocalFile(), QSettings::NativeFormat); const QString refurl = settings.value(QStringLiteral("Flatpak Ref/Url")).toString(); g_autoptr(GError) error = NULL; g_autoptr(FlatpakRemoteRef) remoteRef = nullptr; { QFile f(url.toLocalFile()); if (!f.open(QFile::ReadOnly | QFile::Text)) { return nullptr; } QByteArray contents = f.readAll(); g_autoptr(GBytes) bytes = g_bytes_new (contents.data(), contents.size()); remoteRef = flatpak_installation_install_ref_file (preferredInstallation(), bytes, m_cancellable, &error); if (!remoteRef) { qWarning() << "Failed to install ref file:" << error->message; return nullptr; } } const auto remoteName = flatpak_remote_ref_get_remote_name(remoteRef); auto ref = FLATPAK_REF(remoteRef); AppStream::Component asComponent; asComponent.addUrl(AppStream::Component::UrlKindHomepage, settings.value(QStringLiteral("Flatpak Ref/Homepage")).toString()); asComponent.setDescription(settings.value(QStringLiteral("Flatpak Ref/Description")).toString()); asComponent.setName(settings.value(QStringLiteral("Flatpak Ref/Title")).toString()); asComponent.setSummary(settings.value(QStringLiteral("Flatpak Ref/Comment")).toString()); asComponent.setId(settings.value(QStringLiteral("Flatpak Ref/Name")).toString()); const QString iconUrl = settings.value(QStringLiteral("Flatpak Ref/Icon")).toString(); if (!iconUrl.isEmpty()) { AppStream::Icon icon; icon.setKind(AppStream::Icon::KindRemote); icon.setUrl(QUrl(iconUrl)); asComponent.addIcon(icon); } auto resource = new FlatpakResource(asComponent, preferredInstallation(), this); resource->setFlatpakFileType(QStringLiteral("flatpakref")); resource->setOrigin(QString::fromUtf8(remoteName)); resource->updateFromRef(ref); QUrl runtimeUrl = QUrl(settings.value(QStringLiteral("Flatpak Ref/RuntimeRepo")).toString()); if (!runtimeUrl.isEmpty()) { auto installation = preferredInstallation(); // We need to fetch metadata to find information about required runtime auto fw = new QFutureWatcher(this); fw->setFuture(QtConcurrent::run(&m_threadPool, &FlatpakRunnables::fetchMetadata, installation, resource)); connect(fw, &QFutureWatcher::finished, this, [this, installation, resource, fw, runtimeUrl]() { const auto metadata = fw->result(); // Even when we failed to fetch information about runtime we still want to show the application if (metadata.isEmpty()) { onFetchMetadataFinished(installation, resource, metadata); } else { updateAppMetadata(resource, metadata); auto runtime = getRuntimeForApp(resource); if (!runtime || (runtime && !runtime->isInstalled())) { FlatpakFetchRemoteResourceJob *fetchRemoteResource = new FlatpakFetchRemoteResourceJob(runtimeUrl, this); connect(fetchRemoteResource, &FlatpakFetchRemoteResourceJob::jobFinished, this, [this, resource] (bool success, FlatpakResource *repoResource) { if (success) { installApplication(repoResource); } addResource(resource); }); fetchRemoteResource->start(); return; } else { addResource(resource); } } fw->deleteLater(); }); } else { addResource(resource); } return resource; } FlatpakResource * FlatpakBackend::addSourceFromFlatpakRepo(const QUrl &url) { Q_ASSERT(url.isLocalFile()); QSettings settings(url.toLocalFile(), QSettings::NativeFormat); const QString gpgKey = settings.value(QStringLiteral("Flatpak Repo/GPGKey")).toString(); const QString title = settings.value(QStringLiteral("Flatpak Repo/Title")).toString(); const QString repoUrl = settings.value(QStringLiteral("Flatpak Repo/Url")).toString(); if (gpgKey.isEmpty() || title.isEmpty() || repoUrl.isEmpty()) { return nullptr; } if (gpgKey.startsWith(QStringLiteral("http://")) || gpgKey.startsWith(QStringLiteral("https://"))) { return nullptr; } AppStream::Component asComponent; asComponent.addUrl(AppStream::Component::UrlKindHomepage, settings.value(QStringLiteral("Flatpak Repo/Homepage")).toString()); asComponent.setSummary(settings.value(QStringLiteral("Flatpak Repo/Comment")).toString()); asComponent.setDescription(settings.value(QStringLiteral("Flatpak Repo/Description")).toString()); asComponent.setName(title); asComponent.setId(settings.value(QStringLiteral("Flatpak Ref/Name")).toString()); const QString iconUrl = settings.value(QStringLiteral("Flatpak Repo/Icon")).toString(); if (!iconUrl.isEmpty()) { AppStream::Icon icon; icon.setKind(AppStream::Icon::KindRemote); icon.setUrl(QUrl(iconUrl)); asComponent.addIcon(icon); } auto resource = new FlatpakResource(asComponent, preferredInstallation(), this); // Use metadata only for stuff which are not common for all resources resource->addMetadata(QStringLiteral("gpg-key"), gpgKey); resource->addMetadata(QStringLiteral("repo-url"), repoUrl); resource->setBranch(settings.value(QStringLiteral("Flatpak Repo/DefaultBranch")).toString()); resource->setFlatpakName(url.fileName().remove(QStringLiteral(".flatpakrepo"))); resource->setType(FlatpakResource::Source); auto repo = flatpak_installation_get_remote_by_name(preferredInstallation(), resource->flatpakName().toUtf8().constData(), m_cancellable, nullptr); if (!repo) { resource->setState(AbstractResource::State::None); } else { resource->setState(AbstractResource::State::Installed); } return resource; } void FlatpakBackend::addResource(FlatpakResource *resource) { // Update app with all possible information we have if (!parseMetadataFromAppBundle(resource)) { qWarning() << "Failed to parse metadata from app bundle for" << resource->name(); } auto installation = resource->installation(); updateAppState(installation, resource); // This will update also metadata (required runtime) updateAppSize(installation, resource); m_resources.insert(resource->uniqueId(), resource); } bool FlatpakBackend::compareAppFlatpakRef(FlatpakInstallation *flatpakInstallation, FlatpakResource *resource, FlatpakInstalledRef *ref) const { return resource->uniqueId() == idForInstalledRef(flatpakInstallation, ref); } class FlatpakSource { public: FlatpakSource(FlatpakRemote* remote) : m_remote(remote) {} bool isEnabled() const { return !flatpak_remote_get_disabled(m_remote); } QString appstreamDir() const { g_autoptr(GFile) appstreamDir = flatpak_remote_get_appstream_dir(m_remote, nullptr); if (!appstreamDir) { qWarning() << "No appstream dir for" << flatpak_remote_get_name(m_remote); return {}; } return QString::fromUtf8(g_file_get_path(appstreamDir)); } QString name() const { return QString::fromUtf8(flatpak_remote_get_name(m_remote)); } private: FlatpakRemote* m_remote; }; void FlatpakBackend::finishInitialization() { loadInstalledApps(); checkForUpdates(); } void FlatpakBackend::loadAppsFromAppstreamData() { for (auto installation : qAsConst(m_installations)) { // Load applications from appstream metadata if (!loadAppsFromAppstreamData(installation)) { qWarning() << "Failed to load packages from appstream data from installation" << installation; } } } bool FlatpakBackend::loadAppsFromAppstreamData(FlatpakInstallation *flatpakInstallation) { Q_ASSERT(flatpakInstallation); GPtrArray *remotes = flatpak_installation_list_remotes(flatpakInstallation, m_cancellable, nullptr); if (!remotes) { return false; } m_refreshAppstreamMetadataJobs += remotes->len; for (uint i = 0; i < remotes->len; i++) { FlatpakRemote *remote = FLATPAK_REMOTE(g_ptr_array_index(remotes, i)); g_autoptr(GFile) fileTimestamp = flatpak_remote_get_appstream_timestamp(remote, nullptr); QFileInfo fileInfo = QFileInfo(QString::fromUtf8(g_file_get_path(fileTimestamp))); // Refresh appstream metadata in case they have never been refreshed or the cache is older than 6 hours if (!fileInfo.exists() || fileInfo.lastModified().toUTC().secsTo(QDateTime::currentDateTimeUtc()) > 21600) { refreshAppstreamMetadata(flatpakInstallation, remote); } else { integrateRemote(flatpakInstallation, remote); } } return true; } void FlatpakBackend::integrateRemote(FlatpakInstallation *flatpakInstallation, FlatpakRemote *remote) { m_refreshAppstreamMetadataJobs--; FlatpakSource source(remote); if (!source.isEnabled() || flatpak_remote_get_noenumerate(remote)) { return; } const QString appstreamDirPath = source.appstreamDir(); const QString appstreamIconsPath = source.appstreamDir() + QLatin1String("/icons/"); const QString appDirFileName = appstreamDirPath + QLatin1String("/appstream.xml.gz"); if (!QFile::exists(appDirFileName)) { qWarning() << "No" << appDirFileName << "appstream metadata found for" << source.name(); return; } auto fw = new QFutureWatcher>(this); fw->setFuture(QtConcurrent::run(&m_threadPool, [appDirFileName]() -> QList { AppStream::Metadata metadata; metadata.setFormatStyle(AppStream::Metadata::FormatStyleCollection); AppStream::Metadata::MetadataError error = metadata.parseFile(appDirFileName, AppStream::Metadata::FormatKindXml); if (error != AppStream::Metadata::MetadataErrorNoError) { qWarning() << "Failed to parse appstream metadata: " << error; return {}; } return metadata.components(); })); const auto sourceName = source.name(); connect(fw, &QFutureWatcher>::finished, this, [this, fw, flatpakInstallation, appstreamIconsPath, sourceName]() { const auto components = fw->result(); foreach (const AppStream::Component& appstreamComponent, components) { FlatpakResource *resource = new FlatpakResource(appstreamComponent, flatpakInstallation, this); resource->setIconPath(appstreamIconsPath); resource->setOrigin(sourceName); addResource(resource); } if (!m_refreshAppstreamMetadataJobs) { finishInitialization(); } fw->deleteLater(); }); } void FlatpakBackend::loadInstalledApps() { for (auto installation : qAsConst(m_installations)) { // Load installed applications and update existing resources with info from installed application if (!loadInstalledApps(installation)) { qWarning() << "Failed to load installed packages from installation" << installation; } } } bool FlatpakBackend::loadInstalledApps(FlatpakInstallation *flatpakInstallation) { Q_ASSERT(flatpakInstallation); g_autoptr(GError) localError = nullptr; g_autoptr(GPtrArray) refs = flatpak_installation_list_installed_refs(flatpakInstallation, m_cancellable, &localError); if (!refs) { qWarning() << "Failed to get list of installed refs for listing updates:" << localError->message; return false; } const QString pathExports = FlatpakResource::installationPath(flatpakInstallation) + QLatin1String("/exports/"); const QString pathApps = pathExports + QLatin1String("share/applications/"); for (uint i = 0; i < refs->len; i++) { FlatpakInstalledRef *ref = FLATPAK_INSTALLED_REF(g_ptr_array_index(refs, i)); if (flatpak_ref_get_kind(FLATPAK_REF(ref)) == FLATPAK_REF_KIND_RUNTIME) { continue; } const auto name = QLatin1String(flatpak_ref_get_name(FLATPAK_REF(ref))); AppStream::Metadata metadata; const QString fnDesktop = pathApps + name + QLatin1String(".desktop"); AppStream::Metadata::MetadataError error = metadata.parseFile(fnDesktop, AppStream::Metadata::FormatKindDesktopEntry); if (error != AppStream::Metadata::MetadataErrorNoError) { qWarning() << "Failed to parse appstream metadata: " << error << fnDesktop; continue; } const AppStream::Component appstreamComponent(metadata.component()); const auto res = getAppForInstalledRef(flatpakInstallation, ref); if (res) { res->setState(AbstractResource::Installed); continue; } FlatpakResource *resource = new FlatpakResource(appstreamComponent, flatpakInstallation, this); resource->setIconPath(pathExports); resource->setState(AbstractResource::Installed); resource->setOrigin(QString::fromUtf8(flatpak_installed_ref_get_origin(ref))); resource->updateFromRef(FLATPAK_REF(ref)); addResource(resource); } return true; } void FlatpakBackend::loadLocalUpdates(FlatpakInstallation *flatpakInstallation) { g_autoptr(GError) localError = nullptr; g_autoptr(GPtrArray) refs = nullptr; refs = flatpak_installation_list_installed_refs(flatpakInstallation, m_cancellable, &localError); if (!refs) { qWarning() << "Failed to get list of installed refs for listing updates:" << localError->message; return; } for (uint i = 0; i < refs->len; i++) { FlatpakInstalledRef *ref = FLATPAK_INSTALLED_REF(g_ptr_array_index(refs, i)); const gchar *latestCommit = flatpak_installed_ref_get_latest_commit(ref); if (!latestCommit) { qWarning() << "Couldn't get latest commit for" << flatpak_ref_get_name(FLATPAK_REF(ref)); continue; } const gchar *commit = flatpak_ref_get_commit(FLATPAK_REF(ref)); if (g_strcmp0(commit, latestCommit) == 0) { continue; } FlatpakResource *resource = getAppForInstalledRef(flatpakInstallation, ref); if (resource) { resource->setState(AbstractResource::Upgradeable); updateAppSize(flatpakInstallation, resource); } } } void FlatpakBackend::loadRemoteUpdates(FlatpakInstallation* installation) { auto fw = new QFutureWatcher(this); fw->setFuture(QtConcurrent::run(&m_threadPool, [installation]() -> GPtrArray * { g_autoptr(GCancellable) cancellable = g_cancellable_new(); g_autoptr(GError) localError = nullptr; GPtrArray *refs = flatpak_installation_list_installed_refs_for_update(installation, cancellable, &localError); if (!refs) { qWarning() << "Failed to get list of installed refs for listing updates: " << localError->message; } return refs; })); connect(fw, &QFutureWatcher::finished, this, [this, installation, fw](){ auto refs = fw->result(); onFetchUpdatesFinished(installation, refs); fw->deleteLater(); }); } void FlatpakBackend::onFetchUpdatesFinished(FlatpakInstallation *flatpakInstallation, GPtrArray *updates) { g_autoptr(GPtrArray) fetchedUpdates = updates; for (uint i = 0; i < fetchedUpdates->len; i++) { FlatpakInstalledRef *ref = FLATPAK_INSTALLED_REF(g_ptr_array_index(fetchedUpdates, i)); FlatpakResource *resource = getAppForInstalledRef(flatpakInstallation, ref); if (resource) { resource->setState(AbstractResource::Upgradeable); updateAppSize(flatpakInstallation, resource); } } } bool FlatpakBackend::parseMetadataFromAppBundle(FlatpakResource *resource) { g_autoptr(FlatpakRef) ref = nullptr; g_autoptr(GError) localError = nullptr; AppStream::Bundle bundle = resource->appstreamComponent().bundle(AppStream::Bundle::KindFlatpak); // Get arch/branch/commit/name from FlatpakRef if (!bundle.isEmpty()) { ref = flatpak_ref_parse(bundle.id().toUtf8().constData(), &localError); if (!ref) { qWarning() << "Failed to parse" << bundle.id() << localError->message; return false; } else { resource->updateFromRef(ref); } } return true; } class FlatpakRefreshAppstreamMetadataJob : public QThread { Q_OBJECT public: FlatpakRefreshAppstreamMetadataJob(FlatpakInstallation *installation, FlatpakRemote *remote) : QThread() , m_cancellable(g_cancellable_new()) , m_installation(installation) , m_remote(remote) { connect(this, &FlatpakRefreshAppstreamMetadataJob::finished, this, &QObject::deleteLater); } ~FlatpakRefreshAppstreamMetadataJob() { g_object_unref(m_cancellable); } void cancel() { g_cancellable_cancel(m_cancellable); } void run() override { g_autoptr(GError) localError = nullptr; #if FLATPAK_CHECK_VERSION(0,9,4) // With Flatpak 0.9.4 we can use flatpak_installation_update_appstream_full_sync() providing progress reporting which we don't use at this moment, but still // better to use newer function in case the previous one gets deprecated if (!flatpak_installation_update_appstream_full_sync(m_installation, flatpak_remote_get_name(m_remote), nullptr, nullptr, nullptr, nullptr, m_cancellable, &localError)) { #else if (!flatpak_installation_update_appstream_sync(m_installation, flatpak_remote_get_name(m_remote), nullptr, nullptr, m_cancellable, &localError)) { #endif qWarning() << "Failed to refresh appstream metadata for " << flatpak_remote_get_name(m_remote) << ": " << (localError ? localError->message : ""); Q_EMIT jobRefreshAppstreamMetadataFailed(); } else { Q_EMIT jobRefreshAppstreamMetadataFinished(m_installation, m_remote); } } Q_SIGNALS: void jobRefreshAppstreamMetadataFailed(); void jobRefreshAppstreamMetadataFinished(FlatpakInstallation *installation, FlatpakRemote *remote); private: GCancellable *m_cancellable; FlatpakInstallation *m_installation; FlatpakRemote *m_remote; }; void FlatpakBackend::refreshAppstreamMetadata(FlatpakInstallation *installation, FlatpakRemote *remote) { FlatpakRefreshAppstreamMetadataJob *job = new FlatpakRefreshAppstreamMetadataJob(installation, remote); connect(job, &FlatpakRefreshAppstreamMetadataJob::jobRefreshAppstreamMetadataFailed, this, [this] () { m_refreshAppstreamMetadataJobs--; }); connect(job, &FlatpakRefreshAppstreamMetadataJob::jobRefreshAppstreamMetadataFinished, this, &FlatpakBackend::integrateRemote); job->start(); } bool FlatpakBackend::setupFlatpakInstallations(GError **error) { GPtrArray *installations = flatpak_get_system_installations(m_cancellable, error); if (*error) { qWarning() << "Failed to call flatpak_get_system_installations:" << (*error)->message; } for (uint i = 0; installations && i < installations->len; i++) { m_installations << FLATPAK_INSTALLATION(g_ptr_array_index(installations, i)); } auto user = flatpak_installation_new_user(m_cancellable, error); if (user) { m_installations << user; } return !m_installations.isEmpty(); } void FlatpakBackend::updateAppInstalledMetadata(FlatpakInstalledRef *installedRef, FlatpakResource *resource) { // Update the rest resource->updateFromRef(FLATPAK_REF(installedRef)); resource->setInstalledSize(flatpak_installed_ref_get_installed_size(installedRef)); resource->setOrigin(QString::fromUtf8(flatpak_installed_ref_get_origin(installedRef))); if (resource->state() < AbstractResource::Installed) resource->setState(AbstractResource::Installed); } bool FlatpakBackend::updateAppMetadata(FlatpakInstallation* flatpakInstallation, FlatpakResource *resource) { QByteArray metadataContent; g_autoptr(GFile) installationPath = nullptr; if (resource->type() != FlatpakResource::DesktopApp) { return true; } installationPath = flatpak_installation_get_path(flatpakInstallation); const QString path = QString::fromUtf8(g_file_get_path(installationPath)) + QStringLiteral("/app/%1/%2/%3/active/metadata").arg(resource->flatpakName()).arg(resource->arch()).arg(resource->branch()); if (QFile::exists(path)) { return updateAppMetadata(resource, path); } else { auto fw = new QFutureWatcher(this); fw->setFuture(QtConcurrent::run(&m_threadPool, &FlatpakRunnables::fetchMetadata, flatpakInstallation, resource)); connect(fw, &QFutureWatcher::finished, this, [this, flatpakInstallation, resource, fw]() { const auto metadata = fw->result(); if (!metadata.isEmpty()) onFetchMetadataFinished(flatpakInstallation, resource, metadata); fw->deleteLater(); }); // Return false to indicate we cannot continue (right now used only in updateAppSize()) return false; } } void FlatpakBackend::onFetchMetadataFinished(FlatpakInstallation *flatpakInstallation, FlatpakResource *resource, const QByteArray &metadata) { updateAppMetadata(resource, metadata); // Right now we attempt to update metadata for calculating the size so call updateSizeFromRemote() // as it's what we want. In future if there are other reason to update metadata we will need to somehow // distinguish betwen these calls updateAppSizeFromRemote(flatpakInstallation, resource); } bool FlatpakBackend::updateAppMetadata(FlatpakResource *resource, const QString &path) { // Parse the temporary file QSettings setting(path, QSettings::NativeFormat); setting.beginGroup(QLatin1String("Application")); // Set the runtime in form of name/arch/version which can be later easily parsed resource->setRuntime(setting.value(QLatin1String("runtime")).toString()); // TODO get more information? return true; } bool FlatpakBackend::updateAppMetadata(FlatpakResource *resource, const QByteArray &data) { // Save the content to temporary file QTemporaryFile tempFile; tempFile.setAutoRemove(false); if (!tempFile.open()) { qWarning() << "Failed to get metadata file"; return false; } tempFile.write(data); tempFile.close(); updateAppMetadata(resource, tempFile.fileName()); tempFile.remove(); return true; } bool FlatpakBackend::updateAppSize(FlatpakInstallation *flatpakInstallation, FlatpakResource *resource) { // Check if the size is already set, we should also distiguish between download and installed size, // right now it doesn't matter whether we get size for installed or not installed app, but if we // start making difference then for not installed app check download and install size separately if (resource->state() == AbstractResource::Installed) { // The size appears to be already set (from updateAppInstalledMetadata() apparently) if (resource->installedSize() > 0) { return true; } } else { if (resource->installedSize() > 0 && resource->downloadSize() > 0) { return true; } } // Check if we know the needed runtime which is needed for calculating the size if (resource->runtime().isEmpty()) { if (!updateAppMetadata(flatpakInstallation, resource)) { return false; } } return updateAppSizeFromRemote(flatpakInstallation, resource); } bool FlatpakBackend::updateAppSizeFromRemote(FlatpakInstallation *flatpakInstallation, FlatpakResource *resource) { // Calculate the runtime size if (resource->state() == AbstractResource::None && resource->type() == FlatpakResource::DesktopApp) { auto runtime = getRuntimeForApp(resource); if (runtime) { // Re-check runtime state if case a new one was created updateAppState(flatpakInstallation, runtime); if (!runtime->isInstalled()) { if (!updateAppSize(flatpakInstallation, runtime)) { qWarning() << "Failed to get runtime size needed for total size of" << resource->name(); return false; } // Set required download size to include runtime size even now, in case we fail to // get the app size (e.g. when installing bundles where download size is 0) resource->setDownloadSize(runtime->downloadSize()); } } } if (resource->state() == AbstractResource::Installed) { g_autoptr(FlatpakInstalledRef) ref = nullptr; ref = getInstalledRefForApp(flatpakInstallation, resource); if (!ref) { qWarning() << "Failed to get installed size of" << resource->name(); return false; } resource->setInstalledSize(flatpak_installed_ref_get_installed_size(ref)); } else { if (resource->origin().isEmpty()) { qWarning() << "Failed to get size of" << resource->name() << " because of missing origin"; return false; } auto futureWatcher = new QFutureWatcher(this); futureWatcher->setFuture(QtConcurrent::run(&m_threadPool, &FlatpakRunnables::fetchFlatpakSize, flatpakInstallation, resource)); connect(futureWatcher, &QFutureWatcher::finished, this, [this, resource, futureWatcher]() { auto value = futureWatcher->result(); if (value.valid) { onFetchSizeFinished(resource, value.downloadSize, value.installedSize); } else { resource->setPropertyState(FlatpakResource::DownloadSize, FlatpakResource::UnknownOrFailed); resource->setPropertyState(FlatpakResource::InstalledSize, FlatpakResource::UnknownOrFailed); } futureWatcher->deleteLater(); }); } return true; } void FlatpakBackend::onFetchSizeFinished(FlatpakResource *resource, guint64 downloadSize, guint64 installedSize) { FlatpakResource *runtime = nullptr; if (resource->state() == AbstractResource::None && resource->type() == FlatpakResource::DesktopApp) { runtime = getRuntimeForApp(resource); } if (runtime && !runtime->isInstalled()) { resource->setDownloadSize(runtime->downloadSize() + downloadSize); resource->setInstalledSize(installedSize); } else { resource->setDownloadSize(downloadSize); resource->setInstalledSize(installedSize); } } void FlatpakBackend::updateAppState(FlatpakInstallation *flatpakInstallation, FlatpakResource *resource) { FlatpakInstalledRef *ref = getInstalledRefForApp(flatpakInstallation, resource); if (ref) { // If the app is installed, we can set information about commit, arch etc. updateAppInstalledMetadata(ref, resource); } else { // TODO check if the app is actuall still available resource->setState(AbstractResource::None); } } void FlatpakBackend::setFetching(bool fetching) { if (m_fetching != fetching) { m_fetching = fetching; emit fetchingChanged(); } } int FlatpakBackend::updatesCount() const { return m_updater->updatesCount(); } bool FlatpakBackend::flatpakResourceLessThan(AbstractResource* l, AbstractResource* r) { return (l->isInstalled() != r->isInstalled()) ? l->isInstalled() : (l->origin() != r->origin()) ? l->origin() < r->origin() : l < r; } ResultsStream * FlatpakBackend::search(const AbstractResourcesBackend::Filters &filter) { if (filter.resourceUrl.fileName().endsWith(QLatin1String(".flatpakrepo")) || filter.resourceUrl.fileName().endsWith(QLatin1String(".flatpakref"))) { auto stream = new ResultsStream(QStringLiteral("FlatpakStream-http-")+filter.resourceUrl.fileName()); FlatpakFetchRemoteResourceJob *fetchResourceJob = new FlatpakFetchRemoteResourceJob(filter.resourceUrl, this); connect(fetchResourceJob, &FlatpakFetchRemoteResourceJob::jobFinished, this, [fetchResourceJob, stream] (bool success, FlatpakResource *resource) { if (success) { stream->resourcesFound({resource}); } stream->finish(); fetchResourceJob->deleteLater(); }); fetchResourceJob->start(); return stream; - } else if (!filter.resourceUrl.isEmpty() && filter.resourceUrl.scheme() != QLatin1String("appstream")) + } else if ((!filter.resourceUrl.isEmpty() && filter.resourceUrl.scheme() != QLatin1String("appstream")) || !filter.extends.isEmpty()) return new ResultsStream(QStringLiteral("FlatpakStream-void"), {}); QVector ret; foreach(AbstractResource* r, m_resources) { if (r->isTechnical() && filter.state != AbstractResource::Upgradeable) { continue; } if (!filter.resourceUrl.isEmpty() && filter.resourceUrl.host().compare(r->appstreamId(), Qt::CaseInsensitive) != 0) continue; if (r->state() < filter.state) continue; if (filter.search.isEmpty() || r->name().contains(filter.search, Qt::CaseInsensitive) || r->comment().contains(filter.search, Qt::CaseInsensitive)) { ret += r; } } auto f = [this](AbstractResource* l, AbstractResource* r) { return flatpakResourceLessThan(l,r); }; std::sort(ret.begin(), ret.end(), f); return new ResultsStream(QStringLiteral("FlatpakStream"), ret); } ResultsStream * FlatpakBackend::findResourceByPackageName(const QUrl &url) { QVector resources; if (url.scheme() == QLatin1String("appstream")) { if (url.host().isEmpty()) passiveMessage(i18n("Malformed appstream url '%1'", url.toDisplayString())); else { foreach(FlatpakResource* res, m_resources) { if (QString::compare(res->appstreamId(), url.host(), Qt::CaseInsensitive)==0) resources << res; } } } return new ResultsStream(QStringLiteral("FlatpakStream"), resources); } AbstractBackendUpdater * FlatpakBackend::backendUpdater() const { return m_updater; } AbstractReviewsBackend * FlatpakBackend::reviewsBackend() const { return m_reviews.data(); } Transaction* FlatpakBackend::installApplication(AbstractResource *app, const AddonList &addons) { Q_UNUSED(addons); FlatpakResource *resource = qobject_cast(app); if (resource->type() == FlatpakResource::Source) { // Let source backend handle this FlatpakRemote *remote = m_sources->installSource(resource); if (remote) { resource->setState(AbstractResource::Installed); // Make sure we update appstream metadata first // FIXME we have to let flatpak to return the remote as the one created by FlatpakSourcesBackend will not have appstream directory refreshAppstreamMetadata(preferredInstallation(), flatpak_installation_get_remote_by_name(preferredInstallation(), flatpak_remote_get_name(remote), nullptr, nullptr)); } return nullptr; } FlatpakTransaction *transaction = nullptr; FlatpakInstallation *installation = resource->installation(); if (resource->propertyState(FlatpakResource::RequiredRuntime) == FlatpakResource::NotKnownYet && resource->type() == FlatpakResource::DesktopApp) { transaction = new FlatpakTransaction(resource, Transaction::InstallRole, true); connect(resource, &FlatpakResource::propertyStateChanged, [resource, transaction, this] (FlatpakResource::PropertyKind kind, FlatpakResource::PropertyState state) { if (kind != FlatpakResource::RequiredRuntime) { return; } if (state == FlatpakResource::AlreadyKnown) { FlatpakResource *runtime = getRuntimeForApp(resource); if (runtime && !runtime->isInstalled()) { transaction->setRuntime(runtime); } } transaction->start(); }); } else { FlatpakResource *runtime = getRuntimeForApp(resource); if (runtime && !runtime->isInstalled()) { transaction = new FlatpakTransaction(resource, runtime, Transaction::InstallRole); } else { transaction = new FlatpakTransaction(resource, Transaction::InstallRole); } } connect(transaction, &FlatpakTransaction::statusChanged, [this, installation, resource] (Transaction::Status status) { if (status == Transaction::Status::DoneStatus) { updateAppState(installation, resource); } }); return transaction; } Transaction* FlatpakBackend::installApplication(AbstractResource *app) { return installApplication(app, {}); } Transaction* FlatpakBackend::removeApplication(AbstractResource *app) { FlatpakResource *resource = qobject_cast(app); if (resource->type() == FlatpakResource::Source) { // Let source backend handle this if (m_sources->removeSource(resource->flatpakName())) { resource->setState(AbstractResource::None); } return nullptr; } FlatpakInstallation *installation = resource->installation(); FlatpakTransaction *transaction = new FlatpakTransaction(resource, Transaction::RemoveRole); connect(transaction, &FlatpakTransaction::statusChanged, [this, installation, resource] (Transaction::Status status) { if (status == Transaction::Status::DoneStatus) { updateAppSize(installation, resource); } }); return transaction; } void FlatpakBackend::checkForUpdates() { for (auto installation : qAsConst(m_installations)) { // Load local updates, comparing current and latest commit loadLocalUpdates(installation); // Load updates from remote repositories loadRemoteUpdates(installation); } } AbstractResource * FlatpakBackend::resourceForFile(const QUrl &url) { if (!url.isLocalFile()) { return nullptr; } FlatpakResource *resource = nullptr; if (url.path().endsWith(QLatin1String(".flatpak"))) { resource = addAppFromFlatpakBundle(url); } else if (url.path().endsWith(QLatin1String(".flatpakref"))) { resource = addAppFromFlatpakRef(url); } else if (url.path().endsWith(QLatin1String(".flatpakrepo"))) { resource = addSourceFromFlatpakRepo(url); } return resource; } QString FlatpakBackend::displayName() const { return QStringLiteral("Flatpak"); } #include "FlatpakBackend.moc" diff --git a/libdiscover/backends/PackageKitBackend/PackageKitBackend.cpp b/libdiscover/backends/PackageKitBackend/PackageKitBackend.cpp index 3dd78b3e..9eb4601a 100644 --- a/libdiscover/backends/PackageKitBackend/PackageKitBackend.cpp +++ b/libdiscover/backends/PackageKitBackend/PackageKitBackend.cpp @@ -1,638 +1,637 @@ /*************************************************************************** * Copyright © 2012 Aleix Pol Gonzalez * * Copyright © 2013 Lukas Appelhans * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #include "PackageKitBackend.h" #include "PackageKitSourcesBackend.h" #include "PackageKitResource.h" #include "PackageKitUpdater.h" #include "AppPackageKitResource.h" #include "PKTransaction.h" #include "LocalFilePKResource.h" #include "TransactionSet.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "utils.h" #include "config-paths.h" DISCOVER_BACKEND_PLUGIN(PackageKitBackend) QString PackageKitBackend::locateService(const QString &filename) { return QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("applications/")+filename); } PackageKitBackend::PackageKitBackend(QObject* parent) : AbstractResourcesBackend(parent) , m_updater(new PackageKitUpdater(this)) , m_refresher(nullptr) , m_isFetching(0) , m_reviews(AppStreamIntegration::global()->reviews()) { QTimer* t = new QTimer(this); connect(t, &QTimer::timeout, this, &PackageKitBackend::refreshDatabase); t->setInterval(60 * 60 * 1000); t->setSingleShot(false); t->start(); m_delayedDetailsFetch.setSingleShot(true); m_delayedDetailsFetch.setInterval(0); connect(&m_delayedDetailsFetch, &QTimer::timeout, this, &PackageKitBackend::performDetailsFetch); connect(PackageKit::Daemon::global(), &PackageKit::Daemon::updatesChanged, this, &PackageKitBackend::fetchUpdates); connect(PackageKit::Daemon::global(), &PackageKit::Daemon::isRunningChanged, this, &PackageKitBackend::checkDaemonRunning); connect(m_reviews.data(), &OdrsReviewsBackend::ratingsReady, this, &AbstractResourcesBackend::emitRatingsReady); SourcesModel::global()->addSourcesBackend(new PackageKitSourcesBackend(this)); QString error; const bool b = m_appdata.load(&error); reloadPackageList(); if (!b && m_packages.packages.isEmpty()) { qWarning() << "Could not open the AppStream metadata pool" << error; QTimer::singleShot(0, this, [this]() { Q_EMIT passiveMessage(i18n("Please make sure that Appstream is properly set up on your system")); }); } } PackageKitBackend::~PackageKitBackend() = default; bool PackageKitBackend::isFetching() const { return m_isFetching; } void PackageKitBackend::acquireFetching(bool f) { if (f) m_isFetching++; else m_isFetching--; if ((!f && m_isFetching==0) || (f && m_isFetching==1)) { emit fetchingChanged(); } Q_ASSERT(m_isFetching>=0); } void PackageKitBackend::reloadPackageList() { acquireFetching(true); if (m_refresher) { disconnect(m_refresher.data(), &PackageKit::Transaction::finished, this, &PackageKitBackend::reloadPackageList); } const auto components = m_appdata.components(); QStringList neededPackages; neededPackages.reserve(components.size()); foreach(const AppStream::Component& component, components) { if (component.kind() == AppStream::Component::KindFirmware) continue; const auto pkgNames = component.packageNames(); if (pkgNames.isEmpty()) { if (component.kind() == AppStream::Component::KindDesktopApp) { const QString file = locateService(component.desktopId()); if (!file.isEmpty()) { auto trans = PackageKit::Daemon::searchFiles(file); connect(trans, &PackageKit::Transaction::package, this, [trans](PackageKit::Transaction::Info info, const QString &packageID){ if (info == PackageKit::Transaction::InfoInstalled) trans->setProperty("installedPackage", packageID); }); connect(trans, &PackageKit::Transaction::finished, this, [this, trans, component](PackageKit::Transaction::Exit status) { const auto pkgidVal = trans->property("installedPackage"); if (status == PackageKit::Transaction::ExitSuccess && !pkgidVal.isNull()) { const auto pkgid = pkgidVal.toString(); acquireFetching(true); auto res = addComponent(component, {PackageKit::Daemon::packageName(pkgid)}); res->addPackageId(PackageKit::Transaction::InfoInstalled, pkgid, true); acquireFetching(false); } }); continue; } } qDebug() << "no packages for" << component.name(); continue; } neededPackages += pkgNames; addComponent(component, pkgNames); } acquireFetching(false); neededPackages.removeDuplicates(); resolvePackages(neededPackages); } AppPackageKitResource* PackageKitBackend::addComponent(const AppStream::Component& component, const QStringList& pkgNames) { Q_ASSERT(isFetching()); Q_ASSERT(!pkgNames.isEmpty()); const auto res = new AppPackageKitResource(component, pkgNames.at(0), this); m_packages.packages[component.id()] = res; foreach (const QString& pkg, pkgNames) { m_packages.packageToApp[pkg] += component.id(); } foreach (const QString& pkg, component.extends()) { m_packages.extendedBy[pkg] += res; } return res; } void PackageKitBackend::clearPackages(const QStringList& packageNames) { const auto resources = resourcesByPackageNames>(packageNames); for(auto res: resources) { qobject_cast(res)->clearPackageIds(); } } void PackageKitBackend::resolvePackages(const QStringList &packageNames) { PackageKit::Transaction * tArch = PackageKit::Daemon::resolve(packageNames, PackageKit::Transaction::FilterArch); connect(tArch, &PackageKit::Transaction::package, this, &PackageKitBackend::addPackageArch); connect(tArch, &PackageKit::Transaction::errorCode, this, &PackageKitBackend::transactionError); PackageKit::Transaction * tNotArch = PackageKit::Daemon::resolve(packageNames, PackageKit::Transaction::FilterNotArch); connect(tNotArch, &PackageKit::Transaction::package, this, &PackageKitBackend::addPackageNotArch); connect(tNotArch, &PackageKit::Transaction::errorCode, this, &PackageKitBackend::transactionError); TransactionSet* merge = new TransactionSet({tArch, tNotArch}); connect(merge, &TransactionSet::allFinished, this, &PackageKitBackend::getPackagesFinished); fetchUpdates(); } void PackageKitBackend::fetchUpdates() { if (m_updater->isProgressing()) return; PackageKit::Transaction * tUpdates = PackageKit::Daemon::getUpdates(); connect(tUpdates, &PackageKit::Transaction::finished, this, &PackageKitBackend::getUpdatesFinished); connect(tUpdates, &PackageKit::Transaction::package, this, &PackageKitBackend::addPackageToUpdate); connect(tUpdates, &PackageKit::Transaction::errorCode, this, &PackageKitBackend::transactionError); m_updatesPackageId.clear(); m_hasSecurityUpdates = false; m_updater->setProgressing(true); } void PackageKitBackend::addPackageArch(PackageKit::Transaction::Info info, const QString& packageId, const QString& summary) { addPackage(info, packageId, summary, true); } void PackageKitBackend::addPackageNotArch(PackageKit::Transaction::Info info, const QString& packageId, const QString& summary) { addPackage(info, packageId, summary, false); } void PackageKitBackend::addPackage(PackageKit::Transaction::Info info, const QString &packageId, const QString &summary, bool arch) { const QString packageName = PackageKit::Daemon::packageName(packageId); QSet r = resourcesByPackageName(packageName); if (r.isEmpty()) { auto pk = new PackageKitResource(packageName, summary, this); r = { pk }; m_packagesToAdd.insert(pk); } foreach(auto res, r) static_cast(res)->addPackageId(info, packageId, arch); } void PackageKitBackend::getPackagesFinished() { for(auto it = m_packages.packages.cbegin(); it != m_packages.packages.cend(); ++it) { auto pkr = qobject_cast(it.value()); if (pkr->packages().isEmpty()) { // qWarning() << "Failed to find package for" << it.key(); m_packagesToDelete += pkr; } } includePackagesToAdd(); } void PackageKitBackend::includePackagesToAdd() { if (m_packagesToAdd.isEmpty() && m_packagesToDelete.isEmpty()) return; acquireFetching(true); foreach(PackageKitResource* res, m_packagesToAdd) { m_packages.packages[res->packageName()] = res; } foreach(PackageKitResource* res, m_packagesToDelete) { const auto pkgs = m_packages.packageToApp.value(res->packageName(), {res->packageName()}); foreach(const auto &pkg, pkgs) { auto res = m_packages.packages.take(pkg); if (res) { emit resourceRemoved(res); res->deleteLater(); } } } m_packagesToAdd.clear(); m_packagesToDelete.clear(); acquireFetching(false); } void PackageKitBackend::transactionError(PackageKit::Transaction::Error, const QString& message) { qWarning() << "Transaction error: " << message << sender(); Q_EMIT passiveMessage(message); } void PackageKitBackend::packageDetails(const PackageKit::Details& details) { const QSet resources = resourcesByPackageName(PackageKit::Daemon::packageName(details.packageId())); if (resources.isEmpty()) qWarning() << "couldn't find package for" << details.packageId(); foreach(AbstractResource* res, resources) { qobject_cast(res)->setDetails(details); } } QSet PackageKitBackend::resourcesByPackageName(const QString& name) const { return resourcesByPackageNames>({name}); } template T PackageKitBackend::resourcesByPackageNames(const QStringList &pkgnames) const { T ret; ret.reserve(pkgnames.size()); for(const QString &name : pkgnames) { const QStringList names = m_packages.packageToApp.value(name, QStringList(name)); foreach(const QString& name, names) { AbstractResource* res = m_packages.packages.value(name); if (res) ret += res; } } return ret; } void PackageKitBackend::checkForUpdates() { refreshDatabase(); } void PackageKitBackend::refreshDatabase() { if (!m_refresher) { acquireFetching(true); m_refresher = PackageKit::Daemon::refreshCache(false); connect(m_refresher.data(), &PackageKit::Transaction::errorCode, this, &PackageKitBackend::transactionError); connect(m_refresher.data(), &PackageKit::Transaction::finished, this, [this]() { reloadPackageList(); acquireFetching(false); }); } else { qWarning() << "already resetting"; } } ResultsStream* PackageKitBackend::search(const AbstractResourcesBackend::Filters& filter) { if (!filter.resourceUrl.isEmpty()) { return findResourceByPackageName(filter.resourceUrl); } else if (!filter.extends.isEmpty()) { const auto ext = kTransform>(m_packages.extendedBy[filter.extends], [](AppPackageKitResource* a){ return a; }); return new ResultsStream(QStringLiteral("PackageKitStream-extends"), ext); } else if (filter.search.isEmpty()) { return new ResultsStream(QStringLiteral("PackageKitStream-all"), kFilter>(m_packages.packages, [](AbstractResource* res) { return !res->isTechnical(); })); } else { const QList components = m_appdata.search(filter.search); const QStringList ids = kTransform(components, [](const AppStream::Component& comp) { return comp.id(); }); auto stream = new ResultsStream(QStringLiteral("PackageKitStream-search")); if (!ids.isEmpty()) { const auto resources = resourcesByPackageNames>(ids); QTimer::singleShot(0, this, [stream, resources] () { stream->resourcesFound(resources); }); } PackageKit::Transaction * tArch = PackageKit::Daemon::resolve(filter.search, PackageKit::Transaction::FilterArch); connect(tArch, &PackageKit::Transaction::package, this, &PackageKitBackend::addPackageArch); connect(tArch, &PackageKit::Transaction::package, stream, [stream](PackageKit::Transaction::Info /*info*/, const QString &packageId){ stream->setProperty("packageId", packageId); }); connect(tArch, &PackageKit::Transaction::finished, stream, [stream, ids, this](PackageKit::Transaction::Exit status) { getPackagesFinished(); if (status == PackageKit::Transaction::Exit::ExitSuccess) { const auto packageId = stream->property("packageId"); if (!packageId.isNull()) { const auto res = resourcesByPackageNames>({PackageKit::Daemon::packageName(packageId.toString())}); stream->resourcesFound(kFilter>(res, [ids](AbstractResource* res){ return !ids.contains(res->appstreamId()); })); } } stream->finish(); }, Qt::QueuedConnection); return stream; } } ResultsStream * PackageKitBackend::findResourceByPackageName(const QUrl& url) { AbstractResource* pkg = nullptr; if (url.host().isEmpty()) passiveMessage(i18n("Malformed appstream url '%1'", url.toDisplayString())); else if (url.scheme() == QLatin1String("appstream")) { static const QMap deprecatedAppstreamIds = { { QStringLiteral("org.kde.krita.desktop"), QStringLiteral("krita.desktop") }, { QStringLiteral("org.kde.digikam.desktop"), QStringLiteral("digikam.desktop") }, { QStringLiteral("org.kde.ktorrent.desktop"), QStringLiteral("ktorrent.desktop") }, { QStringLiteral("org.kde.gcompris.desktop"), QStringLiteral("gcompris.desktop") }, { QStringLiteral("org.kde.kmymoney.desktop"), QStringLiteral("kmymoney.desktop") }, { QStringLiteral("org.kde.kolourpaint.desktop"), QStringLiteral("kolourpaint.desktop") }, { QStringLiteral("org.blender.blender.desktop"), QStringLiteral("blender.desktop") }, }; const auto host = url.host(); if (host.isEmpty()) passiveMessage(i18n("Malformed appstream url '%1'", url.toDisplayString())); else { const auto deprecatedHost = deprecatedAppstreamIds.value(url.host()); //try this as fallback for (auto it = m_packages.packages.constBegin(), itEnd = m_packages.packages.constEnd(); it != itEnd; ++it) { if (it.key().compare(host, Qt::CaseInsensitive) == 0 || it.key().compare(deprecatedHost, Qt::CaseInsensitive) == 0) { pkg = it.value(); break; } } if (!pkg) qWarning() << "could not find" << host << deprecatedHost; } } return new ResultsStream(QStringLiteral("PackageKitStream-url"), pkg ? QVector{pkg} : QVector{}); } bool PackageKitBackend::hasSecurityUpdates() const { return m_hasSecurityUpdates; } int PackageKitBackend::updatesCount() const { return m_updatesPackageId.count(); } Transaction* PackageKitBackend::installApplication(AbstractResource* app, const AddonList& addons) { Transaction* t = nullptr; if(!addons.addonsToInstall().isEmpty()) { QVector appsToInstall; if(!app->isInstalled()) appsToInstall << app; foreach(const QString& toInstall, addons.addonsToInstall()) { appsToInstall += m_packages.packages.value(toInstall); Q_ASSERT(appsToInstall.last()); } t = new PKTransaction(appsToInstall, Transaction::ChangeAddonsRole); } if (!addons.addonsToRemove().isEmpty()) { QVector appsToRemove = kTransform>(addons.addonsToRemove(), [this](const QString& toRemove){ return m_packages.packages.value(toRemove); }); t = new PKTransaction(appsToRemove, Transaction::RemoveRole); } if (!app->isInstalled()) t = installApplication(app); return t; } Transaction* PackageKitBackend::installApplication(AbstractResource* app) { return new PKTransaction({app}, Transaction::InstallRole); } Transaction* PackageKitBackend::removeApplication(AbstractResource* app) { Q_ASSERT(!isFetching()); return new PKTransaction({app}, Transaction::RemoveRole); } QSet PackageKitBackend::upgradeablePackages() const { QSet ret; ret.reserve(m_updatesPackageId.size()); Q_FOREACH (const QString& pkgid, m_updatesPackageId) { const QString pkgname = PackageKit::Daemon::packageName(pkgid); const auto pkgs = resourcesByPackageName(pkgname); if (pkgs.isEmpty()) { qWarning() << "couldn't find resource for" << pkgid; } ret.unite(pkgs); } return ret; } void PackageKitBackend::addPackageToUpdate(PackageKit::Transaction::Info info, const QString& packageId, const QString& summary) { if (info == PackageKit::Transaction::InfoBlocked) { return; } if (info == PackageKit::Transaction::InfoSecurity) m_hasSecurityUpdates = true; m_updatesPackageId += packageId; addPackage(info, packageId, summary, true); } void PackageKitBackend::getUpdatesFinished(PackageKit::Transaction::Exit, uint) { if (!m_updatesPackageId.isEmpty()) { PackageKit::Transaction* transaction = PackageKit::Daemon::getDetails(m_updatesPackageId.toList()); connect(transaction, &PackageKit::Transaction::details, this, &PackageKitBackend::packageDetails); connect(transaction, &PackageKit::Transaction::errorCode, this, &PackageKitBackend::transactionError); connect(transaction, &PackageKit::Transaction::finished, this, &PackageKitBackend::getUpdatesDetailsFinished); } m_updater->setProgressing(false); includePackagesToAdd(); emit updatesCountChanged(); } void PackageKitBackend::getUpdatesDetailsFinished(PackageKit::Transaction::Exit exit, uint) { if (exit != PackageKit::Transaction::ExitSuccess) { qWarning() << "Couldn't figure out the updates on PackageKit backend" << exit; } } bool PackageKitBackend::isPackageNameUpgradeable(const PackageKitResource* res) const { return !upgradeablePackageId(res).isEmpty(); } QString PackageKitBackend::upgradeablePackageId(const PackageKitResource* res) const { QString name = res->packageName(); foreach (const QString& pkgid, m_updatesPackageId) { if (PackageKit::Daemon::packageName(pkgid) == name) return pkgid; } return QString(); } void PackageKitBackend::fetchDetails(const QString& pkgid) { if (!m_delayedDetailsFetch.isActive()) { m_delayedDetailsFetch.start(); } m_packageNamesToFetchDetails += pkgid; } void PackageKitBackend::performDetailsFetch() { Q_ASSERT(!m_packageNamesToFetchDetails.isEmpty()); const auto ids = m_packageNamesToFetchDetails.toList(); PackageKit::Transaction* transaction = PackageKit::Daemon::getDetails(ids); connect(transaction, &PackageKit::Transaction::details, this, &PackageKitBackend::packageDetails); connect(transaction, &PackageKit::Transaction::errorCode, this, &PackageKitBackend::transactionError); QSharedPointer> packageDependencies(new QMap); - auto trans = PackageKit::Daemon::installPackages(ids, PackageKit::Transaction::TransactionFlagSimulate); - connect(trans, &PackageKit::Transaction::errorCode, this, &PackageKitBackend::transactionError); + auto trans = PackageKit::Daemon::dependsOn(ids); connect(trans, &PackageKit::Transaction::package, this, [packageDependencies](PackageKit::Transaction::Info /*info*/, const QString &packageID, const QString &/*summary*/) { (*packageDependencies)[packageID] += 1; }); connect(trans, &PackageKit::Transaction::finished, this, [this, packageDependencies](PackageKit::Transaction::Exit /*status*/) { auto pkgDeps = (*packageDependencies); for (auto it = pkgDeps.constBegin(), itEnd = pkgDeps.constEnd(); it != itEnd; ++it) { const auto resources = resourcesByPackageName(PackageKit::Daemon::packageName(it.key())); for(auto resource : resources) { auto pkres = qobject_cast(resource); pkres->setDependenciesCount(it.value()); } } }); } void PackageKitBackend::checkDaemonRunning() { if (!PackageKit::Daemon::isRunning()) { qWarning() << "PackageKit stopped running!"; } } AbstractBackendUpdater* PackageKitBackend::backendUpdater() const { return m_updater; } QVector PackageKitBackend::extendedBy(const QString& id) const { return m_packages.extendedBy[id]; } AbstractReviewsBackend* PackageKitBackend::reviewsBackend() const { return m_reviews.data(); } AbstractResource * PackageKitBackend::resourceForFile(const QUrl& file) { QMimeDatabase db; const auto mime = db.mimeTypeForUrl(file); if ( mime.inherits(QLatin1String("application/vnd.debian.binary-package")) || mime.inherits(QLatin1String("application/x-rpm")) || mime.inherits(QLatin1String("application/x-tar")) || mime.inherits(QLatin1String("application/x-xz-compressed-tar")) ) { return new LocalFilePKResource(file, this); } return nullptr; } static QString readDistroName() { const QStringList osreleasenames = (QStringList() << QStringLiteral("/etc/os-release") << QStringLiteral("/usr/lib/os-release")); foreach (QString osrelease, osreleasenames) { QFile file(osrelease); if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { QByteArray line; while (!file.atEnd()) { line = file.readLine().trimmed(); if (line.startsWith("NAME=")) { auto output = line.right(line.length()-5); output = output.replace('\"',""); return QString::fromLocal8Bit(output); } } } } QProcess process; process.setEnvironment({QStringLiteral("LC_ALL=C")}); process.start(QStringLiteral("lsb_release"), {QStringLiteral("-sd")}); process.waitForFinished(); auto output = process.readAll().trimmed(); if (output.startsWith('\"') && output.endsWith('\"')) output = output.mid(1, output.length()-2); return QString::fromLocal8Bit(output); } QString PackageKitBackend::displayName() const { static const QString distro = readDistroName(); return distro; } #include "PackageKitBackend.moc"