From 21e1ee3a070ec9595fc0607f32dfd3df87718f7b Mon Sep 17 00:00:00 2001
From: Alexey Min <alexey.min@gmail.com>
Date: Sun, 12 Jan 2020 01:02:39 +0300
Subject: [PATCH 01/59] Initial support for AlpineAPK backend

---
 .../AlpineApkBackend/AlpineApkBackend.cpp     | 240 ++++++++++++++++++
 .../AlpineApkBackend/AlpineApkBackend.h       |  73 ++++++
 .../AlpineApkBackend/AlpineApkResource.cpp    | 208 +++++++++++++++
 .../AlpineApkBackend/AlpineApkResource.h      |  81 ++++++
 .../AlpineApkSourcesBackend.cpp               | 151 +++++++++++
 .../AlpineApkSourcesBackend.h                 |  50 ++++
 .../AlpineApkBackend/AlpineApkTransaction.cpp |  90 +++++++
 .../AlpineApkBackend/AlpineApkTransaction.h   |  46 ++++
 .../backends/AlpineApkBackend/CMakeLists.txt  |  52 ++++
 libdiscover/backends/CMakeLists.txt           |  13 +
 10 files changed, 1004 insertions(+)
 create mode 100644 libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp
 create mode 100644 libdiscover/backends/AlpineApkBackend/AlpineApkBackend.h
 create mode 100644 libdiscover/backends/AlpineApkBackend/AlpineApkResource.cpp
 create mode 100644 libdiscover/backends/AlpineApkBackend/AlpineApkResource.h
 create mode 100644 libdiscover/backends/AlpineApkBackend/AlpineApkSourcesBackend.cpp
 create mode 100644 libdiscover/backends/AlpineApkBackend/AlpineApkSourcesBackend.h
 create mode 100644 libdiscover/backends/AlpineApkBackend/AlpineApkTransaction.cpp
 create mode 100644 libdiscover/backends/AlpineApkBackend/AlpineApkTransaction.h
 create mode 100644 libdiscover/backends/AlpineApkBackend/CMakeLists.txt

diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp
new file mode 100644
index 000000000..f5aa5477d
--- /dev/null
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp
@@ -0,0 +1,240 @@
+/***************************************************************************
+ *   Copyright © 2020 Alexey Min <alexey.min@gmail.com>                    *
+ *                                                                         *
+ *   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 <http://www.gnu.org/licenses/>. *
+ ***************************************************************************/
+
+#include "AlpineApkBackend.h"
+#include "AlpineApkResource.h"
+//#include "DummyReviewsBackend.h"
+#include "AlpineApkTransaction.h"
+#include "AlpineApkSourcesBackend.h"
+#include "alpineapk_backend_logging.h"  // generated by ECM
+
+#include "resources/StandardBackendUpdater.h"
+#include "resources/SourcesModel.h"
+#include "Transaction/Transaction.h"
+#include "Category/Category.h"
+
+#include <KAboutData>
+#include <KLocalizedString>
+#include <KPluginFactory>
+#include <KConfigGroup>
+#include <KSharedConfig>
+#include <QDebug>
+#include <QLoggingCategory>
+#include <QThread>
+#include <QTimer>
+#include <QAction>
+
+DISCOVER_BACKEND_PLUGIN(AlpineApkBackend)
+
+AlpineApkBackend::AlpineApkBackend(QObject *parent)
+    : AbstractResourcesBackend(parent)
+    , m_updater(new StandardBackendUpdater(this))
+    , m_startElements(120)
+{
+#ifndef QT_DEBUG
+    const_cast<QLoggingCategory &>(LOG_ALPINEAPK()).setEnabled(QtDebugMsg, false);
+#endif
+
+    qCDebug(LOG_ALPINEAPK) << "constructing backend!";
+
+    QTimer::singleShot(500, this, &AlpineApkBackend::toggleFetching);
+    //connect(m_reviews, &DummyReviewsBackend::ratingsReady, this, &AbstractResourcesBackend::emitRatingsReady);
+    connect(m_updater, &StandardBackendUpdater::updatesCountChanged, this, &AlpineApkBackend::updatesCountChanged);
+
+    populate();
+    //if (!m_fetching)
+    //    m_reviews->initialize();
+
+    SourcesModel::global()->addSourcesBackend(new AlpineApkSourcesBackend(this));
+}
+
+QVector<Category *> AlpineApkBackend::category() const
+{
+    static Category *cat = new Category(
+                QStringLiteral("All applications"),    // displayName
+                QStringLiteral("applications-other"),  // icon
+                { }, // orFilters
+                { displayName() }, // pluginName
+                { }, // subCategories
+                QUrl(), // decoration (what is it?)
+                false  // isAddons
+    );
+    return { cat };
+}
+
+void AlpineApkBackend::populate()
+{
+    qCDebug(LOG_ALPINEAPK) << "populating resources...";
+
+    if (m_apkdb.open(QtApk::Database::QTAPK_OPENF_READONLY)) {
+        m_availablePackages = m_apkdb.getAvailablePackages();
+        m_installedPackages = m_apkdb.getInstalledPackages();
+        m_apkdb.close();
+    }
+
+    if (m_availablePackages.size() > 0) {
+        for (const QtApk::Package &pkg: m_availablePackages) {
+            AlpineApkResource *res = new AlpineApkResource(pkg, this);
+            res->setCategoryName(QStringLiteral("all"));
+            res->setOriginSource(QStringLiteral("apk"));
+            res->setSection(QStringLiteral("dummy"));
+            const QString key = pkg.name.toLower();
+            m_resources.insert(key, res);
+            connect(res, &AlpineApkResource::stateChanged, this, &AlpineApkBackend::updatesCountChanged);
+        }
+        qCDebug(LOG_ALPINEAPK) << " available" << m_availablePackages.size()
+                               << "packages";
+    }
+    if (m_installedPackages.size() > 0) {
+        for (const QtApk::Package &pkg: m_installedPackages) {
+            const QString key = pkg.name.toLower();
+            if (m_resources.contains(key)) {
+                m_resources.value(key)->setState(AbstractResource::Installed);
+            }
+        }
+        qCDebug(LOG_ALPINEAPK) << " installed" << m_installedPackages.size()
+                               << "packages";
+    }
+}
+
+void AlpineApkBackend::toggleFetching()
+{
+    m_fetching = !m_fetching;
+    qCDebug(LOG_ALPINEAPK) << "fetching..." << m_fetching;
+    emit fetchingChanged();
+    //if (!m_fetching)
+    //    m_reviews->initialize();
+}
+
+int AlpineApkBackend::updatesCount() const
+{
+    qCDebug(LOG_ALPINEAPK) << "updatesCount(): " << m_updater->updatesCount();
+    return m_updater->updatesCount();
+}
+
+ResultsStream *AlpineApkBackend::search(const AbstractResourcesBackend::Filters &filter)
+{
+    QVector<AbstractResource*> ret;
+    if (!filter.resourceUrl.isEmpty()) {
+        return findResourceByPackageName(filter.resourceUrl);
+    } else {
+        for (AbstractResource *r: qAsConst(m_resources)) {
+            if (r->type() == AbstractResource::Technical
+                    && filter.state != AbstractResource::Upgradeable) {
+                continue;
+            }
+            if (r->state() < filter.state) {
+                continue;
+            }
+            if(r->name().contains(filter.search, Qt::CaseInsensitive)
+                    || r->comment().contains(filter.search, Qt::CaseInsensitive)) {
+                ret += r;
+            }
+        }
+    }
+    return new ResultsStream(QStringLiteral("AlpineApkStream"), ret);
+}
+
+ResultsStream *AlpineApkBackend::findResourceByPackageName(const QUrl &searchUrl)
+{
+//    if (search.isLocalFile()) {
+//        AlpineApkResource* res = new AlpineApkResource(
+//                    search.fileName(), AbstractResource::Technical, this);
+//        res->setSize(666);
+//        res->setState(AbstractResource::None);
+//        m_resources.insert(res->packageName(), res);
+//        connect(res, &AlpineApkResource::stateChanged, this, &AlpineApkBackend::updatesCountChanged);
+//        return new ResultsStream(QStringLiteral("AlpineApkStream-local"), { res });
+//    }
+
+    AlpineApkResource *result = nullptr;
+
+    // QUrl("appstream://org.kde.krita.desktop")
+    // smart workaround for appstream
+    if (searchUrl.scheme()  == QLatin1String("appstream")) {
+        // remove leading "org.kde."
+        QString pkgName = searchUrl.host();
+        if (pkgName.startsWith(QLatin1String("org.kde."))) {
+            pkgName = pkgName.mid(8);
+        }
+        // remove trailing ".desktop"
+        if (pkgName.endsWith(QLatin1String(".desktop"))) {
+            pkgName = pkgName.left(pkgName.length() - 8);
+        }
+        // now we can search for "krita" package
+        result = m_resources.value(pkgName);
+    }
+
+    if (!result) {
+        return new ResultsStream(QStringLiteral("AlpineApkStream"), {});
+    }
+    return new ResultsStream(QStringLiteral("AlpineApkStream"), { result });
+}
+
+AbstractBackendUpdater *AlpineApkBackend::backendUpdater() const
+{
+    return m_updater;
+}
+
+AbstractReviewsBackend *AlpineApkBackend::reviewsBackend() const
+{
+    // qCDebug(LOG_ALPINEAPK) << "reviewsBbackend(): we don't support reviews (";
+    // return m_reviews;
+    return nullptr;
+}
+
+Transaction* AlpineApkBackend::installApplication(AbstractResource *app, const AddonList &addons)
+{
+    return new AlpineApkTransaction(qobject_cast<AlpineApkResource *>(app), addons, Transaction::InstallRole);
+}
+
+Transaction* AlpineApkBackend::installApplication(AbstractResource *app)
+{
+    return new AlpineApkTransaction(qobject_cast<AlpineApkResource *>(app), Transaction::InstallRole);
+}
+
+Transaction* AlpineApkBackend::removeApplication(AbstractResource *app)
+{
+    return new AlpineApkTransaction(qobject_cast<AlpineApkResource *>(app), Transaction::RemoveRole);
+}
+
+void AlpineApkBackend::checkForUpdates()
+{
+    if(m_fetching) {
+        qCDebug(LOG_ALPINEAPK) << "checkForUpdates(): already fetching";
+        return;
+    }
+    qCDebug(LOG_ALPINEAPK) << "checkForUpdates()!";
+    toggleFetching();
+    // populate(QStringLiteral("Moar"));
+    QTimer::singleShot(1000, this, &AlpineApkBackend::toggleFetching);
+}
+
+QString AlpineApkBackend::displayName() const
+{
+    return QStringLiteral("Alpine APK backend");
+}
+
+bool AlpineApkBackend::hasApplications() const
+{
+    return true;
+}
+
+#include "AlpineApkBackend.moc"
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.h b/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.h
new file mode 100644
index 000000000..624df1c9f
--- /dev/null
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.h
@@ -0,0 +1,73 @@
+/***************************************************************************
+ *   Copyright © 2020 Alexey Min <alexey.min@gmail.com>                    *
+ *                                                                         *
+ *   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 <http://www.gnu.org/licenses/>. *
+ ***************************************************************************/
+
+#ifndef AlpineApkBackend_H
+#define AlpineApkBackend_H
+
+#include <resources/AbstractResourcesBackend.h>
+#include <QVariantList>
+
+#include <QtApk.h>
+
+// class DummyReviewsBackend;
+class StandardBackendUpdater;
+class AlpineApkResource;
+class AlpineApkBackend : public AbstractResourcesBackend
+{
+    Q_OBJECT
+    Q_PROPERTY(int startElements MEMBER m_startElements)
+
+public:
+    explicit AlpineApkBackend(QObject *parent = nullptr);
+
+    QVector<Category *> category() const override;
+    int updatesCount() const override;
+    AbstractBackendUpdater *backendUpdater() const override;
+    AbstractReviewsBackend *reviewsBackend() const override;
+    ResultsStream *search(const AbstractResourcesBackend::Filters &filter) override;
+    ResultsStream *findResourceByPackageName(const QUrl &search);
+    QHash<QString, AlpineApkResource *> resources() const { return m_resources; }
+    bool isValid() const override { return true; } // No external file dependencies that could cause runtime errors
+
+    Transaction *installApplication(AbstractResource *app) override;
+    Transaction *installApplication(AbstractResource *app, const AddonList &addons) override;
+    Transaction *removeApplication(AbstractResource *app) override;
+    bool isFetching() const override { return m_fetching; }
+    void checkForUpdates() override;
+    QString displayName() const override;
+    bool hasApplications() const override;
+
+public Q_SLOTS:
+    void toggleFetching();
+
+private:
+    void populate();
+
+    QHash<QString, AlpineApkResource *> m_resources;
+    StandardBackendUpdater *m_updater;
+    // DummyReviewsBackend* m_reviews;
+    QtApk::Database m_apkdb;
+    QVector<QtApk::Package> m_availablePackages;
+    QVector<QtApk::Package> m_installedPackages;
+    bool m_fetching = true;
+    int m_startElements = 0;
+};
+
+#endif // AlpineApkBackend_H
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkResource.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkResource.cpp
new file mode 100644
index 000000000..d827e7b3b
--- /dev/null
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkResource.cpp
@@ -0,0 +1,208 @@
+/***************************************************************************
+ *   Copyright © 2020 Alexey Min <alexey.min@gmail.com>                    *
+ *                                                                         *
+ *   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 <http://www.gnu.org/licenses/>. *
+ ***************************************************************************/
+
+#include <KRandom>
+#include "AlpineApkResource.h"
+#include "alpineapk_backend_logging.h"  // generated by ECM
+#include "Transaction/AddonList.h"
+
+AlpineApkResource::AlpineApkResource(const QtApk::Package &apkPkg,
+                                     AbstractResourcesBackend *parent)
+    : AbstractResource(parent)
+    , m_state(AbstractResource::State::None)
+    , m_type(Application)
+    , m_pkg(apkPkg)
+{
+}
+
+QList<PackageState> AlpineApkResource::addonsInformation()
+{
+    return m_addons;
+}
+
+QString AlpineApkResource::availableVersion() const
+{
+    return m_pkg.version;
+}
+
+QStringList AlpineApkResource::categories()
+{
+    return { m_category };
+}
+
+QString AlpineApkResource::comment()
+{
+    return m_pkg.description;
+}
+
+int AlpineApkResource::size()
+{
+    return static_cast<int>(m_pkg.size);
+}
+
+QUrl AlpineApkResource::homepage()
+{
+    return QUrl::fromUserInput(m_pkg.url);
+}
+
+QUrl AlpineApkResource::helpURL()
+{
+    return QUrl();
+}
+
+QUrl AlpineApkResource::bugURL()
+{
+    return QUrl();
+}
+
+QUrl AlpineApkResource::donationURL()
+{
+    return QUrl();
+}
+
+QVariant AlpineApkResource::icon() const
+{
+    return QStringLiteral("package-x-generic");
+}
+
+QString AlpineApkResource::installedVersion() const
+{
+    return m_pkg.version;
+}
+
+QJsonArray AlpineApkResource::licenses()
+{
+    return {
+        QJsonObject {
+            { QStringLiteral("name"), m_pkg.license },
+            { QStringLiteral("url"), QStringLiteral("https://spdx.org/license-list") },
+        }
+    };
+}
+
+QString AlpineApkResource::longDescription()
+{
+    return m_pkg.description;
+}
+
+QString AlpineApkResource::name() const
+{
+    return m_pkg.name;
+}
+
+QString AlpineApkResource::origin() const
+{
+    return m_originSoruce;
+}
+
+QString AlpineApkResource::packageName() const
+{
+    return m_pkg.name;
+}
+
+QString AlpineApkResource::section()
+{
+    return m_sectionName;
+}
+
+AbstractResource::State AlpineApkResource::state()
+{
+    return m_state;
+}
+
+void AlpineApkResource::fetchChangelog()
+{
+    // QString log = longDescription();
+    // Q_EMIT changelogFetched(log);
+}
+
+void AlpineApkResource::fetchScreenshots()
+{
+    // Q_EMIT screenshotsFetched(m_screenshotThumbnails, m_screenshots);
+}
+
+void AlpineApkResource::setState(AbstractResource::State state)
+{
+    m_state = state;
+    emit stateChanged();
+}
+
+void AlpineApkResource::setCategoryName(const QString &categoryName)
+{
+    m_category = categoryName;
+}
+
+void AlpineApkResource::setOriginSource(const QString &originSource)
+{
+    m_originSoruce = originSource;
+}
+
+void AlpineApkResource::setSection(const QString &sectionName)
+{
+    m_sectionName = sectionName;
+}
+
+void AlpineApkResource::setAddons(const AddonList &addons)
+{
+    const QStringList addonsToInstall = addons.addonsToInstall();
+    for (const QString &toInstall : addonsToInstall) {
+        setAddonInstalled(toInstall, true);
+    }
+    const QStringList addonsToRemove = addons.addonsToRemove();
+    for (const QString &toRemove : addonsToRemove) {
+        setAddonInstalled(toRemove, false);
+    }
+}
+
+void AlpineApkResource::setAddonInstalled(const QString &addon, bool installed)
+{
+    for(PackageState &elem : m_addons) {
+        if(elem.name() == addon) {
+            elem.setInstalled(installed);
+        }
+    }
+}
+
+
+void AlpineApkResource::invokeApplication() const
+{
+    // QDesktopServices d;
+    // d.openUrl(QUrl(QStringLiteral("https://projects.kde.org/projects/extragear/sysadmin/muon")));
+}
+
+QUrl AlpineApkResource::url() const
+{
+    return QUrl(QLatin1String("apk://") + packageName());
+}
+
+QString AlpineApkResource::author() const
+{
+    return m_pkg.maintainer;
+}
+
+QString AlpineApkResource::sourceIcon() const
+{
+    return QStringLiteral("player-time");
+}
+
+QDate AlpineApkResource::releaseDate() const
+{
+    return m_pkg.buildTime.date();
+}
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkResource.h b/libdiscover/backends/AlpineApkBackend/AlpineApkResource.h
new file mode 100644
index 000000000..79b100e10
--- /dev/null
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkResource.h
@@ -0,0 +1,81 @@
+/***************************************************************************
+ *   Copyright © 2020 Alexey Min <alexey.min@gmail.com>                    *
+ *                                                                         *
+ *   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 <http://www.gnu.org/licenses/>. *
+ ***************************************************************************/
+
+#ifndef ALPINEAPKRESOURCE_H
+#define ALPINEAPKRESOURCE_H
+
+#include <resources/AbstractResource.h>
+#include <QtApkPackage.h>
+
+class AddonList;
+
+class AlpineApkResource : public AbstractResource
+{
+    Q_OBJECT
+
+public:
+    explicit AlpineApkResource(const QtApk::Package &apkPkg, AbstractResourcesBackend *parent);
+
+    QList<PackageState> addonsInformation() override;
+    QString section() override;
+    QString origin() const override;
+    QString longDescription() override;
+    QString availableVersion() const override;
+    QString installedVersion() const override;
+    QJsonArray licenses() override;
+    int size() override;
+    QUrl homepage() override;
+    QUrl helpURL() override;
+    QUrl bugURL() override;
+    QUrl donationURL() override;
+    QStringList categories() override;
+    AbstractResource::State state() override;
+    QVariant icon() const override;
+    QString comment() override;
+    QString name() const override;
+    QString packageName() const override;
+    AbstractResource::Type type() const override { return m_type; }
+    bool canExecute() const override { return true; }
+    void invokeApplication() const override;
+    void fetchChangelog() override;
+    void fetchScreenshots() override;
+    QUrl url() const override;
+    QString author() const override;
+    QString sourceIcon() const override;
+    QDate releaseDate() const override;
+
+    void setState(State state);
+    void setCategoryName(const QString &categoryName);
+    void setOriginSource(const QString &originSource);
+    void setSection(const QString &sectionName);
+    void setAddons(const AddonList &addons);
+    void setAddonInstalled(const QString &addon, bool installed);
+
+public:
+    AbstractResource::State m_state;
+    const AbstractResource::Type m_type;
+    QtApk::Package m_pkg;
+    QString m_category;
+    QString m_originSoruce;
+    QString m_sectionName;
+    QList<PackageState> m_addons;
+};
+
+#endif // ALPINEAPKRESOURCE_H
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkSourcesBackend.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkSourcesBackend.cpp
new file mode 100644
index 000000000..b964b40d1
--- /dev/null
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkSourcesBackend.cpp
@@ -0,0 +1,151 @@
+/***************************************************************************
+ *   Copyright © 2020 Alexey Min <alexey.min@gmail.com>                    *
+ *                                                                         *
+ *   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 <http://www.gnu.org/licenses/>. *
+ ***************************************************************************/
+
+#include "AlpineApkSourcesBackend.h"
+#include "alpineapk_backend_logging.h"  // generated by ECM
+
+#include <QDebug>
+#include <QAction>
+#include <QVector>
+
+// KF5
+#include <KLocalizedString>
+
+// libapk-qt
+#include <QtApk.h>
+
+AlpineApkSourcesBackend::AlpineApkSourcesBackend(AbstractResourcesBackend *parent)
+    : AbstractSourcesBackend(parent)
+    , m_sourcesModel(new QStandardItemModel(this))
+    , m_refreshAction(new QAction(QIcon::fromTheme(QStringLiteral("view-refresh")),
+                                  QStringLiteral("Refresh"), this))
+{
+    loadSources();
+    QObject::connect(m_refreshAction, &QAction::triggered,
+                     this, &AlpineApkSourcesBackend::loadSources);
+
+    // can be used to track enabling/disabling repo source
+    // QObject::connect(m_sourcesModel, &QStandardItemModel::itemChanged, this, [](QStandardItem* item) {
+    //     qCDebug(LOG_ALPINEAPK) << "source backend: DummySource changed" << item << item->checkState();
+    // });
+}
+
+QAbstractItemModel *AlpineApkSourcesBackend::sources()
+{
+    return m_sourcesModel;
+}
+
+bool AlpineApkSourcesBackend::addSource(const QString &id)
+{
+    return addSourceFull(id, QString(), true);
+}
+
+QStandardItem *AlpineApkSourcesBackend::sourceForId(const QString& id) const
+{
+    for (int i = 0, c = m_sourcesModel->rowCount(); i < c; ++i) {
+        QStandardItem *it = m_sourcesModel->item(i, 0);
+        if (it->text() == id) {
+            return it;
+        }
+    }
+    return nullptr;
+}
+
+bool AlpineApkSourcesBackend::addSourceFull(const QString &id, const QString &comment, bool enabled)
+{
+    if (id.isEmpty()) {
+        return false;
+    }
+
+    qCDebug(LOG_ALPINEAPK) << "source backend: Adding source:" << id;
+
+    QStandardItem *it = new QStandardItem(id);
+    it->setData(id, AbstractSourcesBackend::IdRole);
+    it->setData(comment, Qt::ToolTipRole);
+    it->setCheckable(true);
+    it->setCheckState(enabled ? Qt::Checked : Qt::Unchecked);
+    // for now, disable editing sources
+    it->setFlags(it->flags() & ~Qt::ItemIsEnabled);
+    m_sourcesModel->appendRow(it);
+    return true;
+}
+
+void AlpineApkSourcesBackend::loadSources()
+{
+    QVector<QtApk::Repository> repos = QtApk::Database::getRepositories();
+    m_sourcesModel->clear();
+    for (const QtApk::Repository &repo: repos) {
+        addSourceFull(repo.url, repo.comment, repo.enabled);
+    }
+}
+
+bool AlpineApkSourcesBackend::removeSource(const QString &id)
+{
+    const QStandardItem *it = sourceForId(id);
+    if (!it) {
+        qCWarning(LOG_ALPINEAPK) << "source backend: couldn't find " << id;
+        return false;
+    }
+    return m_sourcesModel->removeRow(it->row());
+}
+
+QString AlpineApkSourcesBackend::idDescription()
+{
+    return i18nc("Adding repo", "Enter apk repository URL, for example: "
+                     "http://dl-cdn.alpinelinux.org/alpine/edge/testing/");
+}
+
+QVariantList AlpineApkSourcesBackend::actions() const
+{
+    static const QVariantList s_actions {
+        QVariant::fromValue<QObject *>(m_refreshAction),
+    };
+    return s_actions;
+}
+
+bool AlpineApkSourcesBackend::supportsAdding() const
+{
+    return false; // for now, disable editing sources
+}
+
+bool AlpineApkSourcesBackend::canMoveSources() const
+{
+    return false; // for now, disable editing sources
+}
+
+bool AlpineApkSourcesBackend::moveSource(const QString& sourceId, int delta)
+{
+    int row = sourceForId(sourceId)->row();
+    QList<QStandardItem *> prevRow = m_sourcesModel->takeRow(row);
+    if (prevRow.isEmpty()) {
+        return false;
+    }
+
+    const int destRow = row + delta;
+    m_sourcesModel->insertRow(destRow, prevRow);
+    if (destRow == 0 || row == 0) {
+        Q_EMIT firstSourceIdChanged();
+    }
+    if (destRow == (m_sourcesModel->rowCount() - 1)
+            || row == (m_sourcesModel->rowCount() - 1)) {
+        Q_EMIT lastSourceIdChanged();
+    }
+    return true;
+}
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkSourcesBackend.h b/libdiscover/backends/AlpineApkBackend/AlpineApkSourcesBackend.h
new file mode 100644
index 000000000..578945917
--- /dev/null
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkSourcesBackend.h
@@ -0,0 +1,50 @@
+/***************************************************************************
+ *   Copyright © 2020 Alexey Min <alexey.min@gmail.com>                    *
+ *                                                                         *
+ *   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 <http://www.gnu.org/licenses/>. *
+ ***************************************************************************/
+
+#ifndef ALPINEAPKSOURCESBACKEND_H
+#define ALPINEAPKSOURCESBACKEND_H
+
+#include <resources/AbstractSourcesBackend.h>
+#include <QStandardItemModel>
+
+class AlpineApkSourcesBackend : public AbstractSourcesBackend
+{
+public:
+    explicit AlpineApkSourcesBackend(AbstractResourcesBackend *parent);
+
+    QAbstractItemModel *sources() override;
+    bool addSource(const QString &id) override;
+    bool removeSource(const QString &id) override;
+    QString idDescription() override;
+    QVariantList actions() const override;
+    bool supportsAdding() const override;
+    bool canMoveSources() const override;
+    bool moveSource(const QString &sourceId, int delta) override;
+
+private:
+    QStandardItem *sourceForId(const QString &id) const;
+    bool addSourceFull(const QString &id, const QString &comment, bool enabled);
+    void loadSources();
+
+    QStandardItemModel *m_sourcesModel = nullptr;
+    QAction *m_refreshAction = nullptr;
+};
+
+#endif // ALPINEAPKSOURCESBACKEND_H
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkTransaction.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkTransaction.cpp
new file mode 100644
index 000000000..b4f90df56
--- /dev/null
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkTransaction.cpp
@@ -0,0 +1,90 @@
+/***************************************************************************
+ *   Copyright © 2020 Alexey Min <alexey.min@gmail.com>                    *
+ *                                                                         *
+ *   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 <http://www.gnu.org/licenses/>. *
+ ***************************************************************************/
+
+#include "AlpineApkTransaction.h"
+#include "AlpineApkBackend.h"
+#include "AlpineApkResource.h"
+#include "alpineapk_backend_logging.h"  // generated by ECM
+#include <QTimer>
+#include <QDebug>
+#include <KRandom>
+
+// #define TEST_PROCEED
+
+AlpineApkTransaction::AlpineApkTransaction(AlpineApkResource *app, Role role)
+    : AlpineApkTransaction(app, {}, role)
+{
+}
+
+AlpineApkTransaction::AlpineApkTransaction(AlpineApkResource *app, const AddonList &addons, Transaction::Role role)
+    : Transaction(app->backend(), app, role, addons)
+    , m_app(app)
+{
+    setCancellable(true);
+    setStatus(DownloadingStatus);
+    iterateTransaction();
+}
+
+void AlpineApkTransaction::iterateTransaction()
+{
+    if (!m_iterate) {
+        return;
+    }
+
+    if(progress() < 100) {
+        setProgress(qBound(0, progress() + (KRandom::random() % 30), 100));
+        QTimer::singleShot(/*KRandom::random()%*/100, this, &AlpineApkTransaction::iterateTransaction);
+    } else if (status() == DownloadingStatus) {
+        setStatus(CommittingStatus);
+        QTimer::singleShot(/*KRandom::random()%*/100, this, &AlpineApkTransaction::iterateTransaction);
+    } else {
+        finishTransaction();
+    }
+}
+
+void AlpineApkTransaction::proceed()
+{
+    finishTransaction();
+}
+
+void AlpineApkTransaction::cancel()
+{
+    m_iterate = false;
+
+    setStatus(CancelledStatus);
+}
+
+void AlpineApkTransaction::finishTransaction()
+{
+    AbstractResource::State newState;
+    switch(role()) {
+    case InstallRole:
+    case ChangeAddonsRole:
+        newState = AbstractResource::Installed;
+        break;
+    case RemoveRole:
+        newState = AbstractResource::None;
+        break;
+    }
+    m_app->setAddons(addons());
+    m_app->setState(newState);
+    setStatus(DoneStatus);
+    deleteLater();
+}
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkTransaction.h b/libdiscover/backends/AlpineApkBackend/AlpineApkTransaction.h
new file mode 100644
index 000000000..63aeef8d7
--- /dev/null
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkTransaction.h
@@ -0,0 +1,46 @@
+/***************************************************************************
+ *   Copyright © 2020 Alexey Min <alexey.min@gmail.com>                    *
+ *                                                                         *
+ *   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 <http://www.gnu.org/licenses/>. *
+ ***************************************************************************/
+
+#ifndef ALPINEAPKTRANSACTION_H
+#define ALPINEAPKTRANSACTION_H
+
+#include <Transaction/Transaction.h>
+
+class AlpineApkResource;
+class AlpineApkTransaction : public Transaction
+{
+    Q_OBJECT
+    public:
+        AlpineApkTransaction(AlpineApkResource *app, Role role);
+        AlpineApkTransaction(AlpineApkResource *app, const AddonList &list, Role role);
+
+        void cancel() override;
+        void proceed() override;
+
+    private Q_SLOTS:
+        void iterateTransaction();
+        void finishTransaction();
+
+    private:
+        bool m_iterate = true;
+        AlpineApkResource *m_app;
+};
+
+#endif // ALPINEAPKTRANSACTION_H
diff --git a/libdiscover/backends/AlpineApkBackend/CMakeLists.txt b/libdiscover/backends/AlpineApkBackend/CMakeLists.txt
new file mode 100644
index 000000000..45a56eb63
--- /dev/null
+++ b/libdiscover/backends/AlpineApkBackend/CMakeLists.txt
@@ -0,0 +1,52 @@
+# add_subdirectory(tests) # no tests yet
+
+set(alpineapkbackend_SRCS
+    AlpineApkBackend.cpp
+    AlpineApkBackend.h
+    AlpineApkResource.cpp
+    AlpineApkResource.h
+    AlpineApkSourcesBackend.cpp
+    AlpineApkSourcesBackend.h
+    AlpineApkTransaction.cpp
+    AlpineApkTransaction.h
+)
+
+ecm_qt_declare_logging_category(
+    alpineapkbackend_SRCS  # sources_var
+    HEADER alpineapk_backend_logging.h
+    IDENTIFIER LOG_ALPINEAPK
+    CATEGORY_NAME org.kde.plasma.discover.alpineapk
+    DEFAULT_SEVERITY Debug
+)
+
+add_library(
+    alpineapk-backend
+    MODULE
+        ${alpineapkbackend_SRCS}
+)
+
+target_link_libraries(
+    alpineapk-backend
+    PRIVATE
+        Qt5::Core
+        Qt5::Widgets
+        KF5::CoreAddons
+        KF5::ConfigCore
+        Discover::Common
+        apk-qt
+)
+
+install(
+    TARGETS alpineapk-backend
+    DESTINATION ${PLUGIN_INSTALL_DIR}/discover
+)
+
+# install(FILES alpineapk-backend-categories.xml DESTINATION ${DATA_INSTALL_DIR}/libdiscover/categories)
+
+# add_library(AlpineApkNotifier MODULE AlpineApkNotifier.cpp)
+
+# target_link_libraries(AlpineApkNotifier Discover::Notifiers)
+
+# set_target_properties(AlpineApkNotifier PROPERTIES INSTALL_RPATH ${CMAKE_INSTALL_FULL_LIBDIR}/plasma-discover)
+
+# install(TARGETS AlpineApkNotifier DESTINATION ${PLUGIN_INSTALL_DIR}/discover-notifier)
diff --git a/libdiscover/backends/CMakeLists.txt b/libdiscover/backends/CMakeLists.txt
index 7d878ad20..02ea6ed88 100644
--- a/libdiscover/backends/CMakeLists.txt
+++ b/libdiscover/backends/CMakeLists.txt
@@ -49,3 +49,16 @@ option(BUILD_RpmOstreeBackend "Build rpm-ostree support." "ON")
 if(BUILD_RpmOstreeBackend AND Ostree_FOUND AND RpmOstree_FOUND)
     add_subdirectory(RpmOstreeBackend)
 endif()
+
+# Optional library
+find_package(ApkQt CONFIG)
+set_package_properties(ApkQt PROPERTIES
+    DESCRIPTION "C++/Qt interface library for Alpine package keeper"
+    URL "https://gitlab.com/postmarketOS/libapk-qt"
+    PURPOSE "Required to build the Alpine APK backend"
+    TYPE OPTIONAL)
+
+option(BUILD_AlpineApkBackend "Build Alpine APK support." "ON")
+if(BUILD_AlpineApkBackend AND ApkQt_FOUND)
+    add_subdirectory(AlpineApkBackend)
+endif()
-- 
GitLab


From 661df03bfe24aa45918badbf00af87f5f47a9067 Mon Sep 17 00:00:00 2001
From: Alexey Min <alexey.min@gmail.com>
Date: Wed, 29 Jan 2020 17:35:17 +0300
Subject: [PATCH 02/59] CMake: Add AlpineApkReviewsBackend + its files

---
 .../AlpineApkReviewsBackend.cpp               | 35 +++++++++++++
 .../AlpineApkReviewsBackend.h                 | 52 +++++++++++++++++++
 .../backends/AlpineApkBackend/CMakeLists.txt  |  2 +
 3 files changed, 89 insertions(+)
 create mode 100644 libdiscover/backends/AlpineApkBackend/AlpineApkReviewsBackend.cpp
 create mode 100644 libdiscover/backends/AlpineApkBackend/AlpineApkReviewsBackend.h

diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkReviewsBackend.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkReviewsBackend.cpp
new file mode 100644
index 000000000..fd7ad47f2
--- /dev/null
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkReviewsBackend.cpp
@@ -0,0 +1,35 @@
+/***************************************************************************
+ *   Copyright © 2020 Alexey Min <alexey.min@gmail.com>                    *
+ *                                                                         *
+ *   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 <http://www.gnu.org/licenses/>. *
+ ***************************************************************************/
+
+#include "AlpineApkReviewsBackend.h"
+#include "AlpineApkBackend.h"
+#include "resources/AbstractResource.h"
+
+AlpineApkReviewsBackend::AlpineApkReviewsBackend(AlpineApkBackend *parent)
+    : AbstractReviewsBackend(parent)
+{
+}
+
+void AlpineApkReviewsBackend::fetchReviews(AbstractResource *app, int page)
+{
+    Q_UNUSED(page)
+    static const QVector<ReviewPtr> reviews;
+    Q_EMIT reviewsReady(app, reviews, false);
+}
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkReviewsBackend.h b/libdiscover/backends/AlpineApkBackend/AlpineApkReviewsBackend.h
new file mode 100644
index 000000000..435f845ba
--- /dev/null
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkReviewsBackend.h
@@ -0,0 +1,52 @@
+/***************************************************************************
+ *   Copyright © 2020 Alexey Min <alexey.min@gmail.com>                    *
+ *                                                                         *
+ *   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 <http://www.gnu.org/licenses/>. *
+ ***************************************************************************/
+
+#ifndef ALPINEAPKREVIEWSBACKEND_H
+#define ALPINEAPKREVIEWSBACKEND_H
+
+#include "ReviewsBackend/AbstractReviewsBackend.h"
+
+class AlpineApkBackend;
+
+class AlpineApkReviewsBackend : public AbstractReviewsBackend
+{
+    Q_OBJECT
+
+public:
+    explicit AlpineApkReviewsBackend(AlpineApkBackend *parent = nullptr);
+
+    QString userName() const override { return QStringLiteral("dummy"); }
+    void login() override {}
+    void logout() override {}
+    void registerAndLogin() override {}
+
+    Rating *ratingForApplication(AbstractResource *) const override { return nullptr; }
+    bool hasCredentials() const override { return false; }
+    void deleteReview(Review *) override {}
+    void fetchReviews(AbstractResource *app, int page = 1) override;
+    bool isFetching() const override { return false; }
+    bool isReviewable() const override { return false; }
+    void submitReview(AbstractResource *, const QString &, const QString &, const QString &) override {}
+    void flagReview(Review *, const QString&, const QString&) override {}
+    void submitUsefulness(Review *, bool) override {}
+    bool isResourceSupported(AbstractResource *) const override { return false; }
+};
+
+#endif // ALPINEAPKREVIEWSBACKEND_H
diff --git a/libdiscover/backends/AlpineApkBackend/CMakeLists.txt b/libdiscover/backends/AlpineApkBackend/CMakeLists.txt
index 45a56eb63..7d2b86e82 100644
--- a/libdiscover/backends/AlpineApkBackend/CMakeLists.txt
+++ b/libdiscover/backends/AlpineApkBackend/CMakeLists.txt
@@ -5,6 +5,8 @@ set(alpineapkbackend_SRCS
     AlpineApkBackend.h
     AlpineApkResource.cpp
     AlpineApkResource.h
+    AlpineApkReviewsBackend.cpp
+    AlpineApkReviewsBackend.h
     AlpineApkSourcesBackend.cpp
     AlpineApkSourcesBackend.h
     AlpineApkTransaction.cpp
-- 
GitLab


From 44ee619bca321ab349cb55173b3f52770cd34868 Mon Sep 17 00:00:00 2001
From: Alexey Min <alexey.min@gmail.com>
Date: Wed, 29 Jan 2020 17:35:43 +0300
Subject: [PATCH 03/59] AlpineApkBackend: use own reviews backend

---
 .../AlpineApkBackend/AlpineApkBackend.cpp         | 15 +++++----------
 .../backends/AlpineApkBackend/AlpineApkBackend.h  |  4 ++--
 2 files changed, 7 insertions(+), 12 deletions(-)

diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp
index f5aa5477d..de93609ac 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp
@@ -20,7 +20,7 @@
 
 #include "AlpineApkBackend.h"
 #include "AlpineApkResource.h"
-//#include "DummyReviewsBackend.h"
+#include "AlpineApkReviewsBackend.h"
 #include "AlpineApkTransaction.h"
 #include "AlpineApkSourcesBackend.h"
 #include "alpineapk_backend_logging.h"  // generated by ECM
@@ -46,6 +46,7 @@ DISCOVER_BACKEND_PLUGIN(AlpineApkBackend)
 AlpineApkBackend::AlpineApkBackend(QObject *parent)
     : AbstractResourcesBackend(parent)
     , m_updater(new StandardBackendUpdater(this))
+    , m_reviews(new AlpineApkReviewsBackend(this))
     , m_startElements(120)
 {
 #ifndef QT_DEBUG
@@ -55,12 +56,9 @@ AlpineApkBackend::AlpineApkBackend(QObject *parent)
     qCDebug(LOG_ALPINEAPK) << "constructing backend!";
 
     QTimer::singleShot(500, this, &AlpineApkBackend::toggleFetching);
-    //connect(m_reviews, &DummyReviewsBackend::ratingsReady, this, &AbstractResourcesBackend::emitRatingsReady);
     connect(m_updater, &StandardBackendUpdater::updatesCountChanged, this, &AlpineApkBackend::updatesCountChanged);
 
     populate();
-    //if (!m_fetching)
-    //    m_reviews->initialize();
 
     SourcesModel::global()->addSourcesBackend(new AlpineApkSourcesBackend(this));
 }
@@ -117,10 +115,9 @@ void AlpineApkBackend::populate()
 void AlpineApkBackend::toggleFetching()
 {
     m_fetching = !m_fetching;
+
     qCDebug(LOG_ALPINEAPK) << "fetching..." << m_fetching;
     emit fetchingChanged();
-    //if (!m_fetching)
-    //    m_reviews->initialize();
 }
 
 int AlpineApkBackend::updatesCount() const
@@ -195,9 +192,7 @@ AbstractBackendUpdater *AlpineApkBackend::backendUpdater() const
 
 AbstractReviewsBackend *AlpineApkBackend::reviewsBackend() const
 {
-    // qCDebug(LOG_ALPINEAPK) << "reviewsBbackend(): we don't support reviews (";
-    // return m_reviews;
-    return nullptr;
+    return m_reviews;
 }
 
 Transaction* AlpineApkBackend::installApplication(AbstractResource *app, const AddonList &addons)
@@ -221,7 +216,7 @@ void AlpineApkBackend::checkForUpdates()
         qCDebug(LOG_ALPINEAPK) << "checkForUpdates(): already fetching";
         return;
     }
-    qCDebug(LOG_ALPINEAPK) << "checkForUpdates()!";
+    qCDebug(LOG_ALPINEAPK) << "checkForUpdates() start!";
     toggleFetching();
     // populate(QStringLiteral("Moar"));
     QTimer::singleShot(1000, this, &AlpineApkBackend::toggleFetching);
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.h b/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.h
index 624df1c9f..f8d3aa69f 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.h
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.h
@@ -26,7 +26,7 @@
 
 #include <QtApk.h>
 
-// class DummyReviewsBackend;
+class AlpineApkReviewsBackend;
 class StandardBackendUpdater;
 class AlpineApkResource;
 class AlpineApkBackend : public AbstractResourcesBackend
@@ -62,7 +62,7 @@ private:
 
     QHash<QString, AlpineApkResource *> m_resources;
     StandardBackendUpdater *m_updater;
-    // DummyReviewsBackend* m_reviews;
+    AlpineApkReviewsBackend *m_reviews;
     QtApk::Database m_apkdb;
     QVector<QtApk::Package> m_availablePackages;
     QVector<QtApk::Package> m_installedPackages;
-- 
GitLab


From 4cca86c766f26717b0aa0f2c87810396e0f79a5d Mon Sep 17 00:00:00 2001
From: Alexey Min <alexey.min@gmail.com>
Date: Wed, 29 Jan 2020 19:53:39 +0300
Subject: [PATCH 04/59] AlpineApkBackend: some cleanup on updates checking
 functions

---
 .../AlpineApkBackend/AlpineApkBackend.cpp     | 41 ++++++++++++-------
 .../AlpineApkBackend/AlpineApkBackend.h       |  5 ++-
 2 files changed, 30 insertions(+), 16 deletions(-)

diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp
index de93609ac..41b2e0532 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp
@@ -55,8 +55,10 @@ AlpineApkBackend::AlpineApkBackend(QObject *parent)
 
     qCDebug(LOG_ALPINEAPK) << "constructing backend!";
 
-    QTimer::singleShot(500, this, &AlpineApkBackend::toggleFetching);
-    connect(m_updater, &StandardBackendUpdater::updatesCountChanged, this, &AlpineApkBackend::updatesCountChanged);
+    QTimer::singleShot(1000, this, &AlpineApkBackend::startCheckForUpdates);
+
+    QObject::connect(m_updater, &StandardBackendUpdater::updatesCountChanged,
+                     this, &AlpineApkBackend::updatesCountChanged);
 
     populate();
 
@@ -65,12 +67,14 @@ AlpineApkBackend::AlpineApkBackend(QObject *parent)
 
 QVector<Category *> AlpineApkBackend::category() const
 {
+    // single root category
+    // we could add more, but Alpine apk does not have this concept
     static Category *cat = new Category(
-                QStringLiteral("All applications"),    // displayName
-                QStringLiteral("applications-other"),  // icon
-                { }, // orFilters
+                QStringLiteral("All packages"),    // displayName
+                QStringLiteral("package-x-generic"), // icon
+                {}, // orFilters
                 { displayName() }, // pluginName
-                { }, // subCategories
+                {}, // subCategories
                 QUrl(), // decoration (what is it?)
                 false  // isAddons
     );
@@ -112,11 +116,23 @@ void AlpineApkBackend::populate()
     }
 }
 
-void AlpineApkBackend::toggleFetching()
+void AlpineApkBackend::startCheckForUpdates()
 {
-    m_fetching = !m_fetching;
+    if (m_fetching) {
+        return;
+    }
+    qCDebug(LOG_ALPINEAPK) << "startCheckForUpdates()";
+
+    m_fetching = true;
+    emit fetchingChanged();
+
+    // temporary hack - finish updates check in 5 seconds
+    QTimer::singleShot(5000, this, &AlpineApkBackend::finishCheckForUpdates);
+}
 
-    qCDebug(LOG_ALPINEAPK) << "fetching..." << m_fetching;
+void AlpineApkBackend::finishCheckForUpdates()
+{
+    m_fetching = false;
     emit fetchingChanged();
 }
 
@@ -212,14 +228,11 @@ Transaction* AlpineApkBackend::removeApplication(AbstractResource *app)
 
 void AlpineApkBackend::checkForUpdates()
 {
-    if(m_fetching) {
+    if (m_fetching) {
         qCDebug(LOG_ALPINEAPK) << "checkForUpdates(): already fetching";
         return;
     }
-    qCDebug(LOG_ALPINEAPK) << "checkForUpdates() start!";
-    toggleFetching();
-    // populate(QStringLiteral("Moar"));
-    QTimer::singleShot(1000, this, &AlpineApkBackend::toggleFetching);
+    startCheckForUpdates();
 }
 
 QString AlpineApkBackend::displayName() const
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.h b/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.h
index f8d3aa69f..68a47bc92 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.h
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.h
@@ -55,7 +55,8 @@ public:
     bool hasApplications() const override;
 
 public Q_SLOTS:
-    void toggleFetching();
+    void startCheckForUpdates();
+    void finishCheckForUpdates();
 
 private:
     void populate();
@@ -66,7 +67,7 @@ private:
     QtApk::Database m_apkdb;
     QVector<QtApk::Package> m_availablePackages;
     QVector<QtApk::Package> m_installedPackages;
-    bool m_fetching = true;
+    bool m_fetching = false;
     int m_startElements = 0;
 };
 
-- 
GitLab


From e5a16ed0fc454d019e1017c050fd894a164b6e5d Mon Sep 17 00:00:00 2001
From: Alexey Min <alexey.min@gmail.com>
Date: Wed, 29 Jan 2020 19:54:04 +0300
Subject: [PATCH 05/59] Add KAuth helper process for privileged operations

---
 .../AlpineApkBackend/AlpineApkAuthHelper.cpp  | 56 +++++++++++++++++++
 .../AlpineApkBackend/AlpineApkAuthHelper.h    | 35 ++++++++++++
 .../backends/AlpineApkBackend/CMakeLists.txt  | 26 ++++++++-
 .../org.kde.discover.alpineapkbackend.actions |  5 ++
 4 files changed, 120 insertions(+), 2 deletions(-)
 create mode 100644 libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp
 create mode 100644 libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.h
 create mode 100644 libdiscover/backends/AlpineApkBackend/org.kde.discover.alpineapkbackend.actions

diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp
new file mode 100644
index 000000000..6783f35cc
--- /dev/null
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp
@@ -0,0 +1,56 @@
+/***************************************************************************
+ *   Copyright © 2020 Alexey Min <alexey.min@gmail.com>                    *
+ *                                                                         *
+ *   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 <http://www.gnu.org/licenses/>. *
+ ***************************************************************************/
+
+#include <QProcess>
+#include <QDebug>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonArray>
+#include <QFile>
+#include <KAuthHelperSupport>
+
+#include "AlpineApkAuthHelper.h"
+
+using namespace KAuth;
+
+AlpineApkAuthHelper::AlpineApkAuthHelper() {}
+
+ActionReply AlpineApkAuthHelper::test_action(const QVariantMap &args)
+{
+    const QString txt = args[QStringLiteral("txt")].toString();
+
+    ActionReply reply = ActionReply::HelperErrorReply();
+    QByteArray replyData(QByteArrayLiteral("ok"));
+
+    QFile f(QStringLiteral("/lol.txt"));
+    if (f.open(QIODevice::ReadWrite | QIODevice::Text)) {
+        f.write(txt.toUtf8());
+        f.close();
+
+        reply = ActionReply::SuccessReply();
+        reply.setData({
+            { QStringLiteral("reply"), replyData },
+        });
+    }
+
+    return reply;
+}
+
+KAUTH_HELPER_MAIN("org.kde.discover.alpineapkbackend", AlpineApkAuthHelper)
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.h b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.h
new file mode 100644
index 000000000..86f7d3dcf
--- /dev/null
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.h
@@ -0,0 +1,35 @@
+/***************************************************************************
+ *   Copyright © 2020 Alexey Min <alexey.min@gmail.com>                    *
+ *                                                                         *
+ *   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 <http://www.gnu.org/licenses/>. *
+ ***************************************************************************/
+
+#include <QObject>
+#include <QVariant>
+#include <KAuthActionReply>
+
+using namespace KAuth;
+
+class AlpineApkAuthHelper : public QObject
+{
+    Q_OBJECT
+public:
+    AlpineApkAuthHelper();
+
+public Q_SLOTS:
+    ActionReply test_action(const QVariantMap &args);
+};
diff --git a/libdiscover/backends/AlpineApkBackend/CMakeLists.txt b/libdiscover/backends/AlpineApkBackend/CMakeLists.txt
index 7d2b86e82..f67cf84f9 100644
--- a/libdiscover/backends/AlpineApkBackend/CMakeLists.txt
+++ b/libdiscover/backends/AlpineApkBackend/CMakeLists.txt
@@ -1,4 +1,4 @@
-# add_subdirectory(tests) # no tests yet
+find_package(KF5Auth CONFIG REQUIRED)  # Probably should be moved to top CMakeLists
 
 set(alpineapkbackend_SRCS
     AlpineApkBackend.cpp
@@ -38,12 +38,34 @@ target_link_libraries(
         apk-qt
 )
 
+# KAuth helper exe
+add_executable(alpineapk_kauth_helper
+    AlpineApkAuthHelper.cpp
+    AlpineApkAuthHelper.h
+    org.kde.discover.alpineapkbackend.actions
+)
+set_source_files_properties(
+    org.kde.discover.alpineapkbackend.actions
+    PROPERTIES HEADER_FILE_ONLY ON
+)
+target_link_libraries(alpineapk_kauth_helper
+    Qt5::Core
+    KF5::AuthCore
+    apk-qt
+)
+
+kauth_install_actions(org.kde.discover.alpineapkbackend org.kde.discover.alpineapkbackend.actions)
+kauth_install_helper_files(alpineapk_kauth_helper org.kde.discover.alpineapkbackend root)
+
 install(
     TARGETS alpineapk-backend
     DESTINATION ${PLUGIN_INSTALL_DIR}/discover
 )
 
-# install(FILES alpineapk-backend-categories.xml DESTINATION ${DATA_INSTALL_DIR}/libdiscover/categories)
+install(
+    TARGETS alpineapk_kauth_helper
+    DESTINATION ${KAUTH_HELPER_INSTALL_DIR}
+)
 
 # add_library(AlpineApkNotifier MODULE AlpineApkNotifier.cpp)
 
diff --git a/libdiscover/backends/AlpineApkBackend/org.kde.discover.alpineapkbackend.actions b/libdiscover/backends/AlpineApkBackend/org.kde.discover.alpineapkbackend.actions
new file mode 100644
index 000000000..ba9ede912
--- /dev/null
+++ b/libdiscover/backends/AlpineApkBackend/org.kde.discover.alpineapkbackend.actions
@@ -0,0 +1,5 @@
+[org.kde.discover.alpineapkbackend.test_action]
+Name=Test Action
+Description=Just test
+Policy=auth_admin
+Persistence=session
-- 
GitLab


From 1f7ff628bdf999bec9b15f7964a076dcf3f56cfa Mon Sep 17 00:00:00 2001
From: Alexey Min <alexey.min@gmail.com>
Date: Wed, 29 Jan 2020 20:41:07 +0300
Subject: [PATCH 06/59] AlpineApkBackend: WIP on using KAuth helper

---
 .../AlpineApkBackend/AlpineApkAuthHelper.cpp  |  2 +-
 .../AlpineApkBackend/AlpineApkAuthHelper.h    |  2 +-
 .../AlpineApkBackend/AlpineApkBackend.cpp     | 34 +++++++++++++++++--
 .../AlpineApkBackend/AlpineApkBackend.h       |  3 ++
 .../backends/AlpineApkBackend/CMakeLists.txt  |  1 +
 .../org.kde.discover.alpineapkbackend.actions |  2 +-
 6 files changed, 39 insertions(+), 5 deletions(-)

diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp
index 6783f35cc..19236dcb3 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp
@@ -32,7 +32,7 @@ using namespace KAuth;
 
 AlpineApkAuthHelper::AlpineApkAuthHelper() {}
 
-ActionReply AlpineApkAuthHelper::test_action(const QVariantMap &args)
+ActionReply AlpineApkAuthHelper::test(const QVariantMap &args)
 {
     const QString txt = args[QStringLiteral("txt")].toString();
 
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.h b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.h
index 86f7d3dcf..947dcb7ac 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.h
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.h
@@ -31,5 +31,5 @@ public:
     AlpineApkAuthHelper();
 
 public Q_SLOTS:
-    ActionReply test_action(const QVariantMap &args);
+    ActionReply test(const QVariantMap &args);
 };
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp
index 41b2e0532..ed016aef3 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp
@@ -35,6 +35,8 @@
 #include <KPluginFactory>
 #include <KConfigGroup>
 #include <KSharedConfig>
+#include <KAuthExecuteJob>
+
 #include <QDebug>
 #include <QLoggingCategory>
 #include <QThread>
@@ -116,6 +118,19 @@ void AlpineApkBackend::populate()
     }
 }
 
+void AlpineApkBackend::handleKauthHelperReply(KJob *job)
+{
+    qCDebug(LOG_ALPINEAPK) << Q_FUNC_INFO;
+    KAuth::ExecuteJob* reply = static_cast<KAuth::ExecuteJob *>(job);
+    const QVariantMap replyData = reply->data();
+    if (reply->error() == 0) {
+        qCDebug(LOG_ALPINEAPK) << replyData[QLatin1String("reply")].toString();
+    } else {
+        const QString message = replyData.value(QLatin1String("errorString"), reply->errorString()).toString();
+        qCDebug(LOG_ALPINEAPK) << message;
+    }
+}
+
 void AlpineApkBackend::startCheckForUpdates()
 {
     if (m_fetching) {
@@ -123,11 +138,26 @@ void AlpineApkBackend::startCheckForUpdates()
     }
     qCDebug(LOG_ALPINEAPK) << "startCheckForUpdates()";
 
+    // temporary hack - finish updates check in 5 seconds
+    QTimer::singleShot(5000, this, &AlpineApkBackend::finishCheckForUpdates);
+
     m_fetching = true;
     emit fetchingChanged();
 
-    // temporary hack - finish updates check in 5 seconds
-    QTimer::singleShot(5000, this, &AlpineApkBackend::finishCheckForUpdates);
+    KAuth::Action testAction(QStringLiteral("org.kde.discover.alpineapkbackend.test"));
+    testAction.setHelperId(QStringLiteral("org.kde.discover.alpineapkbackend"));
+    testAction.setArguments({
+        { QStringLiteral("txt"), QLatin1String("Wooo!") },
+    });
+    if (!testAction.isValid()) {
+        qCWarning(LOG_ALPINEAPK) << "kauth action is not valid!";
+        return;
+    }
+
+    KAuth::ExecuteJob *reply = testAction.execute();
+    QObject::connect(reply, &KAuth::ExecuteJob::result,
+                     this, &AlpineApkBackend::handleKauthHelperReply);
+    reply->start();
 }
 
 void AlpineApkBackend::finishCheckForUpdates()
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.h b/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.h
index 68a47bc92..a533cee39 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.h
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.h
@@ -29,6 +29,8 @@
 class AlpineApkReviewsBackend;
 class StandardBackendUpdater;
 class AlpineApkResource;
+class KJob;
+
 class AlpineApkBackend : public AbstractResourcesBackend
 {
     Q_OBJECT
@@ -55,6 +57,7 @@ public:
     bool hasApplications() const override;
 
 public Q_SLOTS:
+    void handleKauthHelperReply(KJob *job);
     void startCheckForUpdates();
     void finishCheckForUpdates();
 
diff --git a/libdiscover/backends/AlpineApkBackend/CMakeLists.txt b/libdiscover/backends/AlpineApkBackend/CMakeLists.txt
index f67cf84f9..ede3157f0 100644
--- a/libdiscover/backends/AlpineApkBackend/CMakeLists.txt
+++ b/libdiscover/backends/AlpineApkBackend/CMakeLists.txt
@@ -34,6 +34,7 @@ target_link_libraries(
         Qt5::Widgets
         KF5::CoreAddons
         KF5::ConfigCore
+        KF5::AuthCore
         Discover::Common
         apk-qt
 )
diff --git a/libdiscover/backends/AlpineApkBackend/org.kde.discover.alpineapkbackend.actions b/libdiscover/backends/AlpineApkBackend/org.kde.discover.alpineapkbackend.actions
index ba9ede912..3b9a3116e 100644
--- a/libdiscover/backends/AlpineApkBackend/org.kde.discover.alpineapkbackend.actions
+++ b/libdiscover/backends/AlpineApkBackend/org.kde.discover.alpineapkbackend.actions
@@ -1,4 +1,4 @@
-[org.kde.discover.alpineapkbackend.test_action]
+[org.kde.discover.alpineapkbackend.test]
 Name=Test Action
 Description=Just test
 Policy=auth_admin
-- 
GitLab


From 3797cb51563a114e1e53735eb6b6b838502f2913 Mon Sep 17 00:00:00 2001
From: Alexey Min <alexey.min@gmail.com>
Date: Mon, 3 Feb 2020 03:49:52 +0300
Subject: [PATCH 07/59] kath helper: add and implement update() and upgrade()
 actions

---
 .../AlpineApkBackend/AlpineApkAuthHelper.cpp  | 97 +++++++++++++++++++
 .../AlpineApkBackend/AlpineApkAuthHelper.h    |  9 ++
 .../org.kde.discover.alpineapkbackend.actions | 24 +++++
 3 files changed, 130 insertions(+)

diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp
index 19236dcb3..4ff5c880a 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp
@@ -20,14 +20,22 @@
 
 #include <QProcess>
 #include <QDebug>
+#include <QLoggingCategory>
 #include <QJsonDocument>
 #include <QJsonObject>
 #include <QJsonArray>
 #include <QFile>
+
 #include <KAuthHelperSupport>
 
 #include "AlpineApkAuthHelper.h"
 
+#ifdef QT_DEBUG
+Q_LOGGING_CATEGORY(LOG_AUTHHELPER, "org.kde.discover.alpineapkbackend.authhelper", QtDebugMsg)
+#else
+Q_LOGGING_CATEGORY(LOG_AUTHHELPER, "org.kde.discover.alpineapkbackend.authhelper", QtWarningMsg)
+#endif
+
 using namespace KAuth;
 
 AlpineApkAuthHelper::AlpineApkAuthHelper() {}
@@ -39,6 +47,7 @@ ActionReply AlpineApkAuthHelper::test(const QVariantMap &args)
     ActionReply reply = ActionReply::HelperErrorReply();
     QByteArray replyData(QByteArrayLiteral("ok"));
 
+    // write some text file at the root directory as root, why not
     QFile f(QStringLiteral("/lol.txt"));
     if (f.open(QIODevice::ReadWrite | QIODevice::Text)) {
         f.write(txt.toUtf8());
@@ -53,4 +62,92 @@ ActionReply AlpineApkAuthHelper::test(const QVariantMap &args)
     return reply;
 }
 
+ActionReply AlpineApkAuthHelper::update(const QVariantMap &args)
+{
+    Q_UNUSED(args)
+    ActionReply reply = ActionReply::HelperErrorReply();
+
+    HelperSupport::progressStep(10);
+
+    if (!m_apkdb.open(QtApk::Database::QTAPK_OPENF_READWRITE)) {
+        reply.setErrorDescription(QStringLiteral("Failed to open database!"));
+        return reply;
+    }
+
+    bool update_ok = m_apkdb.updatePackageIndex();
+
+    if (update_ok) {
+        int updatesCount = m_apkdb.upgradeablePackagesCount();
+        reply = ActionReply::SuccessReply();
+        reply.setData({
+            { QLatin1String("updatesCount"), updatesCount }
+        });
+    } else {
+        reply.setErrorDescription(QStringLiteral("Repo update failed!"));
+        reply.setData({
+            { QLatin1String("errorString"), QStringLiteral("Repo update failed!") }
+        });
+    }
+
+    m_apkdb.close();
+    HelperSupport::progressStep(100);
+
+    return reply;
+}
+
+ActionReply AlpineApkAuthHelper::add(const QVariantMap &args)
+{
+    ActionReply reply = ActionReply::HelperErrorReply();
+    return reply;
+}
+
+ActionReply AlpineApkAuthHelper::del(const QVariantMap &args)
+{
+    ActionReply reply = ActionReply::HelperErrorReply();
+    return reply;
+}
+
+ActionReply AlpineApkAuthHelper::upgrade(const QVariantMap &args)
+{
+    ActionReply reply = ActionReply::HelperErrorReply();
+
+    HelperSupport::progressStep(10);
+
+    if (!m_apkdb.open(QtApk::Database::QTAPK_OPENF_READWRITE)) {
+        reply.setErrorDescription(QStringLiteral("Failed to open database!"));
+        return reply;
+    }
+
+    bool onlySimulate = args.value(QLatin1String("onlySimulate"), false).toBool();
+    QtApk::Database::DbUpgradeFlags flags = QtApk::Database::QTAPK_UPGRADE_DEFAULT;
+    if (onlySimulate) {
+        flags = QtApk::Database::QTAPK_UPGRADE_SIMULATE;
+        qCDebug(LOG_AUTHHELPER) << "Simulating upgrade run.";
+    }
+
+    QtApk::Changeset changes;
+    bool upgrade_ok = m_apkdb.upgrade(flags, &changes);
+
+    if (upgrade_ok) {
+        reply = ActionReply::SuccessReply();
+        QVariantMap replyData;
+        const QVector<QtApk::ChangesetItem> ch = changes.changes();
+        QVector<QVariant> chVector;
+        QVector<QtApk::Package> pkgVector;
+        for (const QtApk::ChangesetItem &it: ch) {
+            pkgVector << it.newPackage;
+        }
+        replyData.insert(QLatin1String("changes"), QVariant::fromValue(pkgVector));
+        replyData.insert(QLatin1String("onlySimulate"), onlySimulate);
+        reply.setData(replyData);
+    } else {
+        reply.setErrorDescription(QStringLiteral("Repo upgrade failed!"));
+    }
+
+    m_apkdb.close();
+    HelperSupport::progressStep(100);
+
+    return reply;
+}
+
 KAUTH_HELPER_MAIN("org.kde.discover.alpineapkbackend", AlpineApkAuthHelper)
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.h b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.h
index 947dcb7ac..5c06e5570 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.h
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.h
@@ -22,6 +22,8 @@
 #include <QVariant>
 #include <KAuthActionReply>
 
+#include <QtApk.h>
+
 using namespace KAuth;
 
 class AlpineApkAuthHelper : public QObject
@@ -32,4 +34,11 @@ public:
 
 public Q_SLOTS:
     ActionReply test(const QVariantMap &args);
+    ActionReply update(const QVariantMap &args);
+    ActionReply add(const QVariantMap &args);
+    ActionReply del(const QVariantMap &args);
+    ActionReply upgrade(const QVariantMap &args);
+
+private:
+    QtApk::Database m_apkdb;
 };
diff --git a/libdiscover/backends/AlpineApkBackend/org.kde.discover.alpineapkbackend.actions b/libdiscover/backends/AlpineApkBackend/org.kde.discover.alpineapkbackend.actions
index 3b9a3116e..10305a1af 100644
--- a/libdiscover/backends/AlpineApkBackend/org.kde.discover.alpineapkbackend.actions
+++ b/libdiscover/backends/AlpineApkBackend/org.kde.discover.alpineapkbackend.actions
@@ -3,3 +3,27 @@ Name=Test Action
 Description=Just test
 Policy=auth_admin
 Persistence=session
+
+[org.kde.discover.alpineapkbackend.update]
+Name=Update repository index
+Description=Updates available packages list from repositories
+Policy=auth_admin
+Persistence=session
+
+[org.kde.discover.alpineapkbackend.upgrade]
+Name=Upgrade all upgradable packages
+Description=Upgrade installed packages to latest versions
+Policy=auth_admin
+Persistence=session
+
+[org.kde.discover.alpineapkbackend.add]
+Name=Install/upgrade package
+Description=Installs/upgrades one package
+Policy=auth_admin
+Persistence=session
+
+[org.kde.discover.alpineapkbackend.del]
+Name=Remove package
+Description=Uninstall one package
+Policy=auth_admin
+Persistence=session
-- 
GitLab


From a06d4ceae46a69c3101b16d18c383558b49ed333 Mon Sep 17 00:00:00 2001
From: Alexey Min <alexey.min@gmail.com>
Date: Mon, 3 Feb 2020 03:51:02 +0300
Subject: [PATCH 08/59] AlpineApkResource: add availableVersion field

for updates
---
 .../backends/AlpineApkBackend/AlpineApkResource.cpp       | 8 ++++++--
 libdiscover/backends/AlpineApkBackend/AlpineApkResource.h | 2 ++
 2 files changed, 8 insertions(+), 2 deletions(-)

diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkResource.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkResource.cpp
index d827e7b3b..af480b6c6 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkResource.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkResource.cpp
@@ -18,7 +18,6 @@
  *   along with this program.  If not, see <http://www.gnu.org/licenses/>. *
  ***************************************************************************/
 
-#include <KRandom>
 #include "AlpineApkResource.h"
 #include "alpineapk_backend_logging.h"  // generated by ECM
 #include "Transaction/AddonList.h"
@@ -39,7 +38,7 @@ QList<PackageState> AlpineApkResource::addonsInformation()
 
 QString AlpineApkResource::availableVersion() const
 {
-    return m_pkg.version;
+    return m_availableVersion;
 }
 
 QStringList AlpineApkResource::categories()
@@ -180,6 +179,11 @@ void AlpineApkResource::setAddonInstalled(const QString &addon, bool installed)
     }
 }
 
+void AlpineApkResource::setAvailableVersion(const QString &av)
+{
+    m_availableVersion = av;
+}
+
 
 void AlpineApkResource::invokeApplication() const
 {
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkResource.h b/libdiscover/backends/AlpineApkBackend/AlpineApkResource.h
index 79b100e10..7140786c5 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkResource.h
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkResource.h
@@ -67,11 +67,13 @@ public:
     void setSection(const QString &sectionName);
     void setAddons(const AddonList &addons);
     void setAddonInstalled(const QString &addon, bool installed);
+    void setAvailableVersion(const QString &av);
 
 public:
     AbstractResource::State m_state;
     const AbstractResource::Type m_type;
     QtApk::Package m_pkg;
+    QString m_availableVersion;
     QString m_category;
     QString m_originSoruce;
     QString m_sectionName;
-- 
GitLab


From c081273de3d5eb12c069012a127f7d6ddc4de054 Mon Sep 17 00:00:00 2001
From: Alexey Min <alexey.min@gmail.com>
Date: Mon, 3 Feb 2020 03:51:58 +0300
Subject: [PATCH 09/59] Add AlpineApkUpdater class

---
 .../AlpineApkBackend/AlpineApkUpdater.cpp     | 286 ++++++++++++++++++
 .../AlpineApkBackend/AlpineApkUpdater.h       | 190 ++++++++++++
 .../backends/AlpineApkBackend/CMakeLists.txt  |   2 +
 3 files changed, 478 insertions(+)
 create mode 100644 libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp
 create mode 100644 libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.h

diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp
new file mode 100644
index 000000000..1dd6cdacd
--- /dev/null
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp
@@ -0,0 +1,286 @@
+/***************************************************************************
+ *   Copyright © 2020 Alexey Min <alexey.min@gmail.com>                    *
+ *                                                                         *
+ *   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 <http://www.gnu.org/licenses/>. *
+ ***************************************************************************/
+
+#include "AlpineApkUpdater.h"
+#include "AlpineApkResource.h"
+#include "AlpineApkBackend.h"
+#include "alpineapk_backend_logging.h"
+#include "utils.h"
+
+#include <KAuthExecuteJob>
+#include <KLocalizedString>
+
+#include <QtApk.h>
+
+
+AlpineApkUpdater::AlpineApkUpdater(AbstractResourcesBackend *parent)
+    : AbstractBackendUpdater(parent)
+    , m_backend(static_cast<AlpineApkBackend *>(parent))
+{
+    //
+}
+
+void AlpineApkUpdater::prepare()
+{
+    qCDebug(LOG_ALPINEAPK) << Q_FUNC_INFO;
+
+    QtApk::Database *db = m_backend->apkdb();
+
+    if (db->isOpen()) {
+        return;
+    }
+
+    if (!db->open(QtApk::Database::QTAPK_OPENF_READONLY)) {
+        emit passiveMessage(i18n("Failed to open APK database!"));
+        return;
+    }
+
+    if (!db->upgrade(QtApk::Database::QTAPK_UPGRADE_SIMULATE, &m_upgradeable)) {
+        emit passiveMessage(i18n("Failed to get a list of packages to upgrade!"));
+        db->close();
+        return;
+    }
+    // clsoe DB ASAP
+    db->close();
+
+    m_updatesCount = m_upgradeable.changes().size();
+    qCDebug(LOG_ALPINEAPK) << "updater: prepare: updates count" << m_updatesCount;
+
+    m_allUpdateable.clear();
+    m_markedToUpdate.clear();
+    QHash<QString, AlpineApkResource *> *resources = m_backend->resourcesPtr();
+    for (const QtApk::ChangesetItem &it : qAsConst(m_upgradeable.changes())) {
+        const QtApk::Package &oldPkg = it.oldPackage;
+        const QString newVersion = it.newPackage.version;
+        AlpineApkResource *res = resources->value(oldPkg.name);
+        if (res) {
+            res->setAvailableVersion(newVersion);
+            m_allUpdateable.insert(res);
+            m_markedToUpdate.insert(res);
+        }
+    }
+
+    // emitting this signal here leads to infinite recursion
+    // emit updatesCountChanged(m_updatesCount);
+}
+
+bool AlpineApkUpdater::hasUpdates() const
+{
+    // qCDebug(LOG_ALPINEAPK) << Q_FUNC_INFO << m_updatesCount;
+    return (m_updatesCount > 0);
+}
+
+qreal AlpineApkUpdater::progress() const
+{
+    qCDebug(LOG_ALPINEAPK) << Q_FUNC_INFO;
+    return 0.0;
+}
+
+void AlpineApkUpdater::removeResources(const QList<AbstractResource *> &apps)
+{
+    // qCDebug(LOG_ALPINEAPK) << Q_FUNC_INFO;
+    const QSet<AbstractResource *> checkSet = kToSet(apps);
+    m_markedToUpdate -= checkSet;
+}
+
+void AlpineApkUpdater::addResources(const QList<AbstractResource *> &apps)
+{
+    //Q_UNUSED(apps)
+    //qCDebug(LOG_ALPINEAPK) << Q_FUNC_INFO;
+    const QSet<AbstractResource *> checkSet = kToSet(apps);
+    m_markedToUpdate += checkSet;
+}
+
+QList<AbstractResource *> AlpineApkUpdater::toUpdate() const
+{
+    // qCDebug(LOG_ALPINEAPK) << Q_FUNC_INFO;
+    return m_allUpdateable.values();
+}
+
+QDateTime AlpineApkUpdater::lastUpdate() const
+{
+    qCDebug(LOG_ALPINEAPK) << Q_FUNC_INFO;
+    return QDateTime();
+}
+
+bool AlpineApkUpdater::isCancelable() const
+{
+    qCDebug(LOG_ALPINEAPK) << Q_FUNC_INFO;
+    return true;
+}
+
+bool AlpineApkUpdater::isProgressing() const
+{
+    qCDebug(LOG_ALPINEAPK) << Q_FUNC_INFO;
+    return false;
+}
+
+bool AlpineApkUpdater::isMarked(AbstractResource *res) const
+{
+    // qCDebug(LOG_ALPINEAPK) << Q_FUNC_INFO;
+    return m_markedToUpdate.contains(res);
+    // return true;
+}
+
+void AlpineApkUpdater::fetchChangelog() const
+{
+    qCDebug(LOG_ALPINEAPK) << Q_FUNC_INFO;
+}
+
+double AlpineApkUpdater::updateSize() const
+{
+    // qCDebug(LOG_ALPINEAPK) << Q_FUNC_INFO;
+    double sum = 0.0;
+    for (AbstractResource *res : m_markedToUpdate) {
+        sum += res->size();
+    }
+    return sum;
+}
+
+quint64 AlpineApkUpdater::downloadSpeed() const
+{
+    qCDebug(LOG_ALPINEAPK) << Q_FUNC_INFO;
+    return 0;
+}
+
+void AlpineApkUpdater::cancel()
+{
+    qCDebug(LOG_ALPINEAPK) << Q_FUNC_INFO;
+}
+
+void AlpineApkUpdater::start()
+{
+    qCDebug(LOG_ALPINEAPK) << Q_FUNC_INFO;
+    return;
+#if 0
+    KAuth::Action upgradeAction(QStringLiteral("org.kde.discover.alpineapkbackend.upgrade"));
+    upgradeAction.setHelperId(QStringLiteral("org.kde.discover.alpineapkbackend"));
+    if (!upgradeAction.isValid()) {
+        qCWarning(LOG_ALPINEAPK) << "kauth upgradeAction is not valid!";
+        return;
+    }
+    upgradeAction.setTimeout(60 * 1000); // 1 minute
+    upgradeAction.setDetails(i18n("Get the list of packages to upgrade"));
+    upgradeAction.addArgument(QLatin1String("onlySimulate"), true);
+
+    // run upgrade check with elevated privileges
+    KAuth::ExecuteJob *reply = upgradeAction.execute();
+    QObject::connect(reply, &KAuth::ExecuteJob::result,
+                     this, &AlpineApkUpdater::handleKAuthUpgradeHelperReply);
+
+    reply->start();
+#endif
+}
+
+void AlpineApkUpdater::proceed()
+{
+    qCDebug(LOG_ALPINEAPK) << Q_FUNC_INFO;
+}
+
+int AlpineApkUpdater::updatesCount()
+{
+    // qDebug(LOG_ALPINEAPK) << Q_FUNC_INFO << m_updatesCount;
+    return m_updatesCount;
+}
+
+void AlpineApkUpdater::startCheckForUpdates()
+{
+    KAuth::Action updateAction(QStringLiteral("org.kde.discover.alpineapkbackend.update"));
+    updateAction.setHelperId(QStringLiteral("org.kde.discover.alpineapkbackend"));
+    if (!updateAction.isValid()) {
+        qCWarning(LOG_ALPINEAPK) << "kauth updateAction is not valid!";
+        return;
+    }
+    updateAction.setTimeout(60 * 1000); // 1 minute
+    updateAction.setDetails(i18n("Update repositories index"));
+
+    // run updates check with elevated privileges to access
+    // system package manager files
+    KAuth::ExecuteJob *reply = updateAction.execute();
+    QObject::connect(reply, &KAuth::ExecuteJob::result,
+                     this, &AlpineApkUpdater::handleKAuthUpdateHelperReply);
+    //QObject::connect(reply, &KAuth::ExecuteJob::newData,
+    //                 this, &AlpineApkUpdater::handleKAuthUpdateHelperProgressStep);
+    QObject::connect(reply, QOverload<KJob *, unsigned long>::of(&KAuth::ExecuteJob::percent),
+                     this, &AlpineApkUpdater::handleKAuthUpdateHelperProgress);
+
+    reply->start();
+}
+
+void AlpineApkUpdater::handleKAuthUpdateHelperReply(KJob *job)
+{
+    KAuth::ExecuteJob *reply = static_cast<KAuth::ExecuteJob *>(job);
+    const QVariantMap &replyData = reply->data();
+    if (reply->error() == 0) {
+        m_updatesCount = replyData.value(QLatin1String("updatesCount")).toInt();
+        qCDebug(LOG_ALPINEAPK) << "KAuth helper update reply received, updatesCount:" << m_updatesCount;
+        Q_EMIT updatesCountChanged(m_updatesCount);
+    } else {
+        const QString message = replyData.value(QLatin1String("errorString"),
+                                                reply->errorString()).toString();
+        qCDebug(LOG_ALPINEAPK) << "KAuth helper returned error:" << message << reply->error();
+        if (reply->error() == KAuth::ActionReply::Error::AuthorizationDeniedError) {
+            Q_EMIT passiveMessage(i18n("Authorization denied"));
+        } else {
+            Q_EMIT passiveMessage(i18n("Error") + QStringLiteral(":\n") + message);
+        }
+    }
+
+    // we are not in the state "Fetching updates" now, update UI
+    Q_EMIT checkForUpdatesFinished();
+}
+
+void AlpineApkUpdater::handleKAuthUpdateHelperProgress(KJob *job, unsigned long percent)
+{
+    Q_UNUSED(job)
+    qCDebug(LOG_ALPINEAPK) << "    fetch updates progress: " << percent;
+    Q_EMIT fetchingUpdatesProgressChanged(percent);
+}
+
+void AlpineApkUpdater::handleKAuthUpgradeHelperReply(KJob *job)
+{
+    KAuth::ExecuteJob *reply = static_cast<KAuth::ExecuteJob *>(job);
+    const QVariantMap &replyData = reply->data();
+    if (reply->error() == 0) {
+        QVariant pkgsV = replyData.value(QLatin1String("changes"));
+        bool onlySimulate = replyData.value(QLatin1String("onlySimulate"), false).toBool();
+        qCDebug(LOG_ALPINEAPK) << "KAuth helper upgrade reply received:" << onlySimulate;
+        if (onlySimulate) {
+            QVector<QtApk::Package> pkgVector = pkgsV.value<QVector<QtApk::Package>>();
+            qCDebug(LOG_ALPINEAPK) << "  num changes:" << pkgVector.size();
+            for (const QtApk::Package &pkg : pkgVector) {
+                qCDebug(LOG_ALPINEAPK)  << "    " << pkg.name << pkg.version;
+            }
+        }
+    } else {
+        const QString message = replyData.value(QLatin1String("errorString"),
+                                                reply->errorString()).toString();
+        qCDebug(LOG_ALPINEAPK) << "KAuth helper returned error:" << message << reply->error();
+        if (reply->error() == KAuth::ActionReply::Error::AuthorizationDeniedError) {
+            Q_EMIT passiveMessage(i18n("Authorization denied"));
+        } else {
+            Q_EMIT passiveMessage(i18n("Error") + QStringLiteral(":\n") + message);
+        }
+    }
+
+    // we are not in the state "Fetching updates" now, update UI
+    Q_EMIT checkForUpdatesFinished();
+}
+
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.h b/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.h
new file mode 100644
index 000000000..504e8c591
--- /dev/null
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.h
@@ -0,0 +1,190 @@
+/***************************************************************************
+ *   Copyright © 2020 Alexey Min <alexey.min@gmail.com>                    *
+ *                                                                         *
+ *   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 <http://www.gnu.org/licenses/>. *
+ ***************************************************************************/
+
+#ifndef ALPINEAPKUPDATER_H
+#define ALPINEAPKUPDATER_H
+
+#include "resources/AbstractBackendUpdater.h"
+#include "resources/AbstractResourcesBackend.h"
+
+#include <QSet>
+#include <QDateTime>
+#include <QTimer>
+#include <QVariant>
+#include <QMap>
+#include <QVector>
+
+#include <QtApkChangeset.h>
+
+class AbstractResourcesBackend;
+class AlpineApkBackend;
+class KJob;
+
+class AlpineApkUpdater : public AbstractBackendUpdater
+{
+    Q_OBJECT
+    Q_PROPERTY(int updatesCount READ updatesCount NOTIFY updatesCountChanged)
+
+public:
+    explicit AlpineApkUpdater(AbstractResourcesBackend *parent = nullptr);
+
+    /**
+     * This method is called, when Muon switches to the updates view.
+     * Here the backend should mark all upgradeable packages as to be upgraded.
+     */
+    void prepare() override;
+
+    /**
+     * @returns true if the backend contains packages which can be updated
+     */
+    bool hasUpdates() const override;
+    /**
+     * @returns the progress of the update in percent
+     */
+    qreal progress() const override;
+
+    /**
+     * This method is used to remove resources from the list of packages
+     * marked to be upgraded. It will potentially be called before \start.
+     */
+    void removeResources(const QList<AbstractResource*> &apps) override;
+
+    /**
+     * This method is used to add resource to the list of packages marked to be upgraded.
+     * It will potentially be called before \start.
+     */
+    void addResources(const QList<AbstractResource*> &apps) override;
+
+    /**
+     * @returns the list of updateable resources in the system
+     */
+    QList<AbstractResource *> toUpdate() const override;
+
+    /**
+     * @returns the QDateTime when the last update happened
+     */
+    QDateTime lastUpdate() const override;
+
+    /**
+     * @returns whether the updater can currently be canceled or not
+     * @see cancelableChanged
+     */
+    bool isCancelable() const override;
+
+    /**
+     * @returns whether the updater is currently running or not
+     * this property decides, if there will be progress reporting in the GUI.
+     * This has to stay true during the whole transaction!
+     * @see progressingChanged
+     */
+    bool isProgressing() const override;
+
+    /**
+     * @returns whether @p res is marked for update
+     */
+    bool isMarked(AbstractResource* res) const override;
+
+    void fetchChangelog() const override;
+
+    /**
+     * @returns the size of all the packages set to update combined
+     */
+    double updateSize() const override;
+
+    /**
+     * @returns the speed at which we are downloading
+     */
+    quint64 downloadSpeed() const override;
+
+public Q_SLOTS:
+    /**
+     * If \isCancelable is true during the transaction, this method has
+     * to be implemented and will potentially be called when the user
+     * wants to cancel the update.
+     */
+    void cancel() override;
+
+    /**
+     * This method starts the update. All packages which are in \toUpdate
+     * are going to be updated.
+     *
+     * From this moment on the AbstractBackendUpdater should continuously update
+     * the other methods to show its progress.
+     *
+     * @see progress
+     * @see progressChanged
+     * @see isProgressing
+     * @see progressingChanged
+     */
+    void start() override;
+
+    /**
+     * Answers a proceed request
+     */
+    void proceed() override;
+
+Q_SIGNALS:
+    void checkForUpdatesFinished();
+    void updatesCountChanged(int updatesCount);
+    void fetchingUpdatesProgressChanged(int progress);
+    //void cancelTransaction();
+
+public Q_SLOTS:
+    int updatesCount();
+    void startCheckForUpdates();
+
+    // KAuth handler slots
+    // update
+    void handleKAuthUpdateHelperReply(KJob *job);
+    void handleKAuthUpdateHelperProgress(KJob *job, unsigned long percent);
+    // upgrade
+    void handleKAuthUpgradeHelperReply(KJob *job);
+
+    //void transactionRemoved(Transaction* t);
+    //void cleanup();
+
+public:
+    QVector<QtApk::ChangesetItem> &changes() { return m_upgradeable.changes(); }
+    const QVector<QtApk::ChangesetItem> &changes() const { return m_upgradeable.changes(); }
+
+private:
+    AlpineApkBackend *const m_backend;
+    int m_updatesCount = 0;
+    QtApk::Changeset m_upgradeable;
+    QSet<AbstractResource *> m_allUpdateable;
+    QSet<AbstractResource *> m_markedToUpdate;
+//    void resourcesChanged(AbstractResource* res, const QVector<QByteArray>& props);
+//    void refreshUpdateable();
+//    void transactionAdded(Transaction* newTransaction);
+//    void transactionProgressChanged();
+//    void refreshProgress();
+//    QVector<Transaction*> transactions() const;
+
+//    QSet<AbstractResource*> m_upgradeable;
+//    QSet<AbstractResource*> m_pendingResources;
+//    bool m_settingUp;
+//    qreal m_progress;
+//    QDateTime m_lastUpdate;
+//    QTimer m_timer;
+//    bool m_canCancel = false;
+};
+
+
+#endif // ALPINEAPKUPDATER_H
diff --git a/libdiscover/backends/AlpineApkBackend/CMakeLists.txt b/libdiscover/backends/AlpineApkBackend/CMakeLists.txt
index ede3157f0..381849453 100644
--- a/libdiscover/backends/AlpineApkBackend/CMakeLists.txt
+++ b/libdiscover/backends/AlpineApkBackend/CMakeLists.txt
@@ -9,6 +9,8 @@ set(alpineapkbackend_SRCS
     AlpineApkReviewsBackend.h
     AlpineApkSourcesBackend.cpp
     AlpineApkSourcesBackend.h
+    AlpineApkUpdater.cpp
+    AlpineApkUpdater.h
     AlpineApkTransaction.cpp
     AlpineApkTransaction.h
 )
-- 
GitLab


From b46679efff222c9716b7a431a9e7beff94c02cca Mon Sep 17 00:00:00 2001
From: Alexey Min <alexey.min@gmail.com>
Date: Mon, 3 Feb 2020 03:52:41 +0300
Subject: [PATCH 10/59] AlpineApkBackend: use our new updater

---
 .../AlpineApkBackend/AlpineApkBackend.cpp     | 158 ++++++++----------
 .../AlpineApkBackend/AlpineApkBackend.h       |  21 ++-
 2 files changed, 85 insertions(+), 94 deletions(-)

diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp
index ed016aef3..757380186 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp
@@ -23,19 +23,14 @@
 #include "AlpineApkReviewsBackend.h"
 #include "AlpineApkTransaction.h"
 #include "AlpineApkSourcesBackend.h"
+#include "AlpineApkUpdater.h"
 #include "alpineapk_backend_logging.h"  // generated by ECM
 
-#include "resources/StandardBackendUpdater.h"
 #include "resources/SourcesModel.h"
 #include "Transaction/Transaction.h"
 #include "Category/Category.h"
 
-#include <KAboutData>
 #include <KLocalizedString>
-#include <KPluginFactory>
-#include <KConfigGroup>
-#include <KSharedConfig>
-#include <KAuthExecuteJob>
 
 #include <QDebug>
 #include <QLoggingCategory>
@@ -47,45 +42,33 @@ DISCOVER_BACKEND_PLUGIN(AlpineApkBackend)
 
 AlpineApkBackend::AlpineApkBackend(QObject *parent)
     : AbstractResourcesBackend(parent)
-    , m_updater(new StandardBackendUpdater(this))
+    , m_updater(new AlpineApkUpdater(this))
     , m_reviews(new AlpineApkReviewsBackend(this))
-    , m_startElements(120)
+    , m_updatesTimeoutTimer(new QTimer(this))
 {
 #ifndef QT_DEBUG
     const_cast<QLoggingCategory &>(LOG_ALPINEAPK()).setEnabled(QtDebugMsg, false);
 #endif
 
-    qCDebug(LOG_ALPINEAPK) << "constructing backend!";
+    // schedule checking for updates
+    QTimer::singleShot(1000, this, &AlpineApkBackend::checkForUpdates);
 
-    QTimer::singleShot(1000, this, &AlpineApkBackend::startCheckForUpdates);
-
-    QObject::connect(m_updater, &StandardBackendUpdater::updatesCountChanged,
+    // connections with our updater
+    QObject::connect(m_updater, &AlpineApkUpdater::updatesCountChanged,
                      this, &AlpineApkBackend::updatesCountChanged);
+    QObject::connect(m_updater, &AlpineApkUpdater::checkForUpdatesFinished,
+                     this, &AlpineApkBackend::finishCheckForUpdates);
+    QObject::connect(m_updater, &AlpineApkUpdater::fetchingUpdatesProgressChanged,
+                     this, &AlpineApkBackend::setFetchingUpdatesProgress);
 
-    populate();
-
-    SourcesModel::global()->addSourcesBackend(new AlpineApkSourcesBackend(this));
-}
-
-QVector<Category *> AlpineApkBackend::category() const
-{
-    // single root category
-    // we could add more, but Alpine apk does not have this concept
-    static Category *cat = new Category(
-                QStringLiteral("All packages"),    // displayName
-                QStringLiteral("package-x-generic"), // icon
-                {}, // orFilters
-                { displayName() }, // pluginName
-                {}, // subCategories
-                QUrl(), // decoration (what is it?)
-                false  // isAddons
-    );
-    return { cat };
-}
+    // safety measure: make sure update check process can finish in some finite time
+    QObject::connect(m_updatesTimeoutTimer, &QTimer::timeout,
+                     this, &AlpineApkBackend::finishCheckForUpdates);
+    m_updatesTimeoutTimer->setTimerType(Qt::CoarseTimer);
+    m_updatesTimeoutTimer->setSingleShot(true);
+    m_updatesTimeoutTimer->setInterval(2 * 60 * 1000); // 2minutes
 
-void AlpineApkBackend::populate()
-{
-    qCDebug(LOG_ALPINEAPK) << "populating resources...";
+    qCDebug(LOG_ALPINEAPK) << "backend: populating resources...";
 
     if (m_apkdb.open(QtApk::Database::QTAPK_OPENF_READONLY)) {
         m_availablePackages = m_apkdb.getAvailablePackages();
@@ -103,7 +86,7 @@ void AlpineApkBackend::populate()
             m_resources.insert(key, res);
             connect(res, &AlpineApkResource::stateChanged, this, &AlpineApkBackend::updatesCountChanged);
         }
-        qCDebug(LOG_ALPINEAPK) << " available" << m_availablePackages.size()
+        qCDebug(LOG_ALPINEAPK) << "  available" << m_availablePackages.size()
                                << "packages";
     }
     if (m_installedPackages.size() > 0) {
@@ -113,62 +96,31 @@ void AlpineApkBackend::populate()
                 m_resources.value(key)->setState(AbstractResource::Installed);
             }
         }
-        qCDebug(LOG_ALPINEAPK) << " installed" << m_installedPackages.size()
+        qCDebug(LOG_ALPINEAPK) << "  installed" << m_installedPackages.size()
                                << "packages";
     }
-}
-
-void AlpineApkBackend::handleKauthHelperReply(KJob *job)
-{
-    qCDebug(LOG_ALPINEAPK) << Q_FUNC_INFO;
-    KAuth::ExecuteJob* reply = static_cast<KAuth::ExecuteJob *>(job);
-    const QVariantMap replyData = reply->data();
-    if (reply->error() == 0) {
-        qCDebug(LOG_ALPINEAPK) << replyData[QLatin1String("reply")].toString();
-    } else {
-        const QString message = replyData.value(QLatin1String("errorString"), reply->errorString()).toString();
-        qCDebug(LOG_ALPINEAPK) << message;
-    }
-}
-
-void AlpineApkBackend::startCheckForUpdates()
-{
-    if (m_fetching) {
-        return;
-    }
-    qCDebug(LOG_ALPINEAPK) << "startCheckForUpdates()";
-
-    // temporary hack - finish updates check in 5 seconds
-    QTimer::singleShot(5000, this, &AlpineApkBackend::finishCheckForUpdates);
-
-    m_fetching = true;
-    emit fetchingChanged();
-
-    KAuth::Action testAction(QStringLiteral("org.kde.discover.alpineapkbackend.test"));
-    testAction.setHelperId(QStringLiteral("org.kde.discover.alpineapkbackend"));
-    testAction.setArguments({
-        { QStringLiteral("txt"), QLatin1String("Wooo!") },
-    });
-    if (!testAction.isValid()) {
-        qCWarning(LOG_ALPINEAPK) << "kauth action is not valid!";
-        return;
-    }
 
-    KAuth::ExecuteJob *reply = testAction.execute();
-    QObject::connect(reply, &KAuth::ExecuteJob::result,
-                     this, &AlpineApkBackend::handleKauthHelperReply);
-    reply->start();
+    SourcesModel::global()->addSourcesBackend(new AlpineApkSourcesBackend(this));
 }
 
-void AlpineApkBackend::finishCheckForUpdates()
+QVector<Category *> AlpineApkBackend::category() const
 {
-    m_fetching = false;
-    emit fetchingChanged();
+    // single root category
+    // we could add more, but Alpine apk does not have this concept
+    static Category *cat = new Category(
+                i18nc("Root category name", "Alpine packages"),
+                QStringLiteral("package-x-generic"), // icon
+                {},                // orFilters
+                { displayName() }, // pluginName
+                {},                // subCategories
+                QUrl(),            // decoration (what is it?)
+                false              // isAddons
+    );
+    return { cat };
 }
 
 int AlpineApkBackend::updatesCount() const
 {
-    qCDebug(LOG_ALPINEAPK) << "updatesCount(): " << m_updater->updatesCount();
     return m_updater->updatesCount();
 }
 
@@ -210,7 +162,7 @@ ResultsStream *AlpineApkBackend::findResourceByPackageName(const QUrl &searchUrl
     AlpineApkResource *result = nullptr;
 
     // QUrl("appstream://org.kde.krita.desktop")
-    // smart workaround for appstream
+    // smart workaround for appstream URLs
     if (searchUrl.scheme()  == QLatin1String("appstream")) {
         // remove leading "org.kde."
         QString pkgName = searchUrl.host();
@@ -256,18 +208,46 @@ Transaction* AlpineApkBackend::removeApplication(AbstractResource *app)
     return new AlpineApkTransaction(qobject_cast<AlpineApkResource *>(app), Transaction::RemoveRole);
 }
 
+int AlpineApkBackend::fetchingUpdatesProgress() const
+{
+    if (!m_fetching) return 100;
+    return m_fetchProgress;
+}
+
 void AlpineApkBackend::checkForUpdates()
 {
     if (m_fetching) {
-        qCDebug(LOG_ALPINEAPK) << "checkForUpdates(): already fetching";
+        qCDebug(LOG_ALPINEAPK) << "backend: checkForUpdates(): already fetching";
         return;
     }
-    startCheckForUpdates();
+
+    qCDebug(LOG_ALPINEAPK) << "backend: start checkForUpdates()";
+
+    // safety measure - finish updates check in some time
+    m_updatesTimeoutTimer->start();
+
+    // let our updater do the job
+    m_updater->startCheckForUpdates();
+
+    // update UI
+    m_fetching = true;
+    m_fetchProgress = 0;
+    emit fetchingChanged();
+    emit fetchingUpdatesProgressChanged();
+}
+
+void AlpineApkBackend::finishCheckForUpdates()
+{
+    m_updatesTimeoutTimer->stop(); // stop safety timer
+    // update UI
+    m_fetching = false;
+    emit fetchingChanged();
+    emit fetchingUpdatesProgressChanged();
 }
 
 QString AlpineApkBackend::displayName() const
 {
-    return QStringLiteral("Alpine APK backend");
+    return i18nc("Backend plugin display name", "Alpine APK backend");
 }
 
 bool AlpineApkBackend::hasApplications() const
@@ -275,4 +255,10 @@ bool AlpineApkBackend::hasApplications() const
     return true;
 }
 
+void AlpineApkBackend::setFetchingUpdatesProgress(int percent)
+{
+    m_fetchProgress = percent;
+    emit fetchingUpdatesProgressChanged();
+}
+
 #include "AlpineApkBackend.moc"
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.h b/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.h
index a533cee39..d69352575 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.h
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.h
@@ -27,14 +27,14 @@
 #include <QtApk.h>
 
 class AlpineApkReviewsBackend;
-class StandardBackendUpdater;
+class AlpineApkUpdater;
 class AlpineApkResource;
 class KJob;
+class QTimer;
 
 class AlpineApkBackend : public AbstractResourcesBackend
 {
     Q_OBJECT
-    Q_PROPERTY(int startElements MEMBER m_startElements)
 
 public:
     explicit AlpineApkBackend(QObject *parent = nullptr);
@@ -46,32 +46,37 @@ public:
     ResultsStream *search(const AbstractResourcesBackend::Filters &filter) override;
     ResultsStream *findResourceByPackageName(const QUrl &search);
     QHash<QString, AlpineApkResource *> resources() const { return m_resources; }
+    QHash<QString, AlpineApkResource *> *resourcesPtr() { return &m_resources; }
     bool isValid() const override { return true; } // No external file dependencies that could cause runtime errors
 
     Transaction *installApplication(AbstractResource *app) override;
     Transaction *installApplication(AbstractResource *app, const AddonList &addons) override;
     Transaction *removeApplication(AbstractResource *app) override;
     bool isFetching() const override { return m_fetching; }
+    int fetchingUpdatesProgress() const override;
     void checkForUpdates() override;
     QString displayName() const override;
     bool hasApplications() const override;
 
 public Q_SLOTS:
-    void handleKauthHelperReply(KJob *job);
-    void startCheckForUpdates();
+    void setFetchingUpdatesProgress(int percent);
+
+private Q_SLOTS:
     void finishCheckForUpdates();
 
-private:
-    void populate();
+public:
+    QtApk::Database *apkdb() { return &m_apkdb; }
 
+private:
     QHash<QString, AlpineApkResource *> m_resources;
-    StandardBackendUpdater *m_updater;
+    AlpineApkUpdater *m_updater;
     AlpineApkReviewsBackend *m_reviews;
     QtApk::Database m_apkdb;
     QVector<QtApk::Package> m_availablePackages;
     QVector<QtApk::Package> m_installedPackages;
     bool m_fetching = false;
-    int m_startElements = 0;
+    int m_fetchProgress = 0;
+    QTimer *m_updatesTimeoutTimer;
 };
 
 #endif // AlpineApkBackend_H
-- 
GitLab


From 3b96f434d3f0c8a47eb959bd69bd80fb84da62aa Mon Sep 17 00:00:00 2001
From: Alexey Min <alexey.min@gmail.com>
Date: Mon, 3 Feb 2020 18:21:04 +0300
Subject: [PATCH 11/59] AlpineApk{Updater,AuthHelper}: pass fakeRoot to helper

Env vars are not inherited by elevated helper
---
 .../backends/AlpineApkBackend/AlpineApkAuthHelper.cpp | 11 ++++++++++-
 .../backends/AlpineApkBackend/AlpineApkUpdater.cpp    |  5 ++++-
 2 files changed, 14 insertions(+), 2 deletions(-)

diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp
index 4ff5c880a..aa3dbdae1 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp
@@ -64,9 +64,13 @@ ActionReply AlpineApkAuthHelper::test(const QVariantMap &args)
 
 ActionReply AlpineApkAuthHelper::update(const QVariantMap &args)
 {
-    Q_UNUSED(args)
     ActionReply reply = ActionReply::HelperErrorReply();
 
+    const QString fakeRoot = args.value(QLatin1String("fakeRoot"), QString()).toString();
+    if (!fakeRoot.isEmpty()) {
+        m_apkdb.setFakeRoot(fakeRoot);
+    }
+
     HelperSupport::progressStep(10);
 
     if (!m_apkdb.open(QtApk::Database::QTAPK_OPENF_READWRITE)) {
@@ -125,6 +129,11 @@ ActionReply AlpineApkAuthHelper::upgrade(const QVariantMap &args)
         qCDebug(LOG_AUTHHELPER) << "Simulating upgrade run.";
     }
 
+    const QString fakeRoot = args.value(QLatin1String("fakeRoot"), QString()).toString();
+    if (!fakeRoot.isEmpty()) {
+        m_apkdb.setFakeRoot(fakeRoot);
+    }
+
     QtApk::Changeset changes;
     bool upgrade_ok = m_apkdb.upgrade(flags, &changes);
 
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp
index 1dd6cdacd..d0c0482ef 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp
@@ -47,6 +47,7 @@ void AlpineApkUpdater::prepare()
         return;
     }
 
+    // readonly is fine for a simulation of upgrade
     if (!db->open(QtApk::Database::QTAPK_OPENF_READONLY)) {
         emit passiveMessage(i18n("Failed to open APK database!"));
         return;
@@ -57,7 +58,7 @@ void AlpineApkUpdater::prepare()
         db->close();
         return;
     }
-    // clsoe DB ASAP
+    // close DB ASAP
     db->close();
 
     m_updatesCount = m_upgradeable.changes().size();
@@ -202,6 +203,7 @@ int AlpineApkUpdater::updatesCount()
 
 void AlpineApkUpdater::startCheckForUpdates()
 {
+    QtApk::Database *db = m_backend->apkdb();
     KAuth::Action updateAction(QStringLiteral("org.kde.discover.alpineapkbackend.update"));
     updateAction.setHelperId(QStringLiteral("org.kde.discover.alpineapkbackend"));
     if (!updateAction.isValid()) {
@@ -210,6 +212,7 @@ void AlpineApkUpdater::startCheckForUpdates()
     }
     updateAction.setTimeout(60 * 1000); // 1 minute
     updateAction.setDetails(i18n("Update repositories index"));
+    updateAction.addArgument(QLatin1String("fakeRoot"), db->fakeRoot());
 
     // run updates check with elevated privileges to access
     // system package manager files
-- 
GitLab


From 3cc9d80dcb37e359571256af7d5dcbbd4cd903c4 Mon Sep 17 00:00:00 2001
From: Alexey Min <alexey.min@gmail.com>
Date: Mon, 3 Feb 2020 20:14:13 +0300
Subject: [PATCH 12/59] Auth helper: WIP on using progress_fd

---
 .../AlpineApkBackend/AlpineApkAuthHelper.cpp        | 13 ++++++++++---
 1 file changed, 10 insertions(+), 3 deletions(-)

diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp
index aa3dbdae1..4c3bf5458 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp
@@ -21,9 +21,8 @@
 #include <QProcess>
 #include <QDebug>
 #include <QLoggingCategory>
-#include <QJsonDocument>
-#include <QJsonObject>
-#include <QJsonArray>
+#include <QSocketNotifier>
+#include <QScopedPointer>
 #include <QFile>
 
 #include <KAuthHelperSupport>
@@ -134,6 +133,14 @@ ActionReply AlpineApkAuthHelper::upgrade(const QVariantMap &args)
         m_apkdb.setFakeRoot(fakeRoot);
     }
 
+    int progress_fd = m_apkdb.progressFd();
+    qCDebug(LOG_AUTHHELPER) << "    progress_fd: " << progress_fd;
+
+    QScopedPointer<QSocketNotifier> notifier(new QSocketNotifier(progress_fd, QSocketNotifier::Read));
+    QObject::connect(notifier.data(), &QSocketNotifier::activated, notifier.data(), [](int sock) {
+        qCDebug(LOG_AUTHHELPER) << "        read trigger from progress_fd!";
+    });
+
     QtApk::Changeset changes;
     bool upgrade_ok = m_apkdb.upgrade(flags, &changes);
 
-- 
GitLab


From 6ba625b2a8c79e6c25c23df13b0be5f5686bb3c4 Mon Sep 17 00:00:00 2001
From: Alexey Min <alexey.min@gmail.com>
Date: Fri, 20 Mar 2020 17:53:01 +0300
Subject: [PATCH 13/59] auth helper: fix usused vars

---
 libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp
index 4c3bf5458..bc9c1ec67 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp
@@ -100,12 +100,14 @@ ActionReply AlpineApkAuthHelper::update(const QVariantMap &args)
 
 ActionReply AlpineApkAuthHelper::add(const QVariantMap &args)
 {
+    Q_UNUSED(args)
     ActionReply reply = ActionReply::HelperErrorReply();
     return reply;
 }
 
 ActionReply AlpineApkAuthHelper::del(const QVariantMap &args)
 {
+    Q_UNUSED(args)
     ActionReply reply = ActionReply::HelperErrorReply();
     return reply;
 }
@@ -138,6 +140,7 @@ ActionReply AlpineApkAuthHelper::upgrade(const QVariantMap &args)
 
     QScopedPointer<QSocketNotifier> notifier(new QSocketNotifier(progress_fd, QSocketNotifier::Read));
     QObject::connect(notifier.data(), &QSocketNotifier::activated, notifier.data(), [](int sock) {
+        Q_UNUSED(sock)
         qCDebug(LOG_AUTHHELPER) << "        read trigger from progress_fd!";
     });
 
-- 
GitLab


From a47c3b97242448f16e3dc719e90aae55d0108e80 Mon Sep 17 00:00:00 2001
From: Alexey Min <alexey.min@gmail.com>
Date: Wed, 8 Apr 2020 05:55:33 +0300
Subject: [PATCH 14/59] AlpineApkBackend: cmake: use namespaced ApkQt

---
 libdiscover/backends/AlpineApkBackend/CMakeLists.txt | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/libdiscover/backends/AlpineApkBackend/CMakeLists.txt b/libdiscover/backends/AlpineApkBackend/CMakeLists.txt
index 381849453..42e09c5b6 100644
--- a/libdiscover/backends/AlpineApkBackend/CMakeLists.txt
+++ b/libdiscover/backends/AlpineApkBackend/CMakeLists.txt
@@ -38,7 +38,7 @@ target_link_libraries(
         KF5::ConfigCore
         KF5::AuthCore
         Discover::Common
-        apk-qt
+        ApkQt::apk-qt
 )
 
 # KAuth helper exe
@@ -54,7 +54,7 @@ set_source_files_properties(
 target_link_libraries(alpineapk_kauth_helper
     Qt5::Core
     KF5::AuthCore
-    apk-qt
+    ApkQt::apk-qt
 )
 
 kauth_install_actions(org.kde.discover.alpineapkbackend org.kde.discover.alpineapkbackend.actions)
-- 
GitLab


From b0d3a719e35468b9583ff7c2be02fd969d05c50b Mon Sep 17 00:00:00 2001
From: Alexey Min <alexey.min@gmail.com>
Date: Thu, 9 Apr 2020 21:18:33 +0300
Subject: [PATCH 15/59] AlpineApkBackend: mark pkgs as Technical type and fix
 searhes

This makes updates appear as "System updates" in updates view
and also makes them correctly categorized in top level
categories when other backend is installed (Flatpak, KNewStuff)
---
 .../AlpineApkBackend/AlpineApkBackend.cpp     | 62 ++++++++++++-------
 .../AlpineApkBackend/AlpineApkResource.cpp    |  2 +-
 2 files changed, 42 insertions(+), 22 deletions(-)

diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp
index 757380186..750d2820c 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp
@@ -79,9 +79,12 @@ AlpineApkBackend::AlpineApkBackend(QObject *parent)
     if (m_availablePackages.size() > 0) {
         for (const QtApk::Package &pkg: m_availablePackages) {
             AlpineApkResource *res = new AlpineApkResource(pkg, this);
-            res->setCategoryName(QStringLiteral("all"));
+            res->setCategoryName(QStringLiteral("alpine_packages"));
             res->setOriginSource(QStringLiteral("apk"));
             res->setSection(QStringLiteral("dummy"));
+            // here is the place to set a proper type of package
+            // AlpineApkResource defaults to AbstractResource::Technical,
+            // which places it into "System updates" section
             const QString key = pkg.name.toLower();
             m_resources.insert(key, res);
             connect(res, &AlpineApkResource::stateChanged, this, &AlpineApkBackend::updatesCountChanged);
@@ -105,18 +108,22 @@ AlpineApkBackend::AlpineApkBackend(QObject *parent)
 
 QVector<Category *> AlpineApkBackend::category() const
 {
-    // single root category
+    static QPair<FilterType, QString> s_apkFlt(
+                FilterType::CategoryFilter, QLatin1String("alpine_packages"));
+
+    // Display a single root category
     // we could add more, but Alpine apk does not have this concept
-    static Category *cat = new Category(
-                i18nc("Root category name", "Alpine packages"),
-                QStringLiteral("package-x-generic"), // icon
-                {},                // orFilters
-                { displayName() }, // pluginName
-                {},                // subCategories
-                QUrl(),            // decoration (what is it?)
-                false              // isAddons
+    static Category *s_rootCat = new Category(
+            i18nc("Root category name", "Alpine packages"),
+            QStringLiteral("package-x-generic"), // icon
+            { s_apkFlt },      // orFilters - include packages that match filter
+            { displayName() }, // pluginName
+            {},                // subCategories - none
+            QUrl(),            // decoration (what is it?)
+            false              // isAddons
     );
-    return { cat };
+
+    return { s_rootCat };
 }
 
 int AlpineApkBackend::updatesCount() const
@@ -130,17 +137,23 @@ ResultsStream *AlpineApkBackend::search(const AbstractResourcesBackend::Filters
     if (!filter.resourceUrl.isEmpty()) {
         return findResourceByPackageName(filter.resourceUrl);
     } else {
-        for (AbstractResource *r: qAsConst(m_resources)) {
-            if (r->type() == AbstractResource::Technical
-                    && filter.state != AbstractResource::Upgradeable) {
-                continue;
-            }
-            if (r->state() < filter.state) {
+        for (AbstractResource *resource: qAsConst(m_resources)) {
+            // skip technical package types (not apps/addons)
+            //      that are not upgradeable
+            //  (does not work because for now all Alpine packages are "technical"
+            // if (resource->type() == AbstractResource::Technical
+            //         && filter.state != AbstractResource::Upgradeable) {
+            //     continue;
+            // }
+
+            // skip not-requested states
+            if (resource->state() < filter.state) {
                 continue;
             }
-            if(r->name().contains(filter.search, Qt::CaseInsensitive)
-                    || r->comment().contains(filter.search, Qt::CaseInsensitive)) {
-                ret += r;
+
+            if(resource->name().contains(filter.search, Qt::CaseInsensitive)
+                    || resource->comment().contains(filter.search, Qt::CaseInsensitive)) {
+                ret += resource;
             }
         }
     }
@@ -162,7 +175,7 @@ ResultsStream *AlpineApkBackend::findResourceByPackageName(const QUrl &searchUrl
     AlpineApkResource *result = nullptr;
 
     // QUrl("appstream://org.kde.krita.desktop")
-    // smart workaround for appstream URLs
+    // smart workaround for appstream URLs - handle "featured" apps
     if (searchUrl.scheme()  == QLatin1String("appstream")) {
         // remove leading "org.kde."
         QString pkgName = searchUrl.host();
@@ -177,6 +190,13 @@ ResultsStream *AlpineApkBackend::findResourceByPackageName(const QUrl &searchUrl
         result = m_resources.value(pkgName);
     }
 
+    // QUrl("apk://krita")
+    // handle packages from Alpine repos
+    if (searchUrl.scheme() == QLatin1String("apk")) {
+        const QString pkgName = searchUrl.host();
+        result = m_resources.value(pkgName);
+    }
+
     if (!result) {
         return new ResultsStream(QStringLiteral("AlpineApkStream"), {});
     }
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkResource.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkResource.cpp
index af480b6c6..346a28b28 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkResource.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkResource.cpp
@@ -26,7 +26,7 @@ AlpineApkResource::AlpineApkResource(const QtApk::Package &apkPkg,
                                      AbstractResourcesBackend *parent)
     : AbstractResource(parent)
     , m_state(AbstractResource::State::None)
-    , m_type(Application)
+    , m_type(AbstractResource::Type::Technical)
     , m_pkg(apkPkg)
 {
 }
-- 
GitLab


From 476d8cdc6dc44f5dfb2e23220f3f6e986c776820 Mon Sep 17 00:00:00 2001
From: Alexey Min <alexey.min@gmail.com>
Date: Fri, 10 Apr 2020 09:00:29 +0300
Subject: [PATCH 16/59] KAuth helper: disable progress_fd for now

---
 .../AlpineApkBackend/AlpineApkAuthHelper.cpp     | 16 +++++++++-------
 1 file changed, 9 insertions(+), 7 deletions(-)

diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp
index bc9c1ec67..11e267866 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp
@@ -26,6 +26,7 @@
 #include <QFile>
 
 #include <KAuthHelperSupport>
+#include <kauth_version.h>
 
 #include "AlpineApkAuthHelper.h"
 
@@ -135,14 +136,15 @@ ActionReply AlpineApkAuthHelper::upgrade(const QVariantMap &args)
         m_apkdb.setFakeRoot(fakeRoot);
     }
 
-    int progress_fd = m_apkdb.progressFd();
-    qCDebug(LOG_AUTHHELPER) << "    progress_fd: " << progress_fd;
+    // no progress notifications for now
+    //int progress_fd = m_apkdb.progressFd();
+    //qCDebug(LOG_AUTHHELPER) << "    progress_fd: " << progress_fd;
 
-    QScopedPointer<QSocketNotifier> notifier(new QSocketNotifier(progress_fd, QSocketNotifier::Read));
-    QObject::connect(notifier.data(), &QSocketNotifier::activated, notifier.data(), [](int sock) {
-        Q_UNUSED(sock)
-        qCDebug(LOG_AUTHHELPER) << "        read trigger from progress_fd!";
-    });
+    //QScopedPointer<QSocketNotifier> notifier(new QSocketNotifier(progress_fd, QSocketNotifier::Read));
+    //QObject::connect(notifier.data(), &QSocketNotifier::activated, notifier.data(), [](int sock) {
+    //    Q_UNUSED(sock)
+    //    qCDebug(LOG_AUTHHELPER) << "        read trigger from progress_fd!";
+    //});
 
     QtApk::Changeset changes;
     bool upgrade_ok = m_apkdb.upgrade(flags, &changes);
-- 
GitLab


From ced6622eabfcf04a5a712c07c0ba8286005fcaca Mon Sep 17 00:00:00 2001
From: Alexey Min <alexey.min@gmail.com>
Date: Fri, 10 Apr 2020 09:06:17 +0300
Subject: [PATCH 17/59] AlpineApkUpdater: refactor out helper error handler
 func

---
 .../AlpineApkBackend/AlpineApkUpdater.cpp     | 37 +++++++++----------
 .../AlpineApkBackend/AlpineApkUpdater.h       |  6 +++
 2 files changed, 24 insertions(+), 19 deletions(-)

diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp
index d0c0482ef..173b7e48c 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp
@@ -215,12 +215,11 @@ void AlpineApkUpdater::startCheckForUpdates()
     updateAction.addArgument(QLatin1String("fakeRoot"), db->fakeRoot());
 
     // run updates check with elevated privileges to access
-    // system package manager files
+    //     system package manager files
     KAuth::ExecuteJob *reply = updateAction.execute();
     QObject::connect(reply, &KAuth::ExecuteJob::result,
                      this, &AlpineApkUpdater::handleKAuthUpdateHelperReply);
-    //QObject::connect(reply, &KAuth::ExecuteJob::newData,
-    //                 this, &AlpineApkUpdater::handleKAuthUpdateHelperProgressStep);
+    // qOverload is needed because of conflict with getter named percent()
     QObject::connect(reply, QOverload<KJob *, unsigned long>::of(&KAuth::ExecuteJob::percent),
                      this, &AlpineApkUpdater::handleKAuthUpdateHelperProgress);
 
@@ -236,14 +235,7 @@ void AlpineApkUpdater::handleKAuthUpdateHelperReply(KJob *job)
         qCDebug(LOG_ALPINEAPK) << "KAuth helper update reply received, updatesCount:" << m_updatesCount;
         Q_EMIT updatesCountChanged(m_updatesCount);
     } else {
-        const QString message = replyData.value(QLatin1String("errorString"),
-                                                reply->errorString()).toString();
-        qCDebug(LOG_ALPINEAPK) << "KAuth helper returned error:" << message << reply->error();
-        if (reply->error() == KAuth::ActionReply::Error::AuthorizationDeniedError) {
-            Q_EMIT passiveMessage(i18n("Authorization denied"));
-        } else {
-            Q_EMIT passiveMessage(i18n("Error") + QStringLiteral(":\n") + message);
-        }
+        handleKAuthHelperError(reply, replyData);
     }
 
     // we are not in the state "Fetching updates" now, update UI
@@ -273,17 +265,24 @@ void AlpineApkUpdater::handleKAuthUpgradeHelperReply(KJob *job)
             }
         }
     } else {
-        const QString message = replyData.value(QLatin1String("errorString"),
-                                                reply->errorString()).toString();
-        qCDebug(LOG_ALPINEAPK) << "KAuth helper returned error:" << message << reply->error();
-        if (reply->error() == KAuth::ActionReply::Error::AuthorizationDeniedError) {
-            Q_EMIT passiveMessage(i18n("Authorization denied"));
-        } else {
-            Q_EMIT passiveMessage(i18n("Error") + QStringLiteral(":\n") + message);
-        }
+        handleKAuthHelperError(reply, replyData);
     }
 
     // we are not in the state "Fetching updates" now, update UI
     Q_EMIT checkForUpdatesFinished();
 }
 
+void AlpineApkUpdater::handleKAuthHelperError(
+        KAuth::ExecuteJob *reply,
+        const QVariantMap &replyData)
+{
+    const QString message = replyData.value(QLatin1String("errorString"),
+                                            reply->errorString()).toString();
+    qCDebug(LOG_ALPINEAPK) << "KAuth helper returned error:" << message << reply->error();
+    if (reply->error() == KAuth::ActionReply::Error::AuthorizationDeniedError) {
+        Q_EMIT passiveMessage(i18n("Authorization denied"));
+    } else {
+        Q_EMIT passiveMessage(i18n("Error") + QStringLiteral(":\n") + message);
+    }
+}
+
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.h b/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.h
index 504e8c591..77140ca25 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.h
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.h
@@ -36,6 +36,9 @@
 class AbstractResourcesBackend;
 class AlpineApkBackend;
 class KJob;
+namespace KAuth {
+    class ExecuteJob;
+}
 
 class AlpineApkUpdater : public AbstractBackendUpdater
 {
@@ -164,6 +167,9 @@ public:
     QVector<QtApk::ChangesetItem> &changes() { return m_upgradeable.changes(); }
     const QVector<QtApk::ChangesetItem> &changes() const { return m_upgradeable.changes(); }
 
+protected:
+    void handleKAuthHelperError(KAuth::ExecuteJob *reply, const QVariantMap &replyData);
+
 private:
     AlpineApkBackend *const m_backend;
     int m_updatesCount = 0;
-- 
GitLab


From 6c41dbb556804e2d7f6bd422005a4431dadb1f49 Mon Sep 17 00:00:00 2001
From: Alexey Min <alexey.min@gmail.com>
Date: Fri, 10 Apr 2020 09:07:38 +0300
Subject: [PATCH 18/59] AlpineApkUpdater: use non-deprecated version of
 setDetails(V2)

since KF 5.68
---
 .../AlpineApkBackend/AlpineApkUpdater.cpp     | 20 +++++++++++++++++--
 1 file changed, 18 insertions(+), 2 deletions(-)

diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp
index 173b7e48c..dae6c2a31 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp
@@ -25,6 +25,7 @@
 #include "utils.h"
 
 #include <KAuthExecuteJob>
+#include <kauth_version.h>
 #include <KLocalizedString>
 
 #include <QtApk.h>
@@ -178,8 +179,15 @@ void AlpineApkUpdater::start()
         return;
     }
     upgradeAction.setTimeout(60 * 1000); // 1 minute
-    upgradeAction.setDetails(i18n("Get the list of packages to upgrade"));
-    upgradeAction.addArgument(QLatin1String("onlySimulate"), true);
+#if KAUTH_VERSION < QT_VERSION_CHECK(5, 68, 0)
+    upgradeAction.setDetails(i18n("Upgrade currently installed packages"));
+#else
+    static const KAuth::Action::DetailsMap details{
+        { KAuth::Action::AuthDetail::DetailMessage, i18n("Upgrade currently installed packages") }
+    };
+    upgradeAction.setDetailsV2(details);
+#endif
+    // upgradeAction.addArgument(QLatin1String("onlySimulate"), true);
 
     // run upgrade check with elevated privileges
     KAuth::ExecuteJob *reply = upgradeAction.execute();
@@ -211,7 +219,15 @@ void AlpineApkUpdater::startCheckForUpdates()
         return;
     }
     updateAction.setTimeout(60 * 1000); // 1 minute
+    // setDetails deprecated since KF 5.68, use setDetailsV2() with DetailsMap.
+#if KAUTH_VERSION < QT_VERSION_CHECK(5, 68, 0)
     updateAction.setDetails(i18n("Update repositories index"));
+#else
+    static const KAuth::Action::DetailsMap details{
+        { KAuth::Action::AuthDetail::DetailMessage, i18n("Update repositories index") }
+    };
+    updateAction.setDetailsV2(details);
+#endif
     updateAction.addArgument(QLatin1String("fakeRoot"), db->fakeRoot());
 
     // run updates check with elevated privileges to access
-- 
GitLab


From a41888d57f1e994e51f9aed39491786a36b18da0 Mon Sep 17 00:00:00 2001
From: Alexey Min <alexey.min@gmail.com>
Date: Fri, 10 Apr 2020 09:09:13 +0300
Subject: [PATCH 19/59] WIP: AlpineApkUpdater: enable upgrade action again

---
 .../backends/AlpineApkBackend/AlpineApkUpdater.cpp | 14 ++++++++------
 1 file changed, 8 insertions(+), 6 deletions(-)

diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp
index dae6c2a31..89a816c44 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp
@@ -170,15 +170,17 @@ void AlpineApkUpdater::cancel()
 void AlpineApkUpdater::start()
 {
     qCDebug(LOG_ALPINEAPK) << Q_FUNC_INFO;
-    return;
-#if 0
+    //return;
+//#if 0
     KAuth::Action upgradeAction(QStringLiteral("org.kde.discover.alpineapkbackend.upgrade"));
     upgradeAction.setHelperId(QStringLiteral("org.kde.discover.alpineapkbackend"));
+
     if (!upgradeAction.isValid()) {
         qCWarning(LOG_ALPINEAPK) << "kauth upgradeAction is not valid!";
         return;
     }
-    upgradeAction.setTimeout(60 * 1000); // 1 minute
+
+    upgradeAction.setTimeout(30 * 60 * 1000); // 30 min
 #if KAUTH_VERSION < QT_VERSION_CHECK(5, 68, 0)
     upgradeAction.setDetails(i18n("Upgrade currently installed packages"));
 #else
@@ -189,13 +191,13 @@ void AlpineApkUpdater::start()
 #endif
     // upgradeAction.addArgument(QLatin1String("onlySimulate"), true);
 
-    // run upgrade check with elevated privileges
+    // run upgrade with elevated privileges
     KAuth::ExecuteJob *reply = upgradeAction.execute();
     QObject::connect(reply, &KAuth::ExecuteJob::result,
                      this, &AlpineApkUpdater::handleKAuthUpgradeHelperReply);
 
     reply->start();
-#endif
+//#endif
 }
 
 void AlpineApkUpdater::proceed()
@@ -272,7 +274,7 @@ void AlpineApkUpdater::handleKAuthUpgradeHelperReply(KJob *job)
     if (reply->error() == 0) {
         QVariant pkgsV = replyData.value(QLatin1String("changes"));
         bool onlySimulate = replyData.value(QLatin1String("onlySimulate"), false).toBool();
-        qCDebug(LOG_ALPINEAPK) << "KAuth helper upgrade reply received:" << onlySimulate;
+        qCDebug(LOG_ALPINEAPK) << "KAuth helper upgrade reply received, onlySimulate:" << onlySimulate;
         if (onlySimulate) {
             QVector<QtApk::Package> pkgVector = pkgsV.value<QVector<QtApk::Package>>();
             qCDebug(LOG_ALPINEAPK) << "  num changes:" << pkgVector.size();
-- 
GitLab


From 1f52fac3d8a1331d68711716ba12f3e2ee93411a Mon Sep 17 00:00:00 2001
From: Alexey Min <alexey.min@gmail.com>
Date: Thu, 11 Jun 2020 22:39:09 +0300
Subject: [PATCH 20/59] Adapt to changes in libapk-qt 0.3

* Rename link target to ApkQt::ApkQt
* rename header include <QtApk.h> => <QtApk>
---
 .../backends/AlpineApkBackend/AlpineApkAuthHelper.cpp     | 8 ++++----
 .../backends/AlpineApkBackend/AlpineApkAuthHelper.h       | 2 +-
 .../backends/AlpineApkBackend/AlpineApkBackend.cpp        | 2 +-
 libdiscover/backends/AlpineApkBackend/AlpineApkBackend.h  | 2 +-
 .../backends/AlpineApkBackend/AlpineApkSourcesBackend.cpp | 2 +-
 .../backends/AlpineApkBackend/AlpineApkUpdater.cpp        | 6 +++---
 libdiscover/backends/AlpineApkBackend/CMakeLists.txt      | 4 ++--
 7 files changed, 13 insertions(+), 13 deletions(-)

diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp
index 11e267866..0496c8f28 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp
@@ -73,7 +73,7 @@ ActionReply AlpineApkAuthHelper::update(const QVariantMap &args)
 
     HelperSupport::progressStep(10);
 
-    if (!m_apkdb.open(QtApk::Database::QTAPK_OPENF_READWRITE)) {
+    if (!m_apkdb.open(QtApk::QTAPK_OPENF_READWRITE)) {
         reply.setErrorDescription(QStringLiteral("Failed to open database!"));
         return reply;
     }
@@ -119,15 +119,15 @@ ActionReply AlpineApkAuthHelper::upgrade(const QVariantMap &args)
 
     HelperSupport::progressStep(10);
 
-    if (!m_apkdb.open(QtApk::Database::QTAPK_OPENF_READWRITE)) {
+    if (!m_apkdb.open(QtApk::QTAPK_OPENF_READWRITE)) {
         reply.setErrorDescription(QStringLiteral("Failed to open database!"));
         return reply;
     }
 
     bool onlySimulate = args.value(QLatin1String("onlySimulate"), false).toBool();
-    QtApk::Database::DbUpgradeFlags flags = QtApk::Database::QTAPK_UPGRADE_DEFAULT;
+    QtApk::DbUpgradeFlags flags = QtApk::QTAPK_UPGRADE_DEFAULT;
     if (onlySimulate) {
-        flags = QtApk::Database::QTAPK_UPGRADE_SIMULATE;
+        flags = QtApk::QTAPK_UPGRADE_SIMULATE;
         qCDebug(LOG_AUTHHELPER) << "Simulating upgrade run.";
     }
 
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.h b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.h
index 5c06e5570..a634ce233 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.h
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.h
@@ -22,7 +22,7 @@
 #include <QVariant>
 #include <KAuthActionReply>
 
-#include <QtApk.h>
+#include <QtApk>
 
 using namespace KAuth;
 
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp
index 750d2820c..a089afea4 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp
@@ -70,7 +70,7 @@ AlpineApkBackend::AlpineApkBackend(QObject *parent)
 
     qCDebug(LOG_ALPINEAPK) << "backend: populating resources...";
 
-    if (m_apkdb.open(QtApk::Database::QTAPK_OPENF_READONLY)) {
+    if (m_apkdb.open(QtApk::QTAPK_OPENF_READONLY)) {
         m_availablePackages = m_apkdb.getAvailablePackages();
         m_installedPackages = m_apkdb.getInstalledPackages();
         m_apkdb.close();
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.h b/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.h
index d69352575..755cf6a5a 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.h
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.h
@@ -24,7 +24,7 @@
 #include <resources/AbstractResourcesBackend.h>
 #include <QVariantList>
 
-#include <QtApk.h>
+#include <QtApk>
 
 class AlpineApkReviewsBackend;
 class AlpineApkUpdater;
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkSourcesBackend.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkSourcesBackend.cpp
index b964b40d1..225fb4436 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkSourcesBackend.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkSourcesBackend.cpp
@@ -29,7 +29,7 @@
 #include <KLocalizedString>
 
 // libapk-qt
-#include <QtApk.h>
+#include <QtApk>
 
 AlpineApkSourcesBackend::AlpineApkSourcesBackend(AbstractResourcesBackend *parent)
     : AbstractSourcesBackend(parent)
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp
index 89a816c44..0083f340d 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp
@@ -28,7 +28,7 @@
 #include <kauth_version.h>
 #include <KLocalizedString>
 
-#include <QtApk.h>
+#include <QtApk>
 
 
 AlpineApkUpdater::AlpineApkUpdater(AbstractResourcesBackend *parent)
@@ -49,12 +49,12 @@ void AlpineApkUpdater::prepare()
     }
 
     // readonly is fine for a simulation of upgrade
-    if (!db->open(QtApk::Database::QTAPK_OPENF_READONLY)) {
+    if (!db->open(QtApk::QTAPK_OPENF_READONLY)) {
         emit passiveMessage(i18n("Failed to open APK database!"));
         return;
     }
 
-    if (!db->upgrade(QtApk::Database::QTAPK_UPGRADE_SIMULATE, &m_upgradeable)) {
+    if (!db->upgrade(QtApk::QTAPK_UPGRADE_SIMULATE, &m_upgradeable)) {
         emit passiveMessage(i18n("Failed to get a list of packages to upgrade!"));
         db->close();
         return;
diff --git a/libdiscover/backends/AlpineApkBackend/CMakeLists.txt b/libdiscover/backends/AlpineApkBackend/CMakeLists.txt
index 42e09c5b6..319c7ad2f 100644
--- a/libdiscover/backends/AlpineApkBackend/CMakeLists.txt
+++ b/libdiscover/backends/AlpineApkBackend/CMakeLists.txt
@@ -38,7 +38,7 @@ target_link_libraries(
         KF5::ConfigCore
         KF5::AuthCore
         Discover::Common
-        ApkQt::apk-qt
+        ApkQt::ApkQt
 )
 
 # KAuth helper exe
@@ -54,7 +54,7 @@ set_source_files_properties(
 target_link_libraries(alpineapk_kauth_helper
     Qt5::Core
     KF5::AuthCore
-    ApkQt::apk-qt
+    ApkQt::ApkQt
 )
 
 kauth_install_actions(org.kde.discover.alpineapkbackend org.kde.discover.alpineapkbackend.actions)
-- 
GitLab


From cd1f0c1520264a4f840b1cf57d6701d562850bb6 Mon Sep 17 00:00:00 2001
From: Alexey Min <alexey.min@gmail.com>
Date: Sun, 28 Jun 2020 03:38:15 +0300
Subject: [PATCH 21/59] AlpineApkAuthHelper: implement all operations in async
 mode

also delete useless test() action
---
 .../AlpineApkBackend/AlpineApkAuthHelper.cpp  | 234 ++++++++++++------
 .../AlpineApkBackend/AlpineApkAuthHelper.h    |  19 +-
 .../org.kde.discover.alpineapkbackend.actions |   6 -
 3 files changed, 181 insertions(+), 78 deletions(-)

diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp
index 0496c8f28..a3236011f 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp
@@ -40,88 +40,194 @@ using namespace KAuth;
 
 AlpineApkAuthHelper::AlpineApkAuthHelper() {}
 
-ActionReply AlpineApkAuthHelper::test(const QVariantMap &args)
+bool AlpineApkAuthHelper::openDatabase(const QVariantMap &args, bool readwrite)
 {
-    const QString txt = args[QStringLiteral("txt")].toString();
-
-    ActionReply reply = ActionReply::HelperErrorReply();
-    QByteArray replyData(QByteArrayLiteral("ok"));
+    // maybe set fakeRoot (needs to be done before Database::open()
+    const QString fakeRoot = args.value(QLatin1String("fakeRoot"), QString()).toString();
+    if (!fakeRoot.isEmpty()) {
+        m_apkdb.setFakeRoot(fakeRoot);
+    }
 
-    // write some text file at the root directory as root, why not
-    QFile f(QStringLiteral("/lol.txt"));
-    if (f.open(QIODevice::ReadWrite | QIODevice::Text)) {
-        f.write(txt.toUtf8());
-        f.close();
+    // calculate flags to use during open
+    QtApk::DbOpenFlags fl = QtApk::QTAPK_OPENF_ENABLE_PROGRESSFD;
+    if (readwrite) {
+        fl |= QtApk::QTAPK_OPENF_READWRITE;
+    }
 
-        reply = ActionReply::SuccessReply();
-        reply.setData({
-            { QStringLiteral("reply"), replyData },
-        });
+    if (!m_apkdb.open(fl)) {
+        return false;
     }
+    return true;
+}
 
-    return reply;
+void AlpineApkAuthHelper::closeDatabase()
+{
+    m_apkdb.close();
 }
 
-ActionReply AlpineApkAuthHelper::update(const QVariantMap &args)
+void AlpineApkAuthHelper::setupTransactionPostCreate(QtApk::Transaction *trans)
 {
-    ActionReply reply = ActionReply::HelperErrorReply();
+    // receive progress notifications
+    QObject::connect(trans, &QtApk::Transaction::progressChanged,
+                     this, &AlpineApkAuthHelper::reportProgress);
 
-    const QString fakeRoot = args.value(QLatin1String("fakeRoot"), QString()).toString();
-    if (!fakeRoot.isEmpty()) {
-        m_apkdb.setFakeRoot(fakeRoot);
-    }
+    // receive error messages
+    QObject::connect(trans, &QtApk::Transaction::errorOccured,
+                     this, &AlpineApkAuthHelper::onTransactionError);
 
-    HelperSupport::progressStep(10);
+    // what to do when transaction is complete
+    QObject::connect(trans, &QtApk::Transaction::finished,
+                     this, &AlpineApkAuthHelper::onTransactionFinished);
+}
 
-    if (!m_apkdb.open(QtApk::QTAPK_OPENF_READWRITE)) {
-        reply.setErrorDescription(QStringLiteral("Failed to open database!"));
-        return reply;
+void AlpineApkAuthHelper::reportProgress(float percent)
+{
+    int p = static_cast<int>(percent);
+    if (p < 0) p = 0;
+    if (p > 100) p = 100;
+    HelperSupport::progressStep(p);
+}
+
+void AlpineApkAuthHelper::onTransactionError(const QString &msg)
+{
+    qCWarning(LOG_AUTHHELPER).nospace() << "ERROR occured in transaction \""
+                                        << m_currentTransaction->desc()
+                                        << "\": " << msg;
+    const QString errMsg = m_currentTransaction->desc() + QLatin1String(" failed: ") + msg;
+    m_actionReply.setErrorDescription(errMsg);
+    m_actionReply.setData({
+        { QLatin1String("errorString"), errMsg }
+    });
+    m_trans_ok = false;
+}
+
+void AlpineApkAuthHelper::onTransactionFinished()
+{
+    m_lastChangeset = m_currentTransaction->changeset();
+    m_currentTransaction->deleteLater();
+    m_currentTransaction = nullptr;
+    m_loop.quit();
+}
+
+ActionReply AlpineApkAuthHelper::update(const QVariantMap &args)
+{
+    // return error by default
+    m_actionReply = ActionReply::HelperErrorReply();
+
+    HelperSupport::progressStep(0);
+
+    if (!openDatabase(args)) {
+        m_actionReply.setErrorDescription(QStringLiteral("Failed to open database!"));
+        return m_actionReply;
     }
 
-    bool update_ok = m_apkdb.updatePackageIndex();
+    m_trans_ok = true;
+    QtApk::Transaction *trans = m_apkdb.updatePackageIndex();
+    setupTransactionPostCreate(trans);
 
-    if (update_ok) {
+    trans->start();
+    m_loop.exec();
+
+    if (m_trans_ok) {
         int updatesCount = m_apkdb.upgradeablePackagesCount();
-        reply = ActionReply::SuccessReply();
-        reply.setData({
+        m_actionReply = ActionReply::SuccessReply();
+        m_actionReply.setData({
             { QLatin1String("updatesCount"), updatesCount }
         });
-    } else {
-        reply.setErrorDescription(QStringLiteral("Repo update failed!"));
-        reply.setData({
-            { QLatin1String("errorString"), QStringLiteral("Repo update failed!") }
-        });
     }
 
-    m_apkdb.close();
+    closeDatabase();
     HelperSupport::progressStep(100);
 
-    return reply;
+    return m_actionReply;
 }
 
 ActionReply AlpineApkAuthHelper::add(const QVariantMap &args)
 {
-    Q_UNUSED(args)
-    ActionReply reply = ActionReply::HelperErrorReply();
-    return reply;
+    // return error by default
+    m_actionReply = ActionReply::HelperErrorReply();
+
+    HelperSupport::progressStep(0);
+
+    if (!openDatabase(args)) {
+        m_actionReply.setErrorDescription(QStringLiteral("Failed to open database!"));
+        return m_actionReply;
+    }
+
+    const QString pkgName = args.value(QLatin1String("pkgName"), QString()).toString();
+    if (pkgName.isEmpty()) {
+        m_actionReply.setErrorDescription(QStringLiteral("Specify pkgName for adding!"));
+        return m_actionReply;
+    }
+
+    m_trans_ok = true;
+    QtApk::Transaction *trans = m_apkdb.add(pkgName);
+    setupTransactionPostCreate(trans);
+
+    trans->start();
+    m_loop.exec();
+
+    if (m_trans_ok) {
+        m_actionReply = ActionReply::SuccessReply();
+    }
+
+    closeDatabase();
+    HelperSupport::progressStep(100);
+
+    return m_actionReply;
 }
 
 ActionReply AlpineApkAuthHelper::del(const QVariantMap &args)
 {
-    Q_UNUSED(args)
-    ActionReply reply = ActionReply::HelperErrorReply();
-    return reply;
+    // return error by default
+    m_actionReply = ActionReply::HelperErrorReply();
+
+    HelperSupport::progressStep(0);
+
+    if (!openDatabase(args)) {
+        m_actionReply.setErrorDescription(QStringLiteral("Failed to open database!"));
+        return m_actionReply;
+    }
+
+    const QString pkgName = args.value(QLatin1String("pkgName"), QString()).toString();
+    if (pkgName.isEmpty()) {
+        m_actionReply.setErrorDescription(QStringLiteral("Specify pkgName for removing!"));
+        return m_actionReply;
+    }
+
+    const bool delRdepends = args.value(QLatin1String("delRdepends"), false).toBool();
+
+    QtApk::DbDelFlags delFlags = QtApk::QTAPK_DEL_DEFAULT;
+    if (delRdepends) {
+        delFlags = QtApk::QTAPK_DEL_RDEPENDS;
+    }
+
+    m_trans_ok = true;
+    QtApk::Transaction *trans = m_apkdb.del(pkgName, delFlags);
+    setupTransactionPostCreate(trans);
+
+    trans->start();
+    m_loop.exec();
+
+    if (m_trans_ok) {
+        m_actionReply = ActionReply::SuccessReply();
+    }
+
+    closeDatabase();
+    HelperSupport::progressStep(100);
+
+    return m_actionReply;
 }
 
 ActionReply AlpineApkAuthHelper::upgrade(const QVariantMap &args)
 {
-    ActionReply reply = ActionReply::HelperErrorReply();
+    m_actionReply = ActionReply::HelperErrorReply();
 
-    HelperSupport::progressStep(10);
+    HelperSupport::progressStep(0);
 
-    if (!m_apkdb.open(QtApk::QTAPK_OPENF_READWRITE)) {
-        reply.setErrorDescription(QStringLiteral("Failed to open database!"));
-        return reply;
+    if (!openDatabase(args)) {
+        m_actionReply.setErrorDescription(QStringLiteral("Failed to open database!"));
+        return m_actionReply;
     }
 
     bool onlySimulate = args.value(QLatin1String("onlySimulate"), false).toBool();
@@ -131,28 +237,18 @@ ActionReply AlpineApkAuthHelper::upgrade(const QVariantMap &args)
         qCDebug(LOG_AUTHHELPER) << "Simulating upgrade run.";
     }
 
-    const QString fakeRoot = args.value(QLatin1String("fakeRoot"), QString()).toString();
-    if (!fakeRoot.isEmpty()) {
-        m_apkdb.setFakeRoot(fakeRoot);
-    }
-
-    // no progress notifications for now
-    //int progress_fd = m_apkdb.progressFd();
-    //qCDebug(LOG_AUTHHELPER) << "    progress_fd: " << progress_fd;
+    m_trans_ok = true;
 
-    //QScopedPointer<QSocketNotifier> notifier(new QSocketNotifier(progress_fd, QSocketNotifier::Read));
-    //QObject::connect(notifier.data(), &QSocketNotifier::activated, notifier.data(), [](int sock) {
-    //    Q_UNUSED(sock)
-    //    qCDebug(LOG_AUTHHELPER) << "        read trigger from progress_fd!";
-    //});
+    QtApk::Transaction *trans = m_apkdb.upgrade(flags);
+    setupTransactionPostCreate(trans);
 
-    QtApk::Changeset changes;
-    bool upgrade_ok = m_apkdb.upgrade(flags, &changes);
+    trans->start();
+    m_loop.exec();
 
-    if (upgrade_ok) {
-        reply = ActionReply::SuccessReply();
+    if (m_trans_ok) {
+        m_actionReply = ActionReply::SuccessReply();
         QVariantMap replyData;
-        const QVector<QtApk::ChangesetItem> ch = changes.changes();
+        const QVector<QtApk::ChangesetItem> ch = m_lastChangeset.changes();
         QVector<QVariant> chVector;
         QVector<QtApk::Package> pkgVector;
         for (const QtApk::ChangesetItem &it: ch) {
@@ -160,15 +256,13 @@ ActionReply AlpineApkAuthHelper::upgrade(const QVariantMap &args)
         }
         replyData.insert(QLatin1String("changes"), QVariant::fromValue(pkgVector));
         replyData.insert(QLatin1String("onlySimulate"), onlySimulate);
-        reply.setData(replyData);
-    } else {
-        reply.setErrorDescription(QStringLiteral("Repo upgrade failed!"));
+        m_actionReply.setData(replyData);
     }
 
-    m_apkdb.close();
+    closeDatabase();
     HelperSupport::progressStep(100);
 
-    return reply;
+    return m_actionReply;
 }
 
 KAUTH_HELPER_MAIN("org.kde.discover.alpineapkbackend", AlpineApkAuthHelper)
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.h b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.h
index a634ce233..a670a2aa1 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.h
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.h
@@ -18,6 +18,7 @@
  *   along with this program.  If not, see <http://www.gnu.org/licenses/>. *
  ***************************************************************************/
 
+#include <QEventLoop>
 #include <QObject>
 #include <QVariant>
 #include <KAuthActionReply>
@@ -33,12 +34,26 @@ public:
     AlpineApkAuthHelper();
 
 public Q_SLOTS:
-    ActionReply test(const QVariantMap &args);
     ActionReply update(const QVariantMap &args);
     ActionReply add(const QVariantMap &args);
     ActionReply del(const QVariantMap &args);
     ActionReply upgrade(const QVariantMap &args);
 
+protected:
+    bool openDatabase(const QVariantMap &args, bool readwrite = true);
+    void closeDatabase();
+    void setupTransactionPostCreate(QtApk::Transaction *trans);
+
+protected Q_SLOTS:
+    void reportProgress(float percent);
+    void onTransactionError(const QString &msg);
+    void onTransactionFinished();
+
 private:
-    QtApk::Database m_apkdb;
+    QtApk::DatabaseAsync m_apkdb; // runs transactions in bg thread
+    QtApk::Transaction *m_currentTransaction = nullptr;
+    QEventLoop m_loop; // event loop that will run and wait while bg transaction is in progress
+    ActionReply m_actionReply; // return value for main action slots
+    bool m_trans_ok = true; // flag to indicate if bg transaction was successful
+    QtApk::Changeset m_lastChangeset; // changeset from last completed transaction
 };
diff --git a/libdiscover/backends/AlpineApkBackend/org.kde.discover.alpineapkbackend.actions b/libdiscover/backends/AlpineApkBackend/org.kde.discover.alpineapkbackend.actions
index 10305a1af..0755c4157 100644
--- a/libdiscover/backends/AlpineApkBackend/org.kde.discover.alpineapkbackend.actions
+++ b/libdiscover/backends/AlpineApkBackend/org.kde.discover.alpineapkbackend.actions
@@ -1,9 +1,3 @@
-[org.kde.discover.alpineapkbackend.test]
-Name=Test Action
-Description=Just test
-Policy=auth_admin
-Persistence=session
-
 [org.kde.discover.alpineapkbackend.update]
 Name=Update repository index
 Description=Updates available packages list from repositories
-- 
GitLab


From 4433330e24e7fd16a3ed149f89acd614f29673aa Mon Sep 17 00:00:00 2001
From: Alexey Min <alexey.min@gmail.com>
Date: Mon, 29 Jun 2020 03:22:42 +0300
Subject: [PATCH 22/59] AlpineApkAuthHelper: add method to write repositories
 config

---
 .../AlpineApkBackend/AlpineApkAuthHelper.cpp  | 21 +++++++++++++++++++
 .../AlpineApkBackend/AlpineApkAuthHelper.h    |  1 +
 .../org.kde.discover.alpineapkbackend.actions |  6 ++++++
 3 files changed, 28 insertions(+)

diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp
index a3236011f..81c18255c 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp
@@ -265,4 +265,25 @@ ActionReply AlpineApkAuthHelper::upgrade(const QVariantMap &args)
     return m_actionReply;
 }
 
+ActionReply AlpineApkAuthHelper::repoconfig(const QVariantMap &args)
+{
+    m_actionReply = ActionReply::HelperErrorReply();
+    HelperSupport::progressStep(10);
+
+    if (args.contains(QLatin1String("repoList"))) {
+        const QVariant v = args.value(QLatin1String("repoList"));
+        const QVector<QtApk::Repository> repoVec = v.value<QVector<QtApk::Repository>>();
+        if (QtApk::Database::saveRepositories(repoVec)) {
+            m_actionReply = ActionReply::SuccessReply(); // OK
+        } else {
+            m_actionReply.setErrorDescription(QStringLiteral("Failed to write repositories config!"));
+        }
+    } else {
+        m_actionReply.setErrorDescription(QStringLiteral("repoList parameter is missing in request!"));
+    }
+
+    HelperSupport::progressStep(100);
+    return m_actionReply;
+}
+
 KAUTH_HELPER_MAIN("org.kde.discover.alpineapkbackend", AlpineApkAuthHelper)
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.h b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.h
index a670a2aa1..acce56488 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.h
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.h
@@ -38,6 +38,7 @@ public Q_SLOTS:
     ActionReply add(const QVariantMap &args);
     ActionReply del(const QVariantMap &args);
     ActionReply upgrade(const QVariantMap &args);
+    ActionReply repoconfig(const QVariantMap &args);
 
 protected:
     bool openDatabase(const QVariantMap &args, bool readwrite = true);
diff --git a/libdiscover/backends/AlpineApkBackend/org.kde.discover.alpineapkbackend.actions b/libdiscover/backends/AlpineApkBackend/org.kde.discover.alpineapkbackend.actions
index 0755c4157..f1bffe654 100644
--- a/libdiscover/backends/AlpineApkBackend/org.kde.discover.alpineapkbackend.actions
+++ b/libdiscover/backends/AlpineApkBackend/org.kde.discover.alpineapkbackend.actions
@@ -21,3 +21,9 @@ Name=Remove package
 Description=Uninstall one package
 Policy=auth_admin
 Persistence=session
+
+[org.kde.discover.alpineapkbackend.repoconfig]
+Name=Remove package
+Description=Configure repositories URLs
+Policy=auth_admin
+Persistence=session
-- 
GitLab


From c9b46d8102e35a2e51f8b4d02badb78a06371619 Mon Sep 17 00:00:00 2001
From: Alexey Min <alexey.min@gmail.com>
Date: Mon, 29 Jun 2020 06:52:42 +0300
Subject: [PATCH 23/59] AlpineApkAuthHelper: create event loop only when needed

It fixes error "QEventLoop: requires QApplication",
this makes it creation delayed to moment *after*
QCoreApplication object is created by KAuthCore
internals.
---
 .../AlpineApkBackend/AlpineApkAuthHelper.cpp       | 14 +++++++++-----
 .../AlpineApkBackend/AlpineApkAuthHelper.h         |  2 +-
 2 files changed, 10 insertions(+), 6 deletions(-)

diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp
index 81c18255c..b436aa73b 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp
@@ -78,6 +78,10 @@ void AlpineApkAuthHelper::setupTransactionPostCreate(QtApk::Transaction *trans)
     // what to do when transaction is complete
     QObject::connect(trans, &QtApk::Transaction::finished,
                      this, &AlpineApkAuthHelper::onTransactionFinished);
+
+    if (!m_loop) {
+        m_loop = new QEventLoop(this);
+    }
 }
 
 void AlpineApkAuthHelper::reportProgress(float percent)
@@ -106,7 +110,7 @@ void AlpineApkAuthHelper::onTransactionFinished()
     m_lastChangeset = m_currentTransaction->changeset();
     m_currentTransaction->deleteLater();
     m_currentTransaction = nullptr;
-    m_loop.quit();
+    m_loop->quit();
 }
 
 ActionReply AlpineApkAuthHelper::update(const QVariantMap &args)
@@ -126,7 +130,7 @@ ActionReply AlpineApkAuthHelper::update(const QVariantMap &args)
     setupTransactionPostCreate(trans);
 
     trans->start();
-    m_loop.exec();
+    m_loop->exec();
 
     if (m_trans_ok) {
         int updatesCount = m_apkdb.upgradeablePackagesCount();
@@ -165,7 +169,7 @@ ActionReply AlpineApkAuthHelper::add(const QVariantMap &args)
     setupTransactionPostCreate(trans);
 
     trans->start();
-    m_loop.exec();
+    m_loop->exec();
 
     if (m_trans_ok) {
         m_actionReply = ActionReply::SuccessReply();
@@ -207,7 +211,7 @@ ActionReply AlpineApkAuthHelper::del(const QVariantMap &args)
     setupTransactionPostCreate(trans);
 
     trans->start();
-    m_loop.exec();
+    m_loop->exec();
 
     if (m_trans_ok) {
         m_actionReply = ActionReply::SuccessReply();
@@ -243,7 +247,7 @@ ActionReply AlpineApkAuthHelper::upgrade(const QVariantMap &args)
     setupTransactionPostCreate(trans);
 
     trans->start();
-    m_loop.exec();
+    m_loop->exec();
 
     if (m_trans_ok) {
         m_actionReply = ActionReply::SuccessReply();
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.h b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.h
index acce56488..eed39f283 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.h
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.h
@@ -53,7 +53,7 @@ protected Q_SLOTS:
 private:
     QtApk::DatabaseAsync m_apkdb; // runs transactions in bg thread
     QtApk::Transaction *m_currentTransaction = nullptr;
-    QEventLoop m_loop; // event loop that will run and wait while bg transaction is in progress
+    QEventLoop *m_loop = nullptr; // event loop that will run and wait while bg transaction is in progress
     ActionReply m_actionReply; // return value for main action slots
     bool m_trans_ok = true; // flag to indicate if bg transaction was successful
     QtApk::Changeset m_lastChangeset; // changeset from last completed transaction
-- 
GitLab


From 2b3ac09639387f5cb550676d0b62ceeffb1761cf Mon Sep 17 00:00:00 2001
From: Alexey Min <alexey.min@gmail.com>
Date: Mon, 29 Jun 2020 06:55:59 +0300
Subject: [PATCH 24/59] AlpineApkAuthHelper: fix nullptr deref in
 onTransactionFinished

m_currentTransaction was never assigned
---
 libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp
index b436aa73b..f5d8e17e5 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp
@@ -67,6 +67,8 @@ void AlpineApkAuthHelper::closeDatabase()
 
 void AlpineApkAuthHelper::setupTransactionPostCreate(QtApk::Transaction *trans)
 {
+    m_currentTransaction = trans; // remember current transaction here
+
     // receive progress notifications
     QObject::connect(trans, &QtApk::Transaction::progressChanged,
                      this, &AlpineApkAuthHelper::reportProgress);
-- 
GitLab


From 68ea166cdfce3d3fe4a93c8e2eba10ae2c6dff77 Mon Sep 17 00:00:00 2001
From: Alexey Min <alexey.min@gmail.com>
Date: Tue, 30 Jun 2020 05:39:42 +0300
Subject: [PATCH 25/59] AlpineApkSourcesBackend: fully functional sources
 editor

* allows enable/disable repo
* allows to add/remove repo
* allows to change sources order
* allows to reload/save changes
---
 .../AlpineApkSourcesBackend.cpp               | 130 +++++++++++++-----
 .../AlpineApkSourcesBackend.h                 |   7 +
 2 files changed, 102 insertions(+), 35 deletions(-)

diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkSourcesBackend.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkSourcesBackend.cpp
index 225fb4436..28f08ef86 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkSourcesBackend.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkSourcesBackend.cpp
@@ -26,6 +26,8 @@
 #include <QVector>
 
 // KF5
+#include <KAuthExecuteJob>
+#include <kauth_version.h>
 #include <KLocalizedString>
 
 // libapk-qt
@@ -35,16 +37,19 @@ AlpineApkSourcesBackend::AlpineApkSourcesBackend(AbstractResourcesBackend *paren
     : AbstractSourcesBackend(parent)
     , m_sourcesModel(new QStandardItemModel(this))
     , m_refreshAction(new QAction(QIcon::fromTheme(QStringLiteral("view-refresh")),
-                                  QStringLiteral("Refresh"), this))
+                                  QStringLiteral("Reload"), this))
+    , m_saveAction(new QAction(QIcon::fromTheme(QStringLiteral("document-save")),
+                                  QStringLiteral("Save"), this))
+    // ^^ unfortunately QML side ignores icons for custom actions
 {
     loadSources();
     QObject::connect(m_refreshAction, &QAction::triggered,
                      this, &AlpineApkSourcesBackend::loadSources);
-
-    // can be used to track enabling/disabling repo source
-    // QObject::connect(m_sourcesModel, &QStandardItemModel::itemChanged, this, [](QStandardItem* item) {
-    //     qCDebug(LOG_ALPINEAPK) << "source backend: DummySource changed" << item << item->checkState();
-    // });
+    QObject::connect(m_saveAction, &QAction::triggered,
+                     this, &AlpineApkSourcesBackend::saveSources);
+    // track enabling/disabling repo source
+    QObject::connect(m_sourcesModel, &QStandardItemModel::itemChanged,
+                     this, &AlpineApkSourcesBackend::onItemChanged);
 }
 
 QAbstractItemModel *AlpineApkSourcesBackend::sources()
@@ -52,48 +57,97 @@ QAbstractItemModel *AlpineApkSourcesBackend::sources()
     return m_sourcesModel;
 }
 
+QStandardItem *AlpineApkSourcesBackend::sourceForId(const QString& id) const
+{
+    for (int i = 0; i < m_sourcesModel->rowCount(); ++i) {
+        QStandardItem *item = m_sourcesModel->item(i, 0);
+        if (item->data(AbstractSourcesBackend::IdRole) == id) {
+            return item;
+        }
+    }
+    return nullptr;
+}
+
 bool AlpineApkSourcesBackend::addSource(const QString &id)
 {
-    return addSourceFull(id, QString(), true);
+    m_repos.append(QtApk::Repository(id, QString(), true));
+    fillModelFromRepos();
+    return true;
 }
 
-QStandardItem *AlpineApkSourcesBackend::sourceForId(const QString& id) const
+void AlpineApkSourcesBackend::loadSources()
+{
+    m_repos = QtApk::Database::getRepositories();
+    fillModelFromRepos();
+}
+
+void AlpineApkSourcesBackend::fillModelFromRepos()
 {
-    for (int i = 0, c = m_sourcesModel->rowCount(); i < c; ++i) {
-        QStandardItem *it = m_sourcesModel->item(i, 0);
-        if (it->text() == id) {
-            return it;
+    m_sourcesModel->clear();
+    for (const QtApk::Repository &repo: m_repos) {
+        if (repo.url.isEmpty()) {
+            continue;
         }
+        qCDebug(LOG_ALPINEAPK) << "source backend: Adding source:" << repo.url << repo.enabled;
+        QStandardItem *it = new QStandardItem(repo.url);
+        it->setData(repo.url, AbstractSourcesBackend::IdRole);
+        it->setData(repo.comment, Qt::ToolTipRole);
+        it->setCheckable(true);
+        it->setCheckState(repo.enabled ? Qt::Checked : Qt::Unchecked);
+        m_sourcesModel->appendRow(it);
     }
-    return nullptr;
 }
 
-bool AlpineApkSourcesBackend::addSourceFull(const QString &id, const QString &comment, bool enabled)
+void AlpineApkSourcesBackend::saveSources()
 {
-    if (id.isEmpty()) {
-        return false;
+    KAuth::Action repoConfigAction(QStringLiteral("org.kde.discover.alpineapkbackend.repoconfig"));
+    repoConfigAction.setHelperId(QStringLiteral("org.kde.discover.alpineapkbackend"));
+    if (!repoConfigAction.isValid()) {
+        qCWarning(LOG_ALPINEAPK) << "repoConfigAction is not valid!";
+        return;
     }
 
-    qCDebug(LOG_ALPINEAPK) << "source backend: Adding source:" << id;
+    repoConfigAction.setTimeout(1 * 60 * 1000); // 1 min
+#if KAUTH_VERSION < QT_VERSION_CHECK(5, 68, 0)
+    upgradeAction.setDetails(i18n("Configure repositories URLs"));
+#else
+    static const KAuth::Action::DetailsMap details{
+        { KAuth::Action::AuthDetail::DetailMessage, i18n("Configure repositories URLs") }
+    };
+    repoConfigAction.setDetailsV2(details);
+#endif
+    // pass in new repositories list
+    repoConfigAction.addArgument(QLatin1String("repoList"),
+                                 QVariant::fromValue<QVector<QtApk::Repository>>(m_repos));
+
+    // run with elevated privileges
+    KAuth::ExecuteJob *reply = repoConfigAction.execute();
+    QObject::connect(reply, &KAuth::ExecuteJob::result, this, [this] (KJob *job) {
+        KAuth::ExecuteJob *reply = static_cast<KAuth::ExecuteJob *>(job);
+        if (reply->error() != 0) {
+            const QString errMessage = reply->errorString();
+            qCWarning(LOG_ALPINEAPK) << "KAuth helper returned error:"
+                                     << reply->error() << errMessage;
+            if (reply->error() == KAuth::ActionReply::Error::AuthorizationDeniedError) {
+                Q_EMIT passiveMessage(i18n("Authorization denied"));
+            } else {
+                Q_EMIT passiveMessage(i18n("Error: ") + errMessage);
+            }
+        }
+        this->loadSources();
+    });
 
-    QStandardItem *it = new QStandardItem(id);
-    it->setData(id, AbstractSourcesBackend::IdRole);
-    it->setData(comment, Qt::ToolTipRole);
-    it->setCheckable(true);
-    it->setCheckState(enabled ? Qt::Checked : Qt::Unchecked);
-    // for now, disable editing sources
-    it->setFlags(it->flags() & ~Qt::ItemIsEnabled);
-    m_sourcesModel->appendRow(it);
-    return true;
+    reply->start();
 }
 
-void AlpineApkSourcesBackend::loadSources()
+void AlpineApkSourcesBackend::onItemChanged(QStandardItem *item)
 {
-    QVector<QtApk::Repository> repos = QtApk::Database::getRepositories();
-    m_sourcesModel->clear();
-    for (const QtApk::Repository &repo: repos) {
-        addSourceFull(repo.url, repo.comment, repo.enabled);
-    }
+    // update internal storage vector and relaod model from it
+    //    otherwise checks state are not updated in UI
+    const Qt::CheckState cs = item->checkState();
+    const QModelIndex idx = m_sourcesModel->indexFromItem(item);
+    m_repos[idx.row()].enabled = (cs == Qt::Checked);
+    fillModelFromRepos();
 }
 
 bool AlpineApkSourcesBackend::removeSource(const QString &id)
@@ -103,18 +157,20 @@ bool AlpineApkSourcesBackend::removeSource(const QString &id)
         qCWarning(LOG_ALPINEAPK) << "source backend: couldn't find " << id;
         return false;
     }
+    m_repos.remove(it->row());
     return m_sourcesModel->removeRow(it->row());
 }
 
 QString AlpineApkSourcesBackend::idDescription()
 {
-    return i18nc("Adding repo", "Enter apk repository URL, for example: "
+    return i18nc("Adding repo", "Enter Alpine repository URL, for example: "
                      "http://dl-cdn.alpinelinux.org/alpine/edge/testing/");
 }
 
 QVariantList AlpineApkSourcesBackend::actions() const
 {
     static const QVariantList s_actions {
+        QVariant::fromValue<QObject *>(m_saveAction),
         QVariant::fromValue<QObject *>(m_refreshAction),
     };
     return s_actions;
@@ -122,12 +178,12 @@ QVariantList AlpineApkSourcesBackend::actions() const
 
 bool AlpineApkSourcesBackend::supportsAdding() const
 {
-    return false; // for now, disable editing sources
+    return true;
 }
 
 bool AlpineApkSourcesBackend::canMoveSources() const
 {
-    return false; // for now, disable editing sources
+    return true;
 }
 
 bool AlpineApkSourcesBackend::moveSource(const QString& sourceId, int delta)
@@ -147,5 +203,9 @@ bool AlpineApkSourcesBackend::moveSource(const QString& sourceId, int delta)
             || row == (m_sourcesModel->rowCount() - 1)) {
         Q_EMIT lastSourceIdChanged();
     }
+
+    // swap also items in internal storage vector
+    m_repos.swapItemsAt(row, destRow);
+
     return true;
 }
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkSourcesBackend.h b/libdiscover/backends/AlpineApkBackend/AlpineApkSourcesBackend.h
index 578945917..eacda22dc 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkSourcesBackend.h
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkSourcesBackend.h
@@ -24,6 +24,8 @@
 #include <resources/AbstractSourcesBackend.h>
 #include <QStandardItemModel>
 
+#include <QtApkRepository.h>
+
 class AlpineApkSourcesBackend : public AbstractSourcesBackend
 {
 public:
@@ -42,9 +44,14 @@ private:
     QStandardItem *sourceForId(const QString &id) const;
     bool addSourceFull(const QString &id, const QString &comment, bool enabled);
     void loadSources();
+    void saveSources();
+    void fillModelFromRepos();
+    void onItemChanged(QStandardItem* item);
 
     QStandardItemModel *m_sourcesModel = nullptr;
     QAction *m_refreshAction = nullptr;
+    QAction *m_saveAction = nullptr;
+    QVector<QtApk::Repository> m_repos;
 };
 
 #endif // ALPINEAPKSOURCESBACKEND_H
-- 
GitLab


From 1df8286de8ac4eed649bb389d90370cc0977b103 Mon Sep 17 00:00:00 2001
From: Alexey Min <alexey.min@gmail.com>
Date: Wed, 1 Jul 2020 08:34:10 +0300
Subject: [PATCH 26/59] AlpineApkUpdater: update progressing status

---
 .../AlpineApkBackend/AlpineApkUpdater.cpp     | 25 +++++++++++--------
 .../AlpineApkBackend/AlpineApkUpdater.h       |  2 +-
 2 files changed, 16 insertions(+), 11 deletions(-)

diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp
index 0083f340d..2f3d184fb 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp
@@ -125,20 +125,18 @@ QDateTime AlpineApkUpdater::lastUpdate() const
 bool AlpineApkUpdater::isCancelable() const
 {
     qCDebug(LOG_ALPINEAPK) << Q_FUNC_INFO;
-    return true;
+    return false;
 }
 
 bool AlpineApkUpdater::isProgressing() const
 {
-    qCDebug(LOG_ALPINEAPK) << Q_FUNC_INFO;
-    return false;
+    qCDebug(LOG_ALPINEAPK) << Q_FUNC_INFO << m_progressing;
+    return m_progressing;
 }
 
 bool AlpineApkUpdater::isMarked(AbstractResource *res) const
 {
-    // qCDebug(LOG_ALPINEAPK) << Q_FUNC_INFO;
     return m_markedToUpdate.contains(res);
-    // return true;
 }
 
 void AlpineApkUpdater::fetchChangelog() const
@@ -148,7 +146,6 @@ void AlpineApkUpdater::fetchChangelog() const
 
 double AlpineApkUpdater::updateSize() const
 {
-    // qCDebug(LOG_ALPINEAPK) << Q_FUNC_INFO;
     double sum = 0.0;
     for (AbstractResource *res : m_markedToUpdate) {
         sum += res->size();
@@ -170,8 +167,6 @@ void AlpineApkUpdater::cancel()
 void AlpineApkUpdater::start()
 {
     qCDebug(LOG_ALPINEAPK) << Q_FUNC_INFO;
-    //return;
-//#if 0
     KAuth::Action upgradeAction(QStringLiteral("org.kde.discover.alpineapkbackend.upgrade"));
     upgradeAction.setHelperId(QStringLiteral("org.kde.discover.alpineapkbackend"));
 
@@ -196,8 +191,10 @@ void AlpineApkUpdater::start()
     QObject::connect(reply, &KAuth::ExecuteJob::result,
                      this, &AlpineApkUpdater::handleKAuthUpgradeHelperReply);
 
+    m_progressing = true;
+    Q_EMIT progressingChanged(m_progressing);
+
     reply->start();
-//#endif
 }
 
 void AlpineApkUpdater::proceed()
@@ -207,7 +204,7 @@ void AlpineApkUpdater::proceed()
 
 int AlpineApkUpdater::updatesCount()
 {
-    // qDebug(LOG_ALPINEAPK) << Q_FUNC_INFO << m_updatesCount;
+    qDebug(LOG_ALPINEAPK) << Q_FUNC_INFO << m_updatesCount;
     return m_updatesCount;
 }
 
@@ -241,6 +238,10 @@ void AlpineApkUpdater::startCheckForUpdates()
     QObject::connect(reply, QOverload<KJob *, unsigned long>::of(&KAuth::ExecuteJob::percent),
                      this, &AlpineApkUpdater::handleKAuthUpdateHelperProgress);
 
+    m_progressing = true;
+    Q_EMIT progressingChanged(m_progressing);
+    Q_EMIT progressChanged(0);
+
     reply->start();
 }
 
@@ -256,6 +257,9 @@ void AlpineApkUpdater::handleKAuthUpdateHelperReply(KJob *job)
         handleKAuthHelperError(reply, replyData);
     }
 
+    m_progressing = false;
+    Q_EMIT progressingChanged(m_progressing);
+
     // we are not in the state "Fetching updates" now, update UI
     Q_EMIT checkForUpdatesFinished();
 }
@@ -265,6 +269,7 @@ void AlpineApkUpdater::handleKAuthUpdateHelperProgress(KJob *job, unsigned long
     Q_UNUSED(job)
     qCDebug(LOG_ALPINEAPK) << "    fetch updates progress: " << percent;
     Q_EMIT fetchingUpdatesProgressChanged(percent);
+    Q_EMIT progressChanged(static_cast<qreal>(percent));
 }
 
 void AlpineApkUpdater::handleKAuthUpgradeHelperReply(KJob *job)
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.h b/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.h
index 77140ca25..0ee2fcb47 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.h
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.h
@@ -185,7 +185,7 @@ private:
 
 //    QSet<AbstractResource*> m_upgradeable;
 //    QSet<AbstractResource*> m_pendingResources;
-//    bool m_settingUp;
+    bool m_progressing = false;
 //    qreal m_progress;
 //    QDateTime m_lastUpdate;
 //    QTimer m_timer;
-- 
GitLab


From 4d7dba0a36362b23a25c043e0627c1f97b4e3717 Mon Sep 17 00:00:00 2001
From: Alexey Min <alexey.min@gmail.com>
Date: Wed, 1 Jul 2020 10:06:03 +0300
Subject: [PATCH 27/59] AlpineApkTransaction: transaction should know its
 backend

---
 .../AlpineApkBackend/AlpineApkTransaction.cpp | 28 +++++++++----------
 .../AlpineApkBackend/AlpineApkTransaction.h   | 27 ++++++++++--------
 2 files changed, 29 insertions(+), 26 deletions(-)

diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkTransaction.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkTransaction.cpp
index b4f90df56..fdc1d0534 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkTransaction.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkTransaction.cpp
@@ -26,7 +26,6 @@
 #include <QDebug>
 #include <KRandom>
 
-// #define TEST_PROCEED
 
 AlpineApkTransaction::AlpineApkTransaction(AlpineApkResource *app, Role role)
     : AlpineApkTransaction(app, {}, role)
@@ -36,12 +35,25 @@ AlpineApkTransaction::AlpineApkTransaction(AlpineApkResource *app, Role role)
 AlpineApkTransaction::AlpineApkTransaction(AlpineApkResource *app, const AddonList &addons, Transaction::Role role)
     : Transaction(app->backend(), app, role, addons)
     , m_app(app)
+    , m_backend(static_cast<AlpineApkBackend *>(app->backend()))
 {
-    setCancellable(true);
+    setCancellable(false);
     setStatus(DownloadingStatus);
     iterateTransaction();
 }
 
+void AlpineApkTransaction::proceed()
+{
+    finishTransaction();
+}
+
+void AlpineApkTransaction::cancel()
+{
+    m_iterate = false;
+
+    setStatus(CancelledStatus);
+}
+
 void AlpineApkTransaction::iterateTransaction()
 {
     if (!m_iterate) {
@@ -59,18 +71,6 @@ void AlpineApkTransaction::iterateTransaction()
     }
 }
 
-void AlpineApkTransaction::proceed()
-{
-    finishTransaction();
-}
-
-void AlpineApkTransaction::cancel()
-{
-    m_iterate = false;
-
-    setStatus(CancelledStatus);
-}
-
 void AlpineApkTransaction::finishTransaction()
 {
     AbstractResource::State newState;
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkTransaction.h b/libdiscover/backends/AlpineApkBackend/AlpineApkTransaction.h
index 63aeef8d7..ba0a77502 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkTransaction.h
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkTransaction.h
@@ -23,24 +23,27 @@
 
 #include <Transaction/Transaction.h>
 
+class AlpineApkBackend;
 class AlpineApkResource;
+
 class AlpineApkTransaction : public Transaction
 {
-    Q_OBJECT
-    public:
-        AlpineApkTransaction(AlpineApkResource *app, Role role);
-        AlpineApkTransaction(AlpineApkResource *app, const AddonList &list, Role role);
+Q_OBJECT
+public:
+    AlpineApkTransaction(AlpineApkResource *app, Role role);
+    AlpineApkTransaction(AlpineApkResource *app, const AddonList &list, Role role);
 
-        void cancel() override;
-        void proceed() override;
+    void cancel() override;
+    void proceed() override;
 
-    private Q_SLOTS:
-        void iterateTransaction();
-        void finishTransaction();
+private Q_SLOTS:
+    void iterateTransaction();
+    void finishTransaction();
 
-    private:
-        bool m_iterate = true;
-        AlpineApkResource *m_app;
+private:
+    bool m_iterate = true;
+    AlpineApkResource *m_app;
+    AlpineApkBackend *m_backend;
 };
 
 #endif // ALPINEAPKTRANSACTION_H
-- 
GitLab


From e1d35bd99030b4f7d1d08208c1fd3535d267d8c1 Mon Sep 17 00:00:00 2001
From: Alexey Min <alexey.min@gmail.com>
Date: Tue, 28 Jul 2020 21:05:11 +0300
Subject: [PATCH 28/59] AlpineApkBackend: style fixes

---
 .../AlpineApkBackend/AlpineApkBackend.cpp         | 15 ++++++++++-----
 1 file changed, 10 insertions(+), 5 deletions(-)

diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp
index a089afea4..b695e5e6b 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp
@@ -66,7 +66,7 @@ AlpineApkBackend::AlpineApkBackend(QObject *parent)
                      this, &AlpineApkBackend::finishCheckForUpdates);
     m_updatesTimeoutTimer->setTimerType(Qt::CoarseTimer);
     m_updatesTimeoutTimer->setSingleShot(true);
-    m_updatesTimeoutTimer->setInterval(2 * 60 * 1000); // 2minutes
+    m_updatesTimeoutTimer->setInterval(5 * 60 * 1000); // 5 minutes
 
     qCDebug(LOG_ALPINEAPK) << "backend: populating resources...";
 
@@ -87,7 +87,8 @@ AlpineApkBackend::AlpineApkBackend(QObject *parent)
             // which places it into "System updates" section
             const QString key = pkg.name.toLower();
             m_resources.insert(key, res);
-            connect(res, &AlpineApkResource::stateChanged, this, &AlpineApkBackend::updatesCountChanged);
+            connect(res, &AlpineApkResource::stateChanged,
+                    this, &AlpineApkBackend::updatesCountChanged);
         }
         qCDebug(LOG_ALPINEAPK) << "  available" << m_availablePackages.size()
                                << "packages";
@@ -215,17 +216,20 @@ AbstractReviewsBackend *AlpineApkBackend::reviewsBackend() const
 
 Transaction* AlpineApkBackend::installApplication(AbstractResource *app, const AddonList &addons)
 {
-    return new AlpineApkTransaction(qobject_cast<AlpineApkResource *>(app), addons, Transaction::InstallRole);
+    return new AlpineApkTransaction(qobject_cast<AlpineApkResource *>(app),
+                                    addons, Transaction::InstallRole);
 }
 
 Transaction* AlpineApkBackend::installApplication(AbstractResource *app)
 {
-    return new AlpineApkTransaction(qobject_cast<AlpineApkResource *>(app), Transaction::InstallRole);
+    return new AlpineApkTransaction(qobject_cast<AlpineApkResource *>(app),
+                                    Transaction::InstallRole);
 }
 
 Transaction* AlpineApkBackend::removeApplication(AbstractResource *app)
 {
-    return new AlpineApkTransaction(qobject_cast<AlpineApkResource *>(app), Transaction::RemoveRole);
+    return new AlpineApkTransaction(qobject_cast<AlpineApkResource *>(app),
+                                    Transaction::RemoveRole);
 }
 
 int AlpineApkBackend::fetchingUpdatesProgress() const
@@ -281,4 +285,5 @@ void AlpineApkBackend::setFetchingUpdatesProgress(int percent)
     emit fetchingUpdatesProgressChanged();
 }
 
+// needed because DISCOVER_BACKEND_PLUGIN(AlpineApkBackend) contains Q_OBJECT
 #include "AlpineApkBackend.moc"
-- 
GitLab


From 96d48d53831b683f31bbb502c8924b57f3d4d946 Mon Sep 17 00:00:00 2001
From: Alexey Min <alexey.min@gmail.com>
Date: Wed, 29 Jul 2020 03:48:50 +0300
Subject: [PATCH 29/59] AlpineApk KAuth backend: fix repoconfig action name

---
 .../AlpineApkBackend/org.kde.discover.alpineapkbackend.actions  | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/libdiscover/backends/AlpineApkBackend/org.kde.discover.alpineapkbackend.actions b/libdiscover/backends/AlpineApkBackend/org.kde.discover.alpineapkbackend.actions
index f1bffe654..5f7f76771 100644
--- a/libdiscover/backends/AlpineApkBackend/org.kde.discover.alpineapkbackend.actions
+++ b/libdiscover/backends/AlpineApkBackend/org.kde.discover.alpineapkbackend.actions
@@ -23,7 +23,7 @@ Policy=auth_admin
 Persistence=session
 
 [org.kde.discover.alpineapkbackend.repoconfig]
-Name=Remove package
+Name=Configure repositories
 Description=Configure repositories URLs
 Policy=auth_admin
 Persistence=session
-- 
GitLab


From f8d49b3c91584343123c440208de2e5486e03f3d Mon Sep 17 00:00:00 2001
From: Alexey Min <alexey.min@gmail.com>
Date: Wed, 29 Jul 2020 03:50:27 +0300
Subject: [PATCH 30/59] AlpineApkAuthHelper: clsoe database only on helper exit

Don't be so quick to close database after each operation, close it in
Helper class destructor, on helper exit.
---
 .../AlpineApkBackend/AlpineApkAuthHelper.cpp  | 24 ++++++++++++-------
 .../AlpineApkBackend/AlpineApkAuthHelper.h    |  1 +
 2 files changed, 16 insertions(+), 9 deletions(-)

diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp
index f5d8e17e5..e6e6cb7a1 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp
@@ -40,8 +40,18 @@ using namespace KAuth;
 
 AlpineApkAuthHelper::AlpineApkAuthHelper() {}
 
+AlpineApkAuthHelper::~AlpineApkAuthHelper()
+{
+    closeDatabase();
+}
+
 bool AlpineApkAuthHelper::openDatabase(const QVariantMap &args, bool readwrite)
 {
+    // is already opened?
+    if (m_apkdb.isOpen()) {
+        return true;
+    }
+
     // maybe set fakeRoot (needs to be done before Database::open()
     const QString fakeRoot = args.value(QLatin1String("fakeRoot"), QString()).toString();
     if (!fakeRoot.isEmpty()) {
@@ -62,7 +72,11 @@ bool AlpineApkAuthHelper::openDatabase(const QVariantMap &args, bool readwrite)
 
 void AlpineApkAuthHelper::closeDatabase()
 {
-    m_apkdb.close();
+    // close database only if opened
+    if (m_apkdb.isOpen()) {
+        // this also stops bg thread
+        m_apkdb.close();
+    }
 }
 
 void AlpineApkAuthHelper::setupTransactionPostCreate(QtApk::Transaction *trans)
@@ -142,9 +156,7 @@ ActionReply AlpineApkAuthHelper::update(const QVariantMap &args)
         });
     }
 
-    closeDatabase();
     HelperSupport::progressStep(100);
-
     return m_actionReply;
 }
 
@@ -177,9 +189,7 @@ ActionReply AlpineApkAuthHelper::add(const QVariantMap &args)
         m_actionReply = ActionReply::SuccessReply();
     }
 
-    closeDatabase();
     HelperSupport::progressStep(100);
-
     return m_actionReply;
 }
 
@@ -219,9 +229,7 @@ ActionReply AlpineApkAuthHelper::del(const QVariantMap &args)
         m_actionReply = ActionReply::SuccessReply();
     }
 
-    closeDatabase();
     HelperSupport::progressStep(100);
-
     return m_actionReply;
 }
 
@@ -265,9 +273,7 @@ ActionReply AlpineApkAuthHelper::upgrade(const QVariantMap &args)
         m_actionReply.setData(replyData);
     }
 
-    closeDatabase();
     HelperSupport::progressStep(100);
-
     return m_actionReply;
 }
 
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.h b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.h
index eed39f283..7fd69a8f3 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.h
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.h
@@ -32,6 +32,7 @@ class AlpineApkAuthHelper : public QObject
     Q_OBJECT
 public:
     AlpineApkAuthHelper();
+    ~AlpineApkAuthHelper() override;
 
 public Q_SLOTS:
     ActionReply update(const QVariantMap &args);
-- 
GitLab


From b69a7b0d4e58d959670c3d370ca7f520d28dc9e4 Mon Sep 17 00:00:00 2001
From: Alexey Min <alexey.min@gmail.com>
Date: Wed, 29 Jul 2020 03:59:10 +0300
Subject: [PATCH 31/59] AlpineApkTransaction: implement add/del packages.

---
 .../AlpineApkBackend/AlpineApkTransaction.cpp | 120 ++++++++++++++----
 .../AlpineApkBackend/AlpineApkTransaction.h   |  12 +-
 2 files changed, 101 insertions(+), 31 deletions(-)

diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkTransaction.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkTransaction.cpp
index fdc1d0534..ffd442927 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkTransaction.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkTransaction.cpp
@@ -22,56 +22,118 @@
 #include "AlpineApkBackend.h"
 #include "AlpineApkResource.h"
 #include "alpineapk_backend_logging.h"  // generated by ECM
-#include <QTimer>
+
+// Qt
 #include <QDebug>
-#include <KRandom>
+#include <QTimer>
 
+// KF5
+#include <KAuthExecuteJob>
+#include <kauth_version.h>
+#include <KLocalizedString>
 
-AlpineApkTransaction::AlpineApkTransaction(AlpineApkResource *app, Role role)
-    : AlpineApkTransaction(app, {}, role)
+AlpineApkTransaction::AlpineApkTransaction(AlpineApkResource *res, Role role)
+    : AlpineApkTransaction(res, {}, role)
 {
 }
 
-AlpineApkTransaction::AlpineApkTransaction(AlpineApkResource *app, const AddonList &addons, Transaction::Role role)
-    : Transaction(app->backend(), app, role, addons)
-    , m_app(app)
-    , m_backend(static_cast<AlpineApkBackend *>(app->backend()))
+AlpineApkTransaction::AlpineApkTransaction(AlpineApkResource *res, const AddonList &addons, Transaction::Role role)
+    : Transaction(res->backend(), res, role, addons)
+    , m_resource(res)
+    , m_backend(static_cast<AlpineApkBackend *>(res->backend()))
 {
     setCancellable(false);
-    setStatus(DownloadingStatus);
-    iterateTransaction();
+    setStatus(QueuedStatus);
+    // seems like Discover's transactions are supposed to start
+    // automatically; no dedicated method to start transaction?
+    startTransaction();
 }
 
 void AlpineApkTransaction::proceed()
 {
-    finishTransaction();
+    startTransaction();
 }
 
 void AlpineApkTransaction::cancel()
 {
-    m_iterate = false;
-
     setStatus(CancelledStatus);
 }
 
-void AlpineApkTransaction::iterateTransaction()
+void AlpineApkTransaction::startTransaction()
 {
-    if (!m_iterate) {
+    KAuth::Action authAction;
+    QString actionDescription(i18n("Install package"));
+    switch(role()) {
+    case InstallRole:
+        authAction.setName(QStringLiteral("org.kde.discover.alpineapkbackend.add"));
+        break;
+    case RemoveRole:
+        authAction.setName(QStringLiteral("org.kde.discover.alpineapkbackend.del"));
+        actionDescription = i18n("Remove package");
+        break;
+    case ChangeAddonsRole:
+        qCWarning(LOG_ALPINEAPK) << "Addons are not supported by Alpine APK Backend!";
         return;
+        break;
     }
+    authAction.setHelperId(QStringLiteral("org.kde.discover.alpineapkbackend"));
 
-    if(progress() < 100) {
-        setProgress(qBound(0, progress() + (KRandom::random() % 30), 100));
-        QTimer::singleShot(/*KRandom::random()%*/100, this, &AlpineApkTransaction::iterateTransaction);
-    } else if (status() == DownloadingStatus) {
-        setStatus(CommittingStatus);
-        QTimer::singleShot(/*KRandom::random()%*/100, this, &AlpineApkTransaction::iterateTransaction);
+    if (!authAction.isValid()) {
+        qCWarning(LOG_ALPINEAPK) << "kauth addAction is not valid!";
+        return;
+    }
+
+    authAction.setTimeout(60 * 60 * 1000); // 60 min
+#if KAUTH_VERSION < QT_VERSION_CHECK(5, 68, 0)
+    addAction.setDetails(actionDescription);
+#else
+    const KAuth::Action::DetailsMap details{
+        { KAuth::Action::AuthDetail::DetailMessage, actionDescription }
+    };
+    authAction.setDetailsV2(details);
+#endif
+    authAction.addArgument(QLatin1String("pkgName"), m_resource->m_pkg.name);
+
+    // run action with elevated privileges
+    KAuth::ExecuteJob *reply = authAction.execute();
+
+    // get result of this job
+    QObject::connect(reply, &KAuth::ExecuteJob::result, this, [this](KJob *job) {
+        KAuth::ExecuteJob *reply = static_cast<KAuth::ExecuteJob *>(job);
+        const QVariantMap &replyData = reply->data();
+        if (reply->error() == 0) {
+            finishTransactionOK();
+        } else {
+            QString message = replyData.value(QLatin1String("errorString"),
+                                              reply->errorString()).toString();
+            if (reply->error() == KAuth::ActionReply::Error::AuthorizationDeniedError) {
+                message = i18n("Error: Authorization denied");
+            }
+            finishTransactionWithError(message);
+        }
+    });
+
+    // get progress reports for this job
+    QObject::connect(reply, QOverload<KJob*, unsigned long>::of(&KAuth::ExecuteJob::percent), this,
+                     [this](KJob *job, unsigned long percent) {
+        Q_UNUSED(job)
+        if (percent >= 40 && role() == InstallRole) {
+            setStatus(CommittingStatus);
+        }
+        setProgress(static_cast<int>(percent));
+    });
+
+    setProgress(0);
+    if (role() == InstallRole) {
+        setStatus(DownloadingStatus);
     } else {
-        finishTransaction();
+        setStatus(CommittingStatus);
     }
+
+    reply->start();
 }
 
-void AlpineApkTransaction::finishTransaction()
+void AlpineApkTransaction::finishTransactionOK()
 {
     AbstractResource::State newState;
     switch(role()) {
@@ -83,8 +145,16 @@ void AlpineApkTransaction::finishTransaction()
         newState = AbstractResource::None;
         break;
     }
-    m_app->setAddons(addons());
-    m_app->setState(newState);
+    m_resource->setAddons(addons());
+    m_resource->setState(newState);
     setStatus(DoneStatus);
     deleteLater();
 }
+
+void AlpineApkTransaction::finishTransactionWithError(const QString &errMsg)
+{
+    qCWarning(LOG_ALPINEAPK) << "Transaction finished with error:" << errMsg;
+    Q_EMIT passiveMessage(i18n("Error") + QStringLiteral(":\n") + errMsg);
+    setStatus(DoneWithErrorStatus);
+    deleteLater();
+}
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkTransaction.h b/libdiscover/backends/AlpineApkBackend/AlpineApkTransaction.h
index ba0a77502..cab1f6b99 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkTransaction.h
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkTransaction.h
@@ -30,19 +30,19 @@ class AlpineApkTransaction : public Transaction
 {
 Q_OBJECT
 public:
-    AlpineApkTransaction(AlpineApkResource *app, Role role);
-    AlpineApkTransaction(AlpineApkResource *app, const AddonList &list, Role role);
+    AlpineApkTransaction(AlpineApkResource *res, Role role);
+    AlpineApkTransaction(AlpineApkResource *res, const AddonList &list, Role role);
 
     void cancel() override;
     void proceed() override;
 
 private Q_SLOTS:
-    void iterateTransaction();
-    void finishTransaction();
+    void startTransaction();
+    void finishTransactionOK();
+    void finishTransactionWithError(const QString &errMsg);
 
 private:
-    bool m_iterate = true;
-    AlpineApkResource *m_app;
+    AlpineApkResource *m_resource;
     AlpineApkBackend *m_backend;
 };
 
-- 
GitLab


From a9c6d73871dbe4fedd1e3419609fab662110395b Mon Sep 17 00:00:00 2001
From: Alexey Min <alexey.min@gmail.com>
Date: Thu, 30 Jul 2020 03:41:57 +0300
Subject: [PATCH 32/59] AlpineApkBackend: build with AppStreamQt support

provide list of AppStreamQt components, load them once at startup
---
 .../AlpineApkBackend/AlpineApkBackend.cpp     | 88 +++++++++++++++++--
 .../AlpineApkBackend/AlpineApkBackend.h       |  4 +
 .../backends/AlpineApkBackend/CMakeLists.txt  |  1 +
 3 files changed, 88 insertions(+), 5 deletions(-)

diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp
index b695e5e6b..ac91a6efa 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp
@@ -32,11 +32,14 @@
 
 #include <KLocalizedString>
 
+#include <AppStreamQt/pool.h>
+
+#include <QAction>
 #include <QDebug>
 #include <QLoggingCategory>
+#include <QSet>
 #include <QThread>
 #include <QTimer>
-#include <QAction>
 
 DISCOVER_BACKEND_PLUGIN(AlpineApkBackend)
 
@@ -68,6 +71,36 @@ AlpineApkBackend::AlpineApkBackend(QObject *parent)
     m_updatesTimeoutTimer->setSingleShot(true);
     m_updatesTimeoutTimer->setInterval(5 * 60 * 1000); // 5 minutes
 
+    qCDebug(LOG_ALPINEAPK) << "backend: loading AppStream metadata...";
+    AppStream::Pool *appStreamPool = new AppStream::Pool(this);
+    appStreamPool->setFlags(AppStream::Pool::FlagReadCollection |
+                            AppStream::Pool::FlagReadMetainfo |
+                            AppStream::Pool::FlagReadDesktopFiles);
+    appStreamPool->setCacheFlags(AppStream::Pool::CacheFlagUseUser |
+                                 AppStream::Pool::CacheFlagUseSystem);
+    if (!appStreamPool->load()) {
+        qCWarning(LOG_ALPINEAPK) << "backend: Failed to load appstream data:"
+                                 << appStreamPool->lastError();
+    } else {
+        m_appStreamComponents = appStreamPool->components();
+        qCDebug(LOG_ALPINEAPK) << "backend: loaded AppStream metadata OK:"
+                               << m_appStreamComponents.size() << "components.";
+        // collect all categories present in appstream metadata
+//        QSet<QString> collectedCategories;
+//        for (const AppStream::Component &component : m_appStreamComponents) {
+//            const QStringList cats = component.categories();
+//            for (const QString &cat : cats) {
+//                collectedCategories.insert(cat);
+//            }
+//        }
+//        for (const QString &cat : collectedCategories) {
+//            qCDebug(LOG_ALPINEAPK) << "    collected category: " << cat;
+//            m_collectedCategories << cat;
+//        }
+    }
+    delete appStreamPool;
+    appStreamPool = nullptr;
+
     qCDebug(LOG_ALPINEAPK) << "backend: populating resources...";
 
     if (m_apkdb.open(QtApk::QTAPK_OPENF_READONLY)) {
@@ -78,13 +111,38 @@ AlpineApkBackend::AlpineApkBackend(QObject *parent)
 
     if (m_availablePackages.size() > 0) {
         for (const QtApk::Package &pkg: m_availablePackages) {
-            AlpineApkResource *res = new AlpineApkResource(pkg, this);
+
+            // try to find appstream data for this package
+            AppStream::Component appstreamComponent;
+            AbstractResource::Type resType = AbstractResource::Type::Technical; // default
+            for (const auto& appsC : m_appStreamComponents) {
+                // find result which package name is exactly the one we want
+                if (appsC.packageNames().contains(pkg.name)) {
+                    // found!
+                    appstreamComponent = appsC;
+                    // determine resource type here
+                    switch (appsC.kind()) {
+                    case AppStream::Component::KindDesktopApp:
+                    case AppStream::Component::KindConsoleApp:
+                    case AppStream::Component::KindWebApp:
+                        resType = AbstractResource::Type::Application;
+                        break;
+                    case AppStream::Component::KindAddon:
+                        resType = AbstractResource::Type::Addon;
+                        break;
+                    default:
+                        resType = AbstractResource::Type::Technical;
+                        break;
+                    }
+                    break; // exit for() loop
+                }
+            }
+
+            AlpineApkResource *res = new AlpineApkResource(pkg, appstreamComponent, resType, this);
             res->setCategoryName(QStringLiteral("alpine_packages"));
             res->setOriginSource(QStringLiteral("apk"));
             res->setSection(QStringLiteral("dummy"));
-            // here is the place to set a proper type of package
-            // AlpineApkResource defaults to AbstractResource::Technical,
-            // which places it into "System updates" section
+
             const QString key = pkg.name.toLower();
             m_resources.insert(key, res);
             connect(res, &AlpineApkResource::stateChanged,
@@ -125,6 +183,26 @@ QVector<Category *> AlpineApkBackend::category() const
     );
 
     return { s_rootCat };
+
+//    static QVector<Category *> s_cats;
+//    if (s_cats.isEmpty()) {
+//        // fill only once
+//        s_cats << s_rootCat;
+//        for (const QString &scat : m_collectedCategories) {
+//            Category *cat = new Category(
+//                        scat,   // name
+//                        QStringLiteral("package-x-generic"), // icon
+//                        {},     // orFilters
+//                        { displayName() }, // pluginName
+//                        {},     // subcategories
+//                        QUrl(), // decoration
+//                        false   // isAddons
+//            );
+//            s_cats << cat;
+//        }
+//    }
+//    return s_cats;
+    // ^^ causes deep hang in discover in recalculating QML bindings
 }
 
 int AlpineApkBackend::updatesCount() const
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.h b/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.h
index 755cf6a5a..efcaf2ea8 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.h
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.h
@@ -26,6 +26,8 @@
 
 #include <QtApk>
 
+#include <AppStreamQt/component.h>
+
 class AlpineApkReviewsBackend;
 class AlpineApkUpdater;
 class AlpineApkResource;
@@ -77,6 +79,8 @@ private:
     bool m_fetching = false;
     int m_fetchProgress = 0;
     QTimer *m_updatesTimeoutTimer;
+    QList<AppStream::Component> m_appStreamComponents;
+    // QVector<QString> m_collectedCategories;
 };
 
 #endif // AlpineApkBackend_H
diff --git a/libdiscover/backends/AlpineApkBackend/CMakeLists.txt b/libdiscover/backends/AlpineApkBackend/CMakeLists.txt
index 319c7ad2f..536525381 100644
--- a/libdiscover/backends/AlpineApkBackend/CMakeLists.txt
+++ b/libdiscover/backends/AlpineApkBackend/CMakeLists.txt
@@ -39,6 +39,7 @@ target_link_libraries(
         KF5::AuthCore
         Discover::Common
         ApkQt::ApkQt
+        AppStreamQt
 )
 
 # KAuth helper exe
-- 
GitLab


From 01ca9b795321cb0b5252aa4de71b9a9269991fbf Mon Sep 17 00:00:00 2001
From: Alexey Min <alexey.min@gmail.com>
Date: Thu, 30 Jul 2020 03:44:55 +0300
Subject: [PATCH 33/59] AlpineApkResource: AppStream integration

return some data from appstream metadata, if available
---
 .../AlpineApkBackend/AlpineApkResource.cpp    | 136 +++++++++++++++++-
 .../AlpineApkBackend/AlpineApkResource.h      |  13 +-
 2 files changed, 141 insertions(+), 8 deletions(-)

diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkResource.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkResource.cpp
index 346a28b28..a6f4bc0fa 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkResource.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkResource.cpp
@@ -20,14 +20,29 @@
 
 #include "AlpineApkResource.h"
 #include "alpineapk_backend_logging.h"  // generated by ECM
+
+#include <AppStreamQt/icon.h>
+#include <AppStreamQt/pool.h>
+#include <AppStreamQt/release.h>
+
+#include <QFileInfo>
+#include <QIcon>
+#include <QProcess>
+
+// libdiscover
+#include "appstream/AppStreamUtils.h"
+#include "config-paths.h"
 #include "Transaction/AddonList.h"
 
 AlpineApkResource::AlpineApkResource(const QtApk::Package &apkPkg,
+                                     AppStream::Component &component,
+                                     AbstractResource::Type typ,
                                      AbstractResourcesBackend *parent)
     : AbstractResource(parent)
     , m_state(AbstractResource::State::None)
-    , m_type(AbstractResource::Type::Technical)
+    , m_type(typ)
     , m_pkg(apkPkg)
+    , m_appsC(component)
 {
 }
 
@@ -43,11 +58,17 @@ QString AlpineApkResource::availableVersion() const
 
 QStringList AlpineApkResource::categories()
 {
+    if (hasAppStreamData()) {
+        return m_appsC.categories();
+    }
     return { m_category };
 }
 
 QString AlpineApkResource::comment()
 {
+    if (hasAppStreamData()) {
+        return m_appsC.summary();
+    }
     return m_pkg.description;
 }
 
@@ -58,26 +79,83 @@ int AlpineApkResource::size()
 
 QUrl AlpineApkResource::homepage()
 {
+    if (hasAppStreamData()) {
+        return m_appsC.url(AppStream::Component::UrlKindHomepage);
+    }
     return QUrl::fromUserInput(m_pkg.url);
 }
 
 QUrl AlpineApkResource::helpURL()
 {
+    if (hasAppStreamData()) {
+        return m_appsC.url(AppStream::Component::UrlKindHelp);
+    }
     return QUrl();
 }
 
 QUrl AlpineApkResource::bugURL()
 {
+    if (hasAppStreamData()) {
+        return m_appsC.url(AppStream::Component::UrlKindBugtracker);
+    }
     return QUrl();
 }
 
 QUrl AlpineApkResource::donationURL()
 {
+    if (hasAppStreamData()) {
+        return m_appsC.url(AppStream::Component::UrlKindDonation);
+    }
     return QUrl();
 }
 
+///xdg-compatible icon name to represent the resource, url or QIcon
 QVariant AlpineApkResource::icon() const
 {
+    if (hasAppStreamData()) {
+        const QList<AppStream::Icon> icns = m_appsC.icons();
+        if (icns.size() == 0) {
+            return QStringLiteral("package-x-generic");
+        }
+        QIcon ico;
+        const AppStream::Icon &appIco = icns.first();
+
+        switch (appIco.kind()) {
+        case AppStream::Icon::KindStock:
+            // we can create icons of this type directly from theme
+            ico = QIcon::fromTheme(appIco.name());
+            break;
+        case AppStream::Icon::KindLocal:
+        case AppStream::Icon::KindCached: {
+            // try from predefined standard Alpine path
+            const QString appstreamIconsPath = QLatin1String("/usr/share/app-info/icons/");
+            const QString path = appstreamIconsPath + appIco.url().path();
+            if (QFileInfo::exists(path)) {
+                ico.addFile(path, appIco.size());
+            } else {
+                const QString altPath = appstreamIconsPath +
+                        QStringLiteral("%1x%2/").arg(appIco.size().width()).arg(appIco.size().height()) +
+                        appIco.url().path();
+                if (QFileInfo::exists(altPath)) {
+                    ico.addFile(altPath, appIco.size());
+                }
+            }
+        } break;
+        default: break;
+        }
+
+        // return icon only if we successfully loaded it
+        if (!ico.isNull()) {
+            return QVariant::fromValue<QIcon>(ico);
+        }
+
+        // try to load from icon theme by package name, this is better
+        //   than nothing and works surprisingly well for many packages
+        ico = QIcon::fromTheme(m_pkg.name);
+        if (!ico.isNull()) {
+            return QVariant::fromValue<QIcon>(ico);
+        }
+    }
     return QStringLiteral("package-x-generic");
 }
 
@@ -98,11 +176,17 @@ QJsonArray AlpineApkResource::licenses()
 
 QString AlpineApkResource::longDescription()
 {
+    if (hasAppStreamData()) {
+        return m_appsC.description();
+    }
     return m_pkg.description;
 }
 
 QString AlpineApkResource::name() const
 {
+    if (hasAppStreamData()) {
+        return m_appsC.name();
+    }
     return m_pkg.name;
 }
 
@@ -128,13 +212,25 @@ AbstractResource::State AlpineApkResource::state()
 
 void AlpineApkResource::fetchChangelog()
 {
-    // QString log = longDescription();
-    // Q_EMIT changelogFetched(log);
+    if (hasAppStreamData()) {
+        emit changelogFetched(AppStreamUtils::changelogToHtml(m_appsC));
+    }
 }
 
 void AlpineApkResource::fetchScreenshots()
 {
-    // Q_EMIT screenshotsFetched(m_screenshotThumbnails, m_screenshots);
+    if (hasAppStreamData()) {
+        const QPair<QList<QUrl>, QList<QUrl> > sc = AppStreamUtils::fetchScreenshots(m_appsC);
+        Q_EMIT screenshotsFetched(sc.first, sc.second);
+    }
+}
+
+QString AlpineApkResource::appstreamId() const
+{
+    if (hasAppStreamData()) {
+        return m_appsC.id();
+    }
+    return QString();
 }
 
 void AlpineApkResource::setState(AbstractResource::State state)
@@ -184,20 +280,41 @@ void AlpineApkResource::setAvailableVersion(const QString &av)
     m_availableVersion = av;
 }
 
+bool AlpineApkResource::hasAppStreamData() const
+{
+    return !m_appsC.id().isEmpty();
+}
+
+bool AlpineApkResource::canExecute() const
+{
+    if (hasAppStreamData()) {
+        return (m_appsC.kind() == AppStream::Component::KindDesktopApp &&
+                (m_state == AbstractResource::Installed || m_state == AbstractResource::Upgradeable));
+    }
+    return false;
+}
 
 void AlpineApkResource::invokeApplication() const
 {
-    // QDesktopServices d;
-    // d.openUrl(QUrl(QStringLiteral("https://projects.kde.org/projects/extragear/sysadmin/muon")));
+    const QString desktopFile = QLatin1String("/usr/share/applications/") + appstreamId();
+    if (QFile::exists(desktopFile)) {
+        QProcess::startDetached(QStringLiteral("kstart5"), {QStringLiteral("--service"), desktopFile});
+    }
 }
 
 QUrl AlpineApkResource::url() const
 {
+    if (hasAppStreamData()) {
+        return QUrl(QStringLiteral("appstream://") + appstreamId());
+    }
     return QUrl(QLatin1String("apk://") + packageName());
 }
 
 QString AlpineApkResource::author() const
 {
+    if (hasAppStreamData()) {
+        return m_appsC.developerName();
+    }
     return m_pkg.maintainer;
 }
 
@@ -208,5 +325,12 @@ QString AlpineApkResource::sourceIcon() const
 
 QDate AlpineApkResource::releaseDate() const
 {
+    if (hasAppStreamData()) {
+        if (!m_appsC.releases().isEmpty()) {
+            auto release = m_appsC.releases().constFirst();
+            return release.timestamp().date();
+        }
+    }
+    // just build date is fine, too
     return m_pkg.buildTime.date();
 }
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkResource.h b/libdiscover/backends/AlpineApkBackend/AlpineApkResource.h
index 7140786c5..e8948c467 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkResource.h
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkResource.h
@@ -23,6 +23,7 @@
 
 #include <resources/AbstractResource.h>
 #include <QtApkPackage.h>
+#include <AppStreamQt/component.h>
 
 class AddonList;
 
@@ -31,7 +32,10 @@ class AlpineApkResource : public AbstractResource
     Q_OBJECT
 
 public:
-    explicit AlpineApkResource(const QtApk::Package &apkPkg, AbstractResourcesBackend *parent);
+    explicit AlpineApkResource(const QtApk::Package &apkPkg,
+                               AppStream::Component &component,
+                               AbstractResource::Type typ,
+                               AbstractResourcesBackend *parent);
 
     QList<PackageState> addonsInformation() override;
     QString section() override;
@@ -52,10 +56,11 @@ public:
     QString name() const override;
     QString packageName() const override;
     AbstractResource::Type type() const override { return m_type; }
-    bool canExecute() const override { return true; }
+    bool canExecute() const override;
     void invokeApplication() const override;
     void fetchChangelog() override;
     void fetchScreenshots() override;
+    QString appstreamId() const override;
     QUrl url() const override;
     QString author() const override;
     QString sourceIcon() const override;
@@ -69,6 +74,9 @@ public:
     void setAddonInstalled(const QString &addon, bool installed);
     void setAvailableVersion(const QString &av);
 
+private:
+    bool hasAppStreamData() const;
+
 public:
     AbstractResource::State m_state;
     const AbstractResource::Type m_type;
@@ -78,6 +86,7 @@ public:
     QString m_originSoruce;
     QString m_sectionName;
     QList<PackageState> m_addons;
+    AppStream::Component m_appsC;
 };
 
 #endif // ALPINEAPKRESOURCE_H
-- 
GitLab


From 95a56c85f35e838cbfb9d80387ceb72a53fa6b4c Mon Sep 17 00:00:00 2001
From: Alexey Min <alexey.min@gmail.com>
Date: Mon, 28 Sep 2020 08:38:25 +0300
Subject: [PATCH 34/59] AlpineApkBackend: cmake: depend on Qt5::Concurrent

---
 libdiscover/backends/AlpineApkBackend/CMakeLists.txt | 1 +
 1 file changed, 1 insertion(+)

diff --git a/libdiscover/backends/AlpineApkBackend/CMakeLists.txt b/libdiscover/backends/AlpineApkBackend/CMakeLists.txt
index 536525381..9e3bd82c2 100644
--- a/libdiscover/backends/AlpineApkBackend/CMakeLists.txt
+++ b/libdiscover/backends/AlpineApkBackend/CMakeLists.txt
@@ -34,6 +34,7 @@ target_link_libraries(
     PRIVATE
         Qt5::Core
         Qt5::Widgets
+        Qt5::Concurrent
         KF5::CoreAddons
         KF5::ConfigCore
         KF5::AuthCore
-- 
GitLab


From 9b2b1a2194a41e860f89ef6e0eb766233fa6d6ac Mon Sep 17 00:00:00 2001
From: Alexey Min <alexey.min@gmail.com>
Date: Mon, 28 Sep 2020 09:51:12 +0300
Subject: [PATCH 35/59] AlpineApkBackend: load packages data in backgroud

Load packages data and appstream metadata in background thread to
unblock UI and increase discover startup time.
---
 .../AlpineApkBackend/AlpineApkBackend.cpp     | 114 +++++++++++++-----
 .../AlpineApkBackend/AlpineApkBackend.h       |   5 +
 2 files changed, 91 insertions(+), 28 deletions(-)

diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp
index ac91a6efa..e441c4b36 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp
@@ -35,10 +35,14 @@
 #include <AppStreamQt/pool.h>
 
 #include <QAction>
+#include <QtConcurrentRun>
 #include <QDebug>
+#include <QFuture>
+#include <QFutureWatcher>
 #include <QLoggingCategory>
 #include <QSet>
 #include <QThread>
+#include <QThreadPool>
 #include <QTimer>
 
 DISCOVER_BACKEND_PLUGIN(AlpineApkBackend)
@@ -53,9 +57,6 @@ AlpineApkBackend::AlpineApkBackend(QObject *parent)
     const_cast<QLoggingCategory &>(LOG_ALPINEAPK()).setEnabled(QtDebugMsg, false);
 #endif
 
-    // schedule checking for updates
-    QTimer::singleShot(1000, this, &AlpineApkBackend::checkForUpdates);
-
     // connections with our updater
     QObject::connect(m_updater, &AlpineApkUpdater::updatesCountChanged,
                      this, &AlpineApkBackend::updatesCountChanged);
@@ -71,8 +72,24 @@ AlpineApkBackend::AlpineApkBackend(QObject *parent)
     m_updatesTimeoutTimer->setSingleShot(true);
     m_updatesTimeoutTimer->setInterval(5 * 60 * 1000); // 5 minutes
 
+    // load packages data in a separate thread; it takes a noticeable amount of time
+    //     and this way UI is not blocked here
+    m_fetching = true; // we are busy!
+    QFuture<void> loadResFuture = QtConcurrent::run(QThreadPool::globalInstance(), this,
+                                                    &AlpineApkBackend::loadResources);
+
+    QObject::connect(&m_voidFutureWatcher, &QFutureWatcher<void>::finished,
+                     this, &AlpineApkBackend::onLoadResourcesFinished);
+    m_voidFutureWatcher.setFuture(loadResFuture);
+
+    SourcesModel::global()->addSourcesBackend(new AlpineApkSourcesBackend(this));
+}
+
+void AlpineApkBackend::loadResources()
+{
     qCDebug(LOG_ALPINEAPK) << "backend: loading AppStream metadata...";
-    AppStream::Pool *appStreamPool = new AppStream::Pool(this);
+
+    AppStream::Pool *appStreamPool = new AppStream::Pool();
     appStreamPool->setFlags(AppStream::Pool::FlagReadCollection |
                             AppStream::Pool::FlagReadMetainfo |
                             AppStream::Pool::FlagReadDesktopFiles);
@@ -102,6 +119,7 @@ AlpineApkBackend::AlpineApkBackend(QObject *parent)
     appStreamPool = nullptr;
 
     qCDebug(LOG_ALPINEAPK) << "backend: populating resources...";
+    emit this->passiveMessage(i18n("Loading, please wait..."));
 
     if (m_apkdb.open(QtApk::QTAPK_OPENF_READONLY)) {
         m_availablePackages = m_apkdb.getAvailablePackages();
@@ -114,43 +132,77 @@ AlpineApkBackend::AlpineApkBackend(QObject *parent)
 
             // try to find appstream data for this package
             AppStream::Component appstreamComponent;
-            AbstractResource::Type resType = AbstractResource::Type::Technical; // default
             for (const auto& appsC : m_appStreamComponents) {
                 // find result which package name is exactly the one we want
                 if (appsC.packageNames().contains(pkg.name)) {
-                    // found!
-                    appstreamComponent = appsC;
-                    // determine resource type here
-                    switch (appsC.kind()) {
-                    case AppStream::Component::KindDesktopApp:
-                    case AppStream::Component::KindConsoleApp:
-                    case AppStream::Component::KindWebApp:
-                        resType = AbstractResource::Type::Application;
-                        break;
-                    case AppStream::Component::KindAddon:
-                        resType = AbstractResource::Type::Addon;
-                        break;
-                    default:
-                        resType = AbstractResource::Type::Technical;
-                        break;
+                    // workaround for kate (Kate Sessions is found first, but
+                    //   package name = "kate" too, bugged metadata?)
+                    if (pkg.name == QStringLiteral("kate")) {
+                        // qCDebug(LOG_ALPINEAPK) << appsC.packageNames() << appsC.id();
+                        // ^^ ("kate") "org.kde.plasma.katesessions"
+                        if (appsC.id() != QStringLiteral("org.kde.kate")) {
+                            continue;
+                        }
                     }
+                    appstreamComponent = appsC;
                     break; // exit for() loop
                 }
             }
 
+            const QString key = pkg.name.toLower();
+            m_resourcesAppstreamData.insert(key, appstreamComponent);
+        }
+    }
+
+    qCDebug(LOG_ALPINEAPK) << "  available" << m_availablePackages.size()
+                           << "packages";
+    qCDebug(LOG_ALPINEAPK) << "  installed" << m_installedPackages.size()
+                           << "packages";
+}
+
+static AbstractResource::Type toDiscoverResourceType(const AppStream::Component &component)
+{
+    AbstractResource::Type resType = AbstractResource::Type::Technical; // default
+    // determine resource type here
+    switch (component.kind()) {
+    case AppStream::Component::KindDesktopApp:
+    case AppStream::Component::KindConsoleApp:
+    case AppStream::Component::KindWebApp:
+        resType = AbstractResource::Type::Application;
+        break;
+    case AppStream::Component::KindAddon:
+        resType = AbstractResource::Type::Addon;
+        break;
+    default:
+        resType = AbstractResource::Type::Technical;
+        break;
+    }
+    return resType;
+}
+
+void AlpineApkBackend::onLoadResourcesFinished()
+{
+    qCDebug(LOG_ALPINEAPK) << "backend: appstream data loaded and sorted; fill in resources";
+
+    if (m_availablePackages.size() > 0) {
+        for (const QtApk::Package &pkg: m_availablePackages) {
+            const QString key = pkg.name.toLower();
+
+            AppStream::Component &appstreamComponent = m_resourcesAppstreamData[key];
+            const AbstractResource::Type resType = toDiscoverResourceType(appstreamComponent);
+
             AlpineApkResource *res = new AlpineApkResource(pkg, appstreamComponent, resType, this);
             res->setCategoryName(QStringLiteral("alpine_packages"));
             res->setOriginSource(QStringLiteral("apk"));
             res->setSection(QStringLiteral("dummy"));
 
-            const QString key = pkg.name.toLower();
             m_resources.insert(key, res);
-            connect(res, &AlpineApkResource::stateChanged,
-                    this, &AlpineApkBackend::updatesCountChanged);
+            QObject::connect(res, &AlpineApkResource::stateChanged,
+                             this, &AlpineApkBackend::updatesCountChanged);
         }
-        qCDebug(LOG_ALPINEAPK) << "  available" << m_availablePackages.size()
-                               << "packages";
     }
+
+    // update "installed/not installed" state
     if (m_installedPackages.size() > 0) {
         for (const QtApk::Package &pkg: m_installedPackages) {
             const QString key = pkg.name.toLower();
@@ -158,11 +210,17 @@ AlpineApkBackend::AlpineApkBackend(QObject *parent)
                 m_resources.value(key)->setState(AbstractResource::Installed);
             }
         }
-        qCDebug(LOG_ALPINEAPK) << "  installed" << m_installedPackages.size()
-                               << "packages";
     }
 
-    SourcesModel::global()->addSourcesBackend(new AlpineApkSourcesBackend(this));
+    qCDebug(LOG_ALPINEAPK) << "backend: resources loaded.";
+
+    m_fetching = false;
+    emit fetchingChanged();
+    // ^^ this causes the UI to update "Featured" page and show
+    //    to user that we actually have loaded packages data
+
+    // schedule check for updates 1 sec after we've loaded all resources
+    QTimer::singleShot(1000, this, &AlpineApkBackend::checkForUpdates);
 }
 
 QVector<Category *> AlpineApkBackend::category() const
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.h b/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.h
index efcaf2ea8..2ec8b00b6 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.h
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.h
@@ -23,6 +23,7 @@
 
 #include <resources/AbstractResourcesBackend.h>
 #include <QVariantList>
+#include <QFutureWatcher>
 
 #include <QtApk>
 
@@ -65,12 +66,15 @@ public Q_SLOTS:
 
 private Q_SLOTS:
     void finishCheckForUpdates();
+    void loadResources();
+    void onLoadResourcesFinished();
 
 public:
     QtApk::Database *apkdb() { return &m_apkdb; }
 
 private:
     QHash<QString, AlpineApkResource *> m_resources;
+    QHash<QString, AppStream::Component> m_resourcesAppstreamData;
     AlpineApkUpdater *m_updater;
     AlpineApkReviewsBackend *m_reviews;
     QtApk::Database m_apkdb;
@@ -81,6 +85,7 @@ private:
     QTimer *m_updatesTimeoutTimer;
     QList<AppStream::Component> m_appStreamComponents;
     // QVector<QString> m_collectedCategories;
+    QFutureWatcher<void> m_voidFutureWatcher;
 };
 
 #endif // AlpineApkBackend_H
-- 
GitLab


From bfa3b8b60945752bc478c6ed9c678959102dc2a9 Mon Sep 17 00:00:00 2001
From: Alexey Min <alexey.min@gmail.com>
Date: Mon, 28 Sep 2020 09:51:35 +0300
Subject: [PATCH 36/59] AlpineApkUpdater: disable too spammy logging

---
 libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp
index 2f3d184fb..c03fd5c5c 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp
@@ -204,7 +204,7 @@ void AlpineApkUpdater::proceed()
 
 int AlpineApkUpdater::updatesCount()
 {
-    qDebug(LOG_ALPINEAPK) << Q_FUNC_INFO << m_updatesCount;
+    // qDebug(LOG_ALPINEAPK) << Q_FUNC_INFO << m_updatesCount;
     return m_updatesCount;
 }
 
-- 
GitLab


From b21f40ff24b49447fb477437a38c098194e01eb1 Mon Sep 17 00:00:00 2001
From: Alexey Min <alexey.min@gmail.com>
Date: Wed, 13 Jan 2021 22:08:44 +0300
Subject: [PATCH 37/59] AlpineApkUpdater: try harder to get error string from
 helper

---
 .../backends/AlpineApkBackend/AlpineApkUpdater.cpp | 14 ++++++++++++--
 1 file changed, 12 insertions(+), 2 deletions(-)

diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp
index c03fd5c5c..677a784f8 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp
@@ -299,12 +299,22 @@ void AlpineApkUpdater::handleKAuthHelperError(
         KAuth::ExecuteJob *reply,
         const QVariantMap &replyData)
 {
-    const QString message = replyData.value(QLatin1String("errorString"),
+    // error message should be received as part of JSON reply from helper
+    QString message = replyData.value(QLatin1String("errorString"),
                                             reply->errorString()).toString();
-    qCDebug(LOG_ALPINEAPK) << "KAuth helper returned error:" << message << reply->error();
     if (reply->error() == KAuth::ActionReply::Error::AuthorizationDeniedError) {
+        qCWarning(LOG_ALPINEAPK) << "updater: KAuth helper returned AuthorizationDeniedError";
         Q_EMIT passiveMessage(i18n("Authorization denied"));
     } else {
+        // if received error message is empty, try other ways to get error text for user
+        // there are multiple ways to get error messages in kauth/kjob
+        if (message.isEmpty()) {
+            message = reply->errorString();
+            if (message.isEmpty()) {
+                message = reply->errorText();
+            }
+        }
+        qCDebug(LOG_ALPINEAPK) << "updater: KAuth helper returned error:" << message << reply->error();
         Q_EMIT passiveMessage(i18n("Error") + QStringLiteral(":\n") + message);
     }
 }
-- 
GitLab


From b4280cc564d0c54fb69d6ae46fd111ab695bf052 Mon Sep 17 00:00:00 2001
From: Alexey Min <alexey.min@gmail.com>
Date: Sun, 24 Jan 2021 08:16:20 +0300
Subject: [PATCH 38/59] AlpineApkBackend: rename root category name

---
 libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp
index e441c4b36..8ddcc8e9f 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp
@@ -231,7 +231,7 @@ QVector<Category *> AlpineApkBackend::category() const
     // Display a single root category
     // we could add more, but Alpine apk does not have this concept
     static Category *s_rootCat = new Category(
-            i18nc("Root category name", "Alpine packages"),
+            i18nc("Root category name", "Alpine Linux packages"),
             QStringLiteral("package-x-generic"), // icon
             { s_apkFlt },      // orFilters - include packages that match filter
             { displayName() }, // pluginName
-- 
GitLab


From 3267907173e509ff8c4f560055cd2cbadcf2e260 Mon Sep 17 00:00:00 2001
From: Alexey Min <alexey.min@gmail.com>
Date: Sun, 24 Jan 2021 08:28:04 +0300
Subject: [PATCH 39/59] KAuth helper: use single KAuth action for all
 operations

Use single entry point for all package management operations.
Using single KAuth action allows user to enter his password only once
for all privileged operations during Discover session.
---
 .../AlpineApkBackend/AlpineApkAuthHelper.cpp  | 91 ++++++++++---------
 .../AlpineApkBackend/AlpineApkAuthHelper.h    | 15 ++-
 .../org.kde.discover.alpineapkbackend.actions | 30 +-----
 3 files changed, 59 insertions(+), 77 deletions(-)

diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp
index e6e6cb7a1..97affc013 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp
@@ -113,6 +113,7 @@ void AlpineApkAuthHelper::onTransactionError(const QString &msg)
     qCWarning(LOG_AUTHHELPER).nospace() << "ERROR occured in transaction \""
                                         << m_currentTransaction->desc()
                                         << "\": " << msg;
+    // construct error message to use in helper reply
     const QString errMsg = m_currentTransaction->desc() + QLatin1String(" failed: ") + msg;
     m_actionReply.setErrorDescription(errMsg);
     m_actionReply.setData({
@@ -129,16 +130,48 @@ void AlpineApkAuthHelper::onTransactionFinished()
     m_loop->quit();
 }
 
-ActionReply AlpineApkAuthHelper::update(const QVariantMap &args)
+// single entry point for all package management actions
+ActionReply AlpineApkAuthHelper::pkgmgmt(const QVariantMap &args)
 {
-    // return error by default
     m_actionReply = ActionReply::HelperErrorReply();
-
     HelperSupport::progressStep(0);
 
+    // actual package management action to perform is passed in "pkgAction" argument
+    if (!args.contains(QLatin1String("pkgAction"))) {
+        m_actionReply.setError(ActionReply::InvalidActionError);
+        m_actionReply.setErrorDescription(QLatin1String("Please pass \'pkgAction\' argument."));
+        HelperSupport::progressStep(100);
+        return m_actionReply;
+    }
+
+    const QString pkgAction = args.value(QLatin1String("pkgAction")).toString();
+
+    if (pkgAction == QStringLiteral("update")) {
+        update(args);
+    } else if (pkgAction == QStringLiteral("add")) {
+        add(args);
+    } else if (pkgAction == QStringLiteral("del")) {
+        del(args);
+    } else if (pkgAction == QStringLiteral("upgrade")) {
+        upgrade(args);
+    } else if (pkgAction == QStringLiteral("repoconfig")) {
+        repoconfig(args);
+    } else {
+        // error: unknown pkgAction
+        m_actionReply.setError(ActionReply::NoSuchActionError);
+        m_actionReply.setErrorDescription(QLatin1String("Please pass a valid \'pkgAction\' argument. "
+                                                        "Action \"%1\" is not recognized.").arg(pkgAction));
+    }
+
+    HelperSupport::progressStep(100);
+    return m_actionReply;
+}
+
+void AlpineApkAuthHelper::update(const QVariantMap &args)
+{
     if (!openDatabase(args)) {
         m_actionReply.setErrorDescription(QStringLiteral("Failed to open database!"));
-        return m_actionReply;
+        return;
     }
 
     m_trans_ok = true;
@@ -155,27 +188,19 @@ ActionReply AlpineApkAuthHelper::update(const QVariantMap &args)
             { QLatin1String("updatesCount"), updatesCount }
         });
     }
-
-    HelperSupport::progressStep(100);
-    return m_actionReply;
 }
 
-ActionReply AlpineApkAuthHelper::add(const QVariantMap &args)
+void AlpineApkAuthHelper::add(const QVariantMap &args)
 {
-    // return error by default
-    m_actionReply = ActionReply::HelperErrorReply();
-
-    HelperSupport::progressStep(0);
-
     if (!openDatabase(args)) {
         m_actionReply.setErrorDescription(QStringLiteral("Failed to open database!"));
-        return m_actionReply;
+        return;
     }
 
     const QString pkgName = args.value(QLatin1String("pkgName"), QString()).toString();
     if (pkgName.isEmpty()) {
         m_actionReply.setErrorDescription(QStringLiteral("Specify pkgName for adding!"));
-        return m_actionReply;
+        return;
     }
 
     m_trans_ok = true;
@@ -188,27 +213,19 @@ ActionReply AlpineApkAuthHelper::add(const QVariantMap &args)
     if (m_trans_ok) {
         m_actionReply = ActionReply::SuccessReply();
     }
-
-    HelperSupport::progressStep(100);
-    return m_actionReply;
 }
 
-ActionReply AlpineApkAuthHelper::del(const QVariantMap &args)
+void AlpineApkAuthHelper::del(const QVariantMap &args)
 {
-    // return error by default
-    m_actionReply = ActionReply::HelperErrorReply();
-
-    HelperSupport::progressStep(0);
-
     if (!openDatabase(args)) {
         m_actionReply.setErrorDescription(QStringLiteral("Failed to open database!"));
-        return m_actionReply;
+        return;
     }
 
     const QString pkgName = args.value(QLatin1String("pkgName"), QString()).toString();
     if (pkgName.isEmpty()) {
         m_actionReply.setErrorDescription(QStringLiteral("Specify pkgName for removing!"));
-        return m_actionReply;
+        return;
     }
 
     const bool delRdepends = args.value(QLatin1String("delRdepends"), false).toBool();
@@ -228,20 +245,13 @@ ActionReply AlpineApkAuthHelper::del(const QVariantMap &args)
     if (m_trans_ok) {
         m_actionReply = ActionReply::SuccessReply();
     }
-
-    HelperSupport::progressStep(100);
-    return m_actionReply;
 }
 
-ActionReply AlpineApkAuthHelper::upgrade(const QVariantMap &args)
+void AlpineApkAuthHelper::upgrade(const QVariantMap &args)
 {
-    m_actionReply = ActionReply::HelperErrorReply();
-
-    HelperSupport::progressStep(0);
-
     if (!openDatabase(args)) {
         m_actionReply.setErrorDescription(QStringLiteral("Failed to open database!"));
-        return m_actionReply;
+        return;
     }
 
     bool onlySimulate = args.value(QLatin1String("onlySimulate"), false).toBool();
@@ -272,16 +282,10 @@ ActionReply AlpineApkAuthHelper::upgrade(const QVariantMap &args)
         replyData.insert(QLatin1String("onlySimulate"), onlySimulate);
         m_actionReply.setData(replyData);
     }
-
-    HelperSupport::progressStep(100);
-    return m_actionReply;
 }
 
-ActionReply AlpineApkAuthHelper::repoconfig(const QVariantMap &args)
+void AlpineApkAuthHelper::repoconfig(const QVariantMap &args)
 {
-    m_actionReply = ActionReply::HelperErrorReply();
-    HelperSupport::progressStep(10);
-
     if (args.contains(QLatin1String("repoList"))) {
         const QVariant v = args.value(QLatin1String("repoList"));
         const QVector<QtApk::Repository> repoVec = v.value<QVector<QtApk::Repository>>();
@@ -293,9 +297,6 @@ ActionReply AlpineApkAuthHelper::repoconfig(const QVariantMap &args)
     } else {
         m_actionReply.setErrorDescription(QStringLiteral("repoList parameter is missing in request!"));
     }
-
-    HelperSupport::progressStep(100);
-    return m_actionReply;
 }
 
 KAUTH_HELPER_MAIN("org.kde.discover.alpineapkbackend", AlpineApkAuthHelper)
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.h b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.h
index 7fd69a8f3..240e6ed35 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.h
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.h
@@ -35,17 +35,22 @@ public:
     ~AlpineApkAuthHelper() override;
 
 public Q_SLOTS:
-    ActionReply update(const QVariantMap &args);
-    ActionReply add(const QVariantMap &args);
-    ActionReply del(const QVariantMap &args);
-    ActionReply upgrade(const QVariantMap &args);
-    ActionReply repoconfig(const QVariantMap &args);
+    // single entry point for all package management operations
+    ActionReply pkgmgmt(const QVariantMap &args);
 
 protected:
+    // helpers
     bool openDatabase(const QVariantMap &args, bool readwrite = true);
     void closeDatabase();
     void setupTransactionPostCreate(QtApk::Transaction *trans);
 
+    // individual pakckage management actions
+    void update(const QVariantMap &args);
+    void add(const QVariantMap &args);
+    void del(const QVariantMap &args);
+    void upgrade(const QVariantMap &args);
+    void repoconfig(const QVariantMap &args);
+
 protected Q_SLOTS:
     void reportProgress(float percent);
     void onTransactionError(const QString &msg);
diff --git a/libdiscover/backends/AlpineApkBackend/org.kde.discover.alpineapkbackend.actions b/libdiscover/backends/AlpineApkBackend/org.kde.discover.alpineapkbackend.actions
index 5f7f76771..c9bb5f9f2 100644
--- a/libdiscover/backends/AlpineApkBackend/org.kde.discover.alpineapkbackend.actions
+++ b/libdiscover/backends/AlpineApkBackend/org.kde.discover.alpineapkbackend.actions
@@ -1,29 +1,5 @@
-[org.kde.discover.alpineapkbackend.update]
-Name=Update repository index
-Description=Updates available packages list from repositories
-Policy=auth_admin
-Persistence=session
-
-[org.kde.discover.alpineapkbackend.upgrade]
-Name=Upgrade all upgradable packages
-Description=Upgrade installed packages to latest versions
-Policy=auth_admin
-Persistence=session
-
-[org.kde.discover.alpineapkbackend.add]
-Name=Install/upgrade package
-Description=Installs/upgrades one package
-Policy=auth_admin
-Persistence=session
-
-[org.kde.discover.alpineapkbackend.del]
-Name=Remove package
-Description=Uninstall one package
-Policy=auth_admin
-Persistence=session
-
-[org.kde.discover.alpineapkbackend.repoconfig]
-Name=Configure repositories
-Description=Configure repositories URLs
+[org.kde.discover.alpineapkbackend.pkgmgmt]
+Name=Package management
+Description=Install or remove packages, upgrade system
 Policy=auth_admin
 Persistence=session
-- 
GitLab


From 66e8f703cfe1059ef763ffa989d7ed7fc1dd975b Mon Sep 17 00:00:00 2001
From: Alexey Min <alexey.min@gmail.com>
Date: Sun, 24 Jan 2021 08:32:59 +0300
Subject: [PATCH 40/59] AlpineApk: refactor code to use single entry for all
 KAuth stuff

 * Add AlpineApkAuthActionFactory to contain all KAuth
   action creation stuff in one place.
 * ActionFactory: Use new single entry point for all KAuth
   helper operations.
 * Switch all places in code that used KAuth actions to
   using ActionFactory:create*() methods.
---
 .../AlpineApkAuthActionFactory.cpp            | 118 ++++++++++++++++++
 .../AlpineApkAuthActionFactory.h              |  41 ++++++
 .../AlpineApkSourcesBackend.cpp               |  28 +----
 .../AlpineApkBackend/AlpineApkTransaction.cpp |  29 +----
 .../AlpineApkBackend/AlpineApkUpdater.cpp     |  45 +------
 .../backends/AlpineApkBackend/CMakeLists.txt  |   2 +
 6 files changed, 178 insertions(+), 85 deletions(-)
 create mode 100644 libdiscover/backends/AlpineApkBackend/AlpineApkAuthActionFactory.cpp
 create mode 100644 libdiscover/backends/AlpineApkBackend/AlpineApkAuthActionFactory.h

diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkAuthActionFactory.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthActionFactory.cpp
new file mode 100644
index 000000000..972f8ec50
--- /dev/null
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthActionFactory.cpp
@@ -0,0 +1,118 @@
+/***************************************************************************
+ *   Copyright © 2020 Alexey Min <alexey.min@gmail.com>                    *
+ *                                                                         *
+ *   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 <http://www.gnu.org/licenses/>. *
+ ***************************************************************************/
+
+#include <KLocalizedString>
+#include <kauth_version.h>
+
+#include "AlpineApkAuthActionFactory.h"
+#include "alpineapk_backend_logging.h"
+
+namespace ActionFactory {
+
+static KAuth::Action createAlpineApkKAuthAction()
+{
+    KAuth::Action action(QStringLiteral("org.kde.discover.alpineapkbackend.pkgmgmt"));
+    action.setHelperId(QStringLiteral("org.kde.discover.alpineapkbackend"));
+    if (!action.isValid()) {
+        qCWarning(LOG_ALPINEAPK) << "Created KAuth action is not valid!";
+        return action;
+    }
+
+    // set action description
+    // setDetails deprecated since KF 5.68, use setDetailsV2() with DetailsMap.
+#if KAUTH_VERSION < QT_VERSION_CHECK(5, 68, 0)
+    action.setDetails(i18n("Package management"));
+#else
+    static const KAuth::Action::DetailsMap details{
+        { KAuth::Action::AuthDetail::DetailMessage, i18n("Package management") }
+    };
+    action.setDetailsV2(details);
+#endif
+
+    // change default timeout to 1 minute, bcause default DBus timeout
+    //   of 25 seconds is not enough
+    action.setTimeout(1 * 60 * 1000);
+
+    return action;
+}
+
+KAuth::ExecuteJob *createUpdateAction(const QString &fakeRoot)
+{
+    KAuth::Action action = createAlpineApkKAuthAction();
+    if (!action.isValid()) {
+        return nullptr;
+    }
+    // update-action specific details
+    action.setTimeout(2 * 60 * 1000); // 2 minutes
+    action.addArgument(QLatin1String("pkgAction"), QLatin1String("update"));
+    action.addArgument(QLatin1String("fakeRoot"), fakeRoot);
+    return action.execute();
+}
+
+KAuth::ExecuteJob *createUpgradeAction(bool onlySimulate)
+{
+    KAuth::Action action = createAlpineApkKAuthAction();
+    if (!action.isValid()) {
+        return nullptr;
+    }
+    action.setTimeout(3 * 60 * 60 * 1000); // 3 hours, system upgrade can take really long
+    action.addArgument(QLatin1String("pkgAction"), QLatin1String("upgrade"));
+    action.addArgument(QLatin1String("onlySimulate"), onlySimulate);
+    return action.execute();
+}
+
+KAuth::ExecuteJob *createAddAction(const QString &pkgName)
+{
+    KAuth::Action action = createAlpineApkKAuthAction();
+    if (!action.isValid()) {
+        return nullptr;
+    }
+    action.setTimeout(1 * 60 * 60 * 1000); // 1 hour, in case package is really big?
+    action.addArgument(QLatin1String("pkgAction"), QLatin1String("add"));
+    action.addArgument(QLatin1String("pkgName"), pkgName);
+    return action.execute();
+}
+
+KAuth::ExecuteJob *createDelAction(const QString &pkgName)
+{
+    KAuth::Action action = createAlpineApkKAuthAction();
+    if (!action.isValid()) {
+        return nullptr;
+    }
+    action.setTimeout(1 * 60 * 60 * 1000); // although deletion is almost instant
+    action.addArgument(QLatin1String("pkgAction"), QLatin1String("del"));
+    action.addArgument(QLatin1String("pkgName"), pkgName);
+    return action.execute();
+}
+
+KAuth::ExecuteJob *createRepoconfigAction(const QVariant &repoUrls)
+{
+    KAuth::Action action = createAlpineApkKAuthAction();
+    if (!action.isValid()) {
+        return nullptr;
+    }
+    // should be instant, writes few lines to /etc/apk/repositories
+    action.setTimeout(1 * 60 * 1000); // 1 minute
+    action.addArgument(QLatin1String("pkgAction"), QLatin1String("repoconfig"));
+    action.addArgument(QLatin1String("repoList"), repoUrls);
+    return action.execute();
+}
+
+} // namespace ActionFactory
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkAuthActionFactory.h b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthActionFactory.h
new file mode 100644
index 000000000..ff5667f85
--- /dev/null
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthActionFactory.h
@@ -0,0 +1,41 @@
+/***************************************************************************
+ *   Copyright © 2020 Alexey Min <alexey.min@gmail.com>                    *
+ *                                                                         *
+ *   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 <http://www.gnu.org/licenses/>. *
+ ***************************************************************************/
+
+#ifndef AlpineApkAuthActionFactory_H
+#define AlpineApkAuthActionFactory_H
+
+#include <QString>
+#include <QVariant>
+
+#include <KAuthAction>
+#include <KAuthActionReply>
+#include <KAuthExecuteJob>
+
+namespace ActionFactory {
+
+KAuth::ExecuteJob *createUpdateAction(const QString &fakeRoot);
+KAuth::ExecuteJob *createUpgradeAction(bool onlySimulate = false);
+KAuth::ExecuteJob *createAddAction(const QString &pkgName);
+KAuth::ExecuteJob *createDelAction(const QString &pkgName);
+KAuth::ExecuteJob *createRepoconfigAction(const QVariant &repoUrls);
+
+} // namespace ActionFactory
+
+#endif
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkSourcesBackend.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkSourcesBackend.cpp
index 28f08ef86..a126483a9 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkSourcesBackend.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkSourcesBackend.cpp
@@ -19,6 +19,7 @@
  ***************************************************************************/
 
 #include "AlpineApkSourcesBackend.h"
+#include "AlpineApkAuthActionFactory.h"
 #include "alpineapk_backend_logging.h"  // generated by ECM
 
 #include <QDebug>
@@ -27,7 +28,6 @@
 
 // KF5
 #include <KAuthExecuteJob>
-#include <kauth_version.h>
 #include <KLocalizedString>
 
 // libapk-qt
@@ -100,28 +100,12 @@ void AlpineApkSourcesBackend::fillModelFromRepos()
 
 void AlpineApkSourcesBackend::saveSources()
 {
-    KAuth::Action repoConfigAction(QStringLiteral("org.kde.discover.alpineapkbackend.repoconfig"));
-    repoConfigAction.setHelperId(QStringLiteral("org.kde.discover.alpineapkbackend"));
-    if (!repoConfigAction.isValid()) {
-        qCWarning(LOG_ALPINEAPK) << "repoConfigAction is not valid!";
-        return;
-    }
-
-    repoConfigAction.setTimeout(1 * 60 * 1000); // 1 min
-#if KAUTH_VERSION < QT_VERSION_CHECK(5, 68, 0)
-    upgradeAction.setDetails(i18n("Configure repositories URLs"));
-#else
-    static const KAuth::Action::DetailsMap details{
-        { KAuth::Action::AuthDetail::DetailMessage, i18n("Configure repositories URLs") }
-    };
-    repoConfigAction.setDetailsV2(details);
-#endif
-    // pass in new repositories list
-    repoConfigAction.addArgument(QLatin1String("repoList"),
-                                 QVariant::fromValue<QVector<QtApk::Repository>>(m_repos));
+    const QVariant repoUrls = QVariant::fromValue<QVector<QtApk::Repository>>(m_repos);
 
     // run with elevated privileges
-    KAuth::ExecuteJob *reply = repoConfigAction.execute();
+    KAuth::ExecuteJob *reply = ActionFactory::createRepoconfigAction(repoUrls);
+    if (!reply) return;
+
     QObject::connect(reply, &KAuth::ExecuteJob::result, this, [this] (KJob *job) {
         KAuth::ExecuteJob *reply = static_cast<KAuth::ExecuteJob *>(job);
         if (reply->error() != 0) {
@@ -142,7 +126,7 @@ void AlpineApkSourcesBackend::saveSources()
 
 void AlpineApkSourcesBackend::onItemChanged(QStandardItem *item)
 {
-    // update internal storage vector and relaod model from it
+    // update internal storage vector and reload model from it
     //    otherwise checks state are not updated in UI
     const Qt::CheckState cs = item->checkState();
     const QModelIndex idx = m_sourcesModel->indexFromItem(item);
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkTransaction.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkTransaction.cpp
index ffd442927..26bf1bd0d 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkTransaction.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkTransaction.cpp
@@ -21,6 +21,7 @@
 #include "AlpineApkTransaction.h"
 #include "AlpineApkBackend.h"
 #include "AlpineApkResource.h"
+#include "AlpineApkAuthActionFactory.h"
 #include "alpineapk_backend_logging.h"  // generated by ECM
 
 // Qt
@@ -29,7 +30,6 @@
 
 // KF5
 #include <KAuthExecuteJob>
-#include <kauth_version.h>
 #include <KLocalizedString>
 
 AlpineApkTransaction::AlpineApkTransaction(AlpineApkResource *res, Role role)
@@ -61,42 +61,23 @@ void AlpineApkTransaction::cancel()
 
 void AlpineApkTransaction::startTransaction()
 {
-    KAuth::Action authAction;
-    QString actionDescription(i18n("Install package"));
+    KAuth::ExecuteJob *reply = nullptr;
     switch(role()) {
     case InstallRole:
-        authAction.setName(QStringLiteral("org.kde.discover.alpineapkbackend.add"));
+        reply = ActionFactory::createAddAction(m_resource->m_pkg.name);
         break;
     case RemoveRole:
-        authAction.setName(QStringLiteral("org.kde.discover.alpineapkbackend.del"));
-        actionDescription = i18n("Remove package");
+        reply = ActionFactory::createDelAction(m_resource->m_pkg.name);
         break;
     case ChangeAddonsRole:
         qCWarning(LOG_ALPINEAPK) << "Addons are not supported by Alpine APK Backend!";
-        return;
         break;
     }
-    authAction.setHelperId(QStringLiteral("org.kde.discover.alpineapkbackend"));
 
-    if (!authAction.isValid()) {
-        qCWarning(LOG_ALPINEAPK) << "kauth addAction is not valid!";
+    if (!reply) {
         return;
     }
 
-    authAction.setTimeout(60 * 60 * 1000); // 60 min
-#if KAUTH_VERSION < QT_VERSION_CHECK(5, 68, 0)
-    addAction.setDetails(actionDescription);
-#else
-    const KAuth::Action::DetailsMap details{
-        { KAuth::Action::AuthDetail::DetailMessage, actionDescription }
-    };
-    authAction.setDetailsV2(details);
-#endif
-    authAction.addArgument(QLatin1String("pkgName"), m_resource->m_pkg.name);
-
-    // run action with elevated privileges
-    KAuth::ExecuteJob *reply = authAction.execute();
-
     // get result of this job
     QObject::connect(reply, &KAuth::ExecuteJob::result, this, [this](KJob *job) {
         KAuth::ExecuteJob *reply = static_cast<KAuth::ExecuteJob *>(job);
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp
index 677a784f8..0bbbce1c0 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp
@@ -21,11 +21,11 @@
 #include "AlpineApkUpdater.h"
 #include "AlpineApkResource.h"
 #include "AlpineApkBackend.h"
+#include "AlpineApkAuthActionFactory.h"
 #include "alpineapk_backend_logging.h"
 #include "utils.h"
 
 #include <KAuthExecuteJob>
-#include <kauth_version.h>
 #include <KLocalizedString>
 
 #include <QtApk>
@@ -167,27 +167,10 @@ void AlpineApkUpdater::cancel()
 void AlpineApkUpdater::start()
 {
     qCDebug(LOG_ALPINEAPK) << Q_FUNC_INFO;
-    KAuth::Action upgradeAction(QStringLiteral("org.kde.discover.alpineapkbackend.upgrade"));
-    upgradeAction.setHelperId(QStringLiteral("org.kde.discover.alpineapkbackend"));
-
-    if (!upgradeAction.isValid()) {
-        qCWarning(LOG_ALPINEAPK) << "kauth upgradeAction is not valid!";
-        return;
-    }
-
-    upgradeAction.setTimeout(30 * 60 * 1000); // 30 min
-#if KAUTH_VERSION < QT_VERSION_CHECK(5, 68, 0)
-    upgradeAction.setDetails(i18n("Upgrade currently installed packages"));
-#else
-    static const KAuth::Action::DetailsMap details{
-        { KAuth::Action::AuthDetail::DetailMessage, i18n("Upgrade currently installed packages") }
-    };
-    upgradeAction.setDetailsV2(details);
-#endif
-    // upgradeAction.addArgument(QLatin1String("onlySimulate"), true);
 
     // run upgrade with elevated privileges
-    KAuth::ExecuteJob *reply = upgradeAction.execute();
+    KAuth::ExecuteJob *reply = ActionFactory::createUpgradeAction();
+    if (!reply) return;
     QObject::connect(reply, &KAuth::ExecuteJob::result,
                      this, &AlpineApkUpdater::handleKAuthUpgradeHelperReply);
 
@@ -211,27 +194,11 @@ int AlpineApkUpdater::updatesCount()
 void AlpineApkUpdater::startCheckForUpdates()
 {
     QtApk::Database *db = m_backend->apkdb();
-    KAuth::Action updateAction(QStringLiteral("org.kde.discover.alpineapkbackend.update"));
-    updateAction.setHelperId(QStringLiteral("org.kde.discover.alpineapkbackend"));
-    if (!updateAction.isValid()) {
-        qCWarning(LOG_ALPINEAPK) << "kauth updateAction is not valid!";
-        return;
-    }
-    updateAction.setTimeout(60 * 1000); // 1 minute
-    // setDetails deprecated since KF 5.68, use setDetailsV2() with DetailsMap.
-#if KAUTH_VERSION < QT_VERSION_CHECK(5, 68, 0)
-    updateAction.setDetails(i18n("Update repositories index"));
-#else
-    static const KAuth::Action::DetailsMap details{
-        { KAuth::Action::AuthDetail::DetailMessage, i18n("Update repositories index") }
-    };
-    updateAction.setDetailsV2(details);
-#endif
-    updateAction.addArgument(QLatin1String("fakeRoot"), db->fakeRoot());
 
     // run updates check with elevated privileges to access
     //     system package manager files
-    KAuth::ExecuteJob *reply = updateAction.execute();
+    KAuth::ExecuteJob *reply = ActionFactory::createUpdateAction(db->fakeRoot());
+    if (!reply) return;
     QObject::connect(reply, &KAuth::ExecuteJob::result,
                      this, &AlpineApkUpdater::handleKAuthUpdateHelperReply);
     // qOverload is needed because of conflict with getter named percent()
@@ -279,8 +246,8 @@ void AlpineApkUpdater::handleKAuthUpgradeHelperReply(KJob *job)
     if (reply->error() == 0) {
         QVariant pkgsV = replyData.value(QLatin1String("changes"));
         bool onlySimulate = replyData.value(QLatin1String("onlySimulate"), false).toBool();
-        qCDebug(LOG_ALPINEAPK) << "KAuth helper upgrade reply received, onlySimulate:" << onlySimulate;
         if (onlySimulate) {
+            qCDebug(LOG_ALPINEAPK) << "KAuth helper upgrade reply received, simulation mode";
             QVector<QtApk::Package> pkgVector = pkgsV.value<QVector<QtApk::Package>>();
             qCDebug(LOG_ALPINEAPK) << "  num changes:" << pkgVector.size();
             for (const QtApk::Package &pkg : pkgVector) {
diff --git a/libdiscover/backends/AlpineApkBackend/CMakeLists.txt b/libdiscover/backends/AlpineApkBackend/CMakeLists.txt
index 9e3bd82c2..7e54cfd9b 100644
--- a/libdiscover/backends/AlpineApkBackend/CMakeLists.txt
+++ b/libdiscover/backends/AlpineApkBackend/CMakeLists.txt
@@ -1,6 +1,8 @@
 find_package(KF5Auth CONFIG REQUIRED)  # Probably should be moved to top CMakeLists
 
 set(alpineapkbackend_SRCS
+    AlpineApkAuthActionFactory.h
+    AlpineApkAuthActionFactory.cpp
     AlpineApkBackend.cpp
     AlpineApkBackend.h
     AlpineApkResource.cpp
-- 
GitLab


From f1e7b2388828eb15739d82ef52e9522d9ac8d036 Mon Sep 17 00:00:00 2001
From: Alexey Min <alexey.min@gmail.com>
Date: Fri, 29 Jan 2021 12:00:56 +0300
Subject: [PATCH 41/59] AlpineApkResource: add AppStream data setter

---
 libdiscover/backends/AlpineApkBackend/AlpineApkResource.cpp | 5 +++++
 libdiscover/backends/AlpineApkBackend/AlpineApkResource.h   | 1 +
 2 files changed, 6 insertions(+)

diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkResource.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkResource.cpp
index a6f4bc0fa..8f493a490 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkResource.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkResource.cpp
@@ -285,6 +285,11 @@ bool AlpineApkResource::hasAppStreamData() const
     return !m_appsC.id().isEmpty();
 }
 
+void AlpineApkResource::setAppStreamData(const AppStream::Component &component)
+{
+    m_appsC = component;
+}
+
 bool AlpineApkResource::canExecute() const
 {
     if (hasAppStreamData()) {
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkResource.h b/libdiscover/backends/AlpineApkBackend/AlpineApkResource.h
index e8948c467..5304a8773 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkResource.h
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkResource.h
@@ -73,6 +73,7 @@ public:
     void setAddons(const AddonList &addons);
     void setAddonInstalled(const QString &addon, bool installed);
     void setAvailableVersion(const QString &av);
+    void setAppStreamData(const AppStream::Component &component);
 
 private:
     bool hasAppStreamData() const;
-- 
GitLab


From aa839425501c9e5d4ab33504197ba8401e2c5367 Mon Sep 17 00:00:00 2001
From: Alexey Min <alexey.min@gmail.com>
Date: Sat, 17 Jul 2021 21:10:57 +0300
Subject: [PATCH 42/59] CMake: AlpineApkBackend: depend on KIOCore, KJobWidgets

Needed for AppStreamDataDownloader
---
 libdiscover/backends/AlpineApkBackend/CMakeLists.txt | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/libdiscover/backends/AlpineApkBackend/CMakeLists.txt b/libdiscover/backends/AlpineApkBackend/CMakeLists.txt
index 7e54cfd9b..0d657c6ab 100644
--- a/libdiscover/backends/AlpineApkBackend/CMakeLists.txt
+++ b/libdiscover/backends/AlpineApkBackend/CMakeLists.txt
@@ -1,4 +1,5 @@
 find_package(KF5Auth CONFIG REQUIRED)  # Probably should be moved to top CMakeLists
+find_package(KF5JobWidgets CONFIG REQUIRED)
 
 set(alpineapkbackend_SRCS
     AlpineApkAuthActionFactory.h
@@ -37,9 +38,11 @@ target_link_libraries(
         Qt5::Core
         Qt5::Widgets
         Qt5::Concurrent
-        KF5::CoreAddons
-        KF5::ConfigCore
         KF5::AuthCore
+        KF5::ConfigCore
+        KF5::CoreAddons
+        KF5::JobWidgets
+        KF5::KIOCore
         Discover::Common
         ApkQt::ApkQt
         AppStreamQt
-- 
GitLab


From d77b2d522027e5f25c68bb76e189b13b8ede819d Mon Sep 17 00:00:00 2001
From: Alexey Min <alexey.min@gmail.com>
Date: Fri, 29 Jan 2021 12:11:51 +0300
Subject: [PATCH 43/59] Add AppstreamDataDownloader class

* Use KIO::get - Simpler and more readable code
  than the one using QNetworkAccessManager.
* use temp files for downloading
* review comments from apol
* use KUiServerJobTracker to show some indication
---
 .../AppstreamDataDownloader.cpp               | 237 ++++++++++++++++++
 .../AppstreamDataDownloader.h                 | 156 ++++++++++++
 .../backends/AlpineApkBackend/CMakeLists.txt  |   2 +
 3 files changed, 395 insertions(+)
 create mode 100644 libdiscover/backends/AlpineApkBackend/AppstreamDataDownloader.cpp
 create mode 100644 libdiscover/backends/AlpineApkBackend/AppstreamDataDownloader.h

diff --git a/libdiscover/backends/AlpineApkBackend/AppstreamDataDownloader.cpp b/libdiscover/backends/AlpineApkBackend/AppstreamDataDownloader.cpp
new file mode 100644
index 000000000..e791b2e02
--- /dev/null
+++ b/libdiscover/backends/AlpineApkBackend/AppstreamDataDownloader.cpp
@@ -0,0 +1,237 @@
+/***************************************************************************
+ *   Copyright © 2020 Alexey Min <alexey.min@gmail.com>                    *
+ *                                                                         *
+ *   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 <http://www.gnu.org/licenses/>. *
+ ***************************************************************************/
+
+#include <QDateTime>
+#include <QDir>
+#include <QFile>
+#include <QFileInfo>
+#include <QJsonArray>
+#include <QJsonDocument>
+#include <QJsonParseError>
+#include <QJsonObject>
+#include <QStandardPaths>
+#include <QUrl>
+
+// KF5
+#include <KIO/TransferJob>
+#include <KUiServerJobTracker>
+
+#include "AppstreamDataDownloader.h"
+#include "alpineapk_backend_logging.h"
+
+namespace DiscoverVersion {
+// contains static QLatin1String version("5.20.5"); definition
+// autogenerated from top CMakeLists.txt
+#include "../../../DiscoverVersion.h"
+}
+
+QString AppstreamDataDownloader::appStreamCacheDir()
+{
+    const QString cachePath = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QLatin1String("/external_appstream_data");
+    // ^^ "~/.cache/discover/external_appstream_data"
+    QDir(cachePath).mkpath(QStringLiteral("."));
+    return cachePath;
+}
+
+AppstreamDataDownloader::AppstreamDataDownloader(QObject *parent)
+    : QObject(parent)
+{
+    m_jobTracker = new KUiServerJobTracker(this);
+}
+
+void AppstreamDataDownloader::setCacheExpirePeriodSecs(qint64 secs)
+{
+    m_cacheExpireSeconds = secs;
+}
+
+void AppstreamDataDownloader::loadUrlsJson(const QString &jsonPath)
+{
+    const QString jsonBaseName = QFileInfo(jsonPath).baseName();
+    QFile jsonFile(jsonPath);
+    if (!jsonFile.open(QIODevice::ReadOnly)) {
+        qCWarning(LOG_ALPINEAPK) << "Failed to open JSON:" << jsonPath << "for reading!";
+        Q_EMIT downloadFinished();
+        return;
+    }
+    const QByteArray jsonBa = jsonFile.readAll();
+    jsonFile.close();
+
+    QJsonParseError jsonError;
+    const QJsonDocument jDoc = QJsonDocument::fromJson(jsonBa, &jsonError);
+    if (jDoc.isNull()) {
+        qCWarning(LOG_ALPINEAPK) << "Failed to parse JSON:" << jsonPath << "!";
+        qCWarning(LOG_ALPINEAPK) << jsonError.errorString();
+        Q_EMIT downloadFinished();
+        return;
+    }
+    // JSON structure:
+    // {
+    //    "urls": [
+    //        "https://...",  "https://...",  "https://..."
+    //    ]
+    // }
+    const QJsonObject rootObj = jDoc.object();
+    const QJsonArray urls = rootObj.value(QLatin1String("urls")).toArray();
+    for (const QJsonValue &urlValue : urls) {
+        const QString url = urlValue.toString();
+        m_urls.append(url);
+        // prefixes are used to avoid name clashes with potential similar
+        //    URL paths from other JSON files. Json file basename is used
+        //    as prefix
+        m_urlPrefixes.insert(url, jsonBaseName);
+    }
+}
+
+QString AppstreamDataDownloader::calcLocalFileSavePath(const QUrl &urlToDownload)
+{
+    // we are adding a prefix here to local file name to avoid possible
+    //    file name clashes with files from other JSONs
+    const QString urlPrefix = m_urlPrefixes.value(urlToDownload.toString(), QString());
+    const QFileInfo urlInfo(urlToDownload.path());
+    const QString localCacheFile = AppstreamDataDownloader::appStreamCacheDir() + QDir::separator() + urlPrefix + QLatin1Char('_') + urlInfo.fileName();
+    // aka "~/.cache/discover/external_appstream_data/urlPrefix_fileName.xml.gz"
+    return localCacheFile;
+}
+
+void AppstreamDataDownloader::start()
+{
+    m_urls.clear();
+    // load json files with appdata URLs configuration
+    const QString path = QStandardPaths::locate(
+        QStandardPaths::GenericDataLocation,
+        QLatin1String("libdiscover/external-appstream-urls"),
+        QStandardPaths::LocateDirectory);
+    if (path.isEmpty()) {
+        qCWarning(LOG_ALPINEAPK) << "external-appstream-urls directory does not exist.";
+        return;
+    }
+
+    QDir jsonsDir(path);
+    // search for all JSON files in that directory and load each one
+    QFileInfoList fileList = jsonsDir.entryInfoList({QStringLiteral("*.json")}, QDir::Files);
+    for (const QFileInfo &fi : fileList) {
+        qCDebug(LOG_ALPINEAPK) << " reading URLs JSON: " << fi.absoluteFilePath();
+        loadUrlsJson(fi.absoluteFilePath());
+    }
+
+    qCDebug(LOG_ALPINEAPK) << "appstream_downloader: urls:" << m_urls;
+
+    // check if download is needed at all, maybe all files are already up to date?
+
+    appStreamCacheDir(); // can create a cache dir if not exists
+
+    const QDateTime dtNow = QDateTime::currentDateTime();
+    m_urlsToDownload.clear();
+    for (const QString &url : m_urls) {
+        const QUrl urlToDownload(url, QUrl::TolerantMode);
+        const QString localCacheFile = calcLocalFileSavePath(urlToDownload);
+        const QFileInfo localFi(localCacheFile);
+        if (localFi.exists()) {
+            int modifiedSecsAgo = localFi.lastModified().secsTo(dtNow);
+            if (modifiedSecsAgo >= m_cacheExpireSeconds) {
+                qCDebug(LOG_ALPINEAPK) << " appstream metadata file: " << localFi.fileName() << " was last modified " << modifiedSecsAgo
+                                       << " seconds ago, need to download";
+                m_urlsToDownload.append(url);
+            }
+        } else {
+            // locally downloaded file does not even exist, we need to download it
+            m_urlsToDownload.append(url);
+            qCDebug(LOG_ALPINEAPK) << " appstream metadata file: " << localFi.fileName()
+                                   << " does not exist, queued for downloading";
+        }
+    }
+
+    if (!m_urlsToDownload.isEmpty()) {
+        // some files are outdated; download is needed
+        qCDebug(LOG_ALPINEAPK) << "appstream_downloader: We will need to download "
+                               << m_urlsToDownload.size() << " file(s)";
+
+        const QString discoverVersion(QStringLiteral("plasma-discover %1").arg(DiscoverVersion::version));
+
+        m_jobs.clear();
+        for (const QString &sUrl : qAsConst(m_urlsToDownload)) {
+            const QUrl url(sUrl, QUrl::TolerantMode);
+            KIO::TransferJob *job = KIO::get(url, KIO::LoadType::Reload, KIO::JobFlag::HideProgressInfo);
+            job->addMetaData(QLatin1String("UserAgent"), discoverVersion);
+
+            m_jobTracker->registerJob(job);
+
+            QObject::connect(job, &KJob::result, this, &AppstreamDataDownloader::onJobResult);
+            QObject::connect(job, &KIO::TransferJob::data, this, &AppstreamDataDownloader::onJobData);
+
+            m_jobs.push_back(job);
+        }
+    } else {
+        // no need to download anything
+        qCDebug(LOG_ALPINEAPK) << "appstream_downloader: All appstream data files "
+                                  "are up to date, not downloading anything";
+        Q_EMIT downloadFinished();
+        return;
+    }
+}
+
+void AppstreamDataDownloader::onJobData(KIO::Job *job, const QByteArray &data)
+{
+    KIO::TransferJob *tjob = qobject_cast<KIO::TransferJob *>(job);
+    if (data.size() < 1) {
+        return;
+    }
+    // while downloading, save data to temporary file
+    const QString filePath = calcLocalFileSavePath(tjob->url()) + QLatin1String(".tmp");
+    QFile fout(filePath);
+    if (!fout.open(QIODevice::WriteOnly | QIODevice::Append)) {
+        qCWarning(LOG_ALPINEAPK) << "appstream_downloader: failed to write: " << filePath;
+        return;
+    }
+    fout.write(data);
+    fout.close();
+}
+
+void AppstreamDataDownloader::onJobResult(KJob *job)
+{
+    KIO::TransferJob *tjob = qobject_cast<KIO::TransferJob *>(job);
+    m_jobs.removeOne(tjob);
+    m_jobTracker->unregisterJob(tjob);
+
+    const QString localCacheFile = calcLocalFileSavePath(tjob->url());
+    const QString localCacheFileTmp = localCacheFile + QLatin1String(".tmp");
+
+    if (tjob->error()) {
+        qCWarning(LOG_ALPINEAPK) << "appstream_downloader: failed to download: " << tjob->url();
+        qCWarning(LOG_ALPINEAPK) << tjob->errorString();
+        // error cleanup - remove temp file
+        QFile::remove(localCacheFileTmp);
+    } else {
+        // success - rename tmp file to real
+        QFile::remove(localCacheFile); // just in case, or QFile::rename() will fail
+        QFile::rename(localCacheFileTmp, localCacheFile);
+        m_cacheWasUpdated = true;
+        qCDebug(LOG_ALPINEAPK) << "appstream_downloader: saved: " << localCacheFile;
+    }
+
+    tjob->deleteLater();
+
+    qCDebug(LOG_ALPINEAPK).nospace() << "appstream_downloader: " << localCacheFile << " request finished (" << m_jobs.size() << " left)";
+
+    if (m_jobs.isEmpty()) {
+        qCDebug(LOG_ALPINEAPK) << "appstream_downloader: all downloads have finished!";
+        Q_EMIT downloadFinished();
+    }
+}
diff --git a/libdiscover/backends/AlpineApkBackend/AppstreamDataDownloader.h b/libdiscover/backends/AlpineApkBackend/AppstreamDataDownloader.h
new file mode 100644
index 000000000..1b3cc6e28
--- /dev/null
+++ b/libdiscover/backends/AlpineApkBackend/AppstreamDataDownloader.h
@@ -0,0 +1,156 @@
+/***************************************************************************
+ *   Copyright © 2020 Alexey Min <alexey.min@gmail.com>                    *
+ *                                                                         *
+ *   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 <http://www.gnu.org/licenses/>. *
+ ***************************************************************************/
+
+#ifndef AlpineAppstreamDataDownloader_H
+#define AlpineAppstreamDataDownloader_H
+
+#include <QByteArray>
+#include <QHash>
+#include <QList>
+#include <QObject>
+#include <QString>
+#include <QUrl>
+
+class KJob;
+class KUiServerJobTracker;
+namespace KIO
+{
+class Job;
+class TransferJob;
+}
+
+/**
+ * @class AppstreamDataDownloader
+ *
+ * @details The job of this class is to download appstream data
+ * gzipped XMLs from some web server.
+ *
+ * Some distros (for example, Alpine Linux) do not provide
+ * appstream data as installable package, instead they host it
+ * on the internet (at https://appstream.alpinelinux.org) and
+ * software center app has to download them on its own.
+ *
+ * Those files are not very large (from few kilobytes to couple
+ * of megabytes) but we still need them.
+ *
+ * URLs to download archives from are stored in JSON files in
+ * /usr/share/libdiscover/external-appstream-urls/ directory
+ * (QStandardPaths::GenericDataLocation/libdiscover/external-appstream-urls
+ * from C++/Qt code, ${DATA_INSTALL_DIR}/libdiscover/external-appstream-urls
+ * from cmake).
+ *
+ * JSON file format:
+ * ---------------------
+ * {
+ *      "urls": [
+ *          "https://url1",
+ *          "https://url2",
+ *          ...
+ *      ]
+ * }
+ * ---------------------
+ * These JSON files, if needed, should be provided by the linux
+ * distribution.
+ *
+ * This class can load any amount of those JSON files,
+ * fetch URLs from them and download all files pointed by
+ * those URLs to discover's cache directory:
+ * "~/.cache/discover/external_appstream_data" (aka
+ * QStandardPaths::CacheLocation). Use
+ * getAppStreamCacheDir() to get this path.
+ * If files are already present in cache and not outdated,
+ * they are not downloaded again. Default cache expiration
+ * time is 7 days and can be tweaked using
+ * setCacheExpirePeriodSecs().
+ */
+class AppstreamDataDownloader: public QObject
+{
+    Q_OBJECT
+public:
+    explicit AppstreamDataDownloader(QObject *parent = nullptr);
+
+    /**
+     * Use return value of this function to add extra metadata
+     * directories to AppStream loader, in case of AppStreamQt:
+     * AppStream::Pool::addMetadataLocation().
+     * This method creates a cache dir if it does not exist.
+     * @return directory where downloaded files are stored.
+     */
+    static QString appStreamCacheDir();
+
+    /**
+     * Call this after receiving downloadFinished() signal to
+     * test if there actually was something new downloaded.
+     * @return true, if new files were actually downloaded, or
+     *         false if files already present in cache are up to date.
+     */
+    bool cacheWasUpdated() const { return m_cacheWasUpdated; }
+
+    /**
+     * @return cache expire timeout in seconds
+     */
+    qint64 cacheExpirePeriodSecs() const
+    {
+        return m_cacheExpireSeconds;
+    }
+
+    /**
+     * Set cache expiration timeout.
+     * @param secs - new cache expiration timeout, in seconds.
+     */
+    void setCacheExpirePeriodSecs(qint64 secs);
+
+public Q_SLOTS:
+    /**
+     * Start the asynchronous download job.
+     * downloadFinished() signal will be emitted when everything is done.
+     * start() may finish immediately if all cached files are
+     * up to date and no downloads are needed.
+     */
+    void start();
+
+Q_SIGNALS:
+    /**
+     * This signal is emitted when download job is finished.
+     * To check if there were actual downloads performed, call
+     * cacheWasUpdated().
+     */
+    void downloadFinished();
+
+private:
+    QString calcLocalFileSavePath(const QUrl &urlTodownload);
+    void loadUrlsJson(const QString &path);
+
+private Q_SLOTS:
+    void onJobData(KIO::Job *job, const QByteArray &data);
+    void onJobResult(KJob *job);
+
+protected:
+    qint64 m_cacheExpireSeconds = 7 * 24 * 3600;
+    QStringList m_urls;
+    QStringList m_urlsToDownload;
+    QHash<QString, QString> m_urlPrefixes;
+    bool m_cacheWasUpdated = false;
+
+    QList<KIO::TransferJob *> m_jobs;
+    KUiServerJobTracker *m_jobTracker;
+};
+
+#endif
diff --git a/libdiscover/backends/AlpineApkBackend/CMakeLists.txt b/libdiscover/backends/AlpineApkBackend/CMakeLists.txt
index 0d657c6ab..08c679a01 100644
--- a/libdiscover/backends/AlpineApkBackend/CMakeLists.txt
+++ b/libdiscover/backends/AlpineApkBackend/CMakeLists.txt
@@ -16,6 +16,8 @@ set(alpineapkbackend_SRCS
     AlpineApkUpdater.h
     AlpineApkTransaction.cpp
     AlpineApkTransaction.h
+    AppstreamDataDownloader.h
+    AppstreamDataDownloader.cpp
 )
 
 ecm_qt_declare_logging_category(
-- 
GitLab


From 506bcc57d67616a8bbe606736bbb54c1314b1655 Mon Sep 17 00:00:00 2001
From: Alexey Min <alexey.min@gmail.com>
Date: Fri, 29 Jan 2021 12:17:48 +0300
Subject: [PATCH 44/59] AlpineApkBackend: use AppstreamDataDownloader

Download AppStream data from external URLs after
loading resources initially. Reload AppStream
metadata after download has finished.

Refactor resource loading process into several
subroutines for easier understanding and code sharing.
---
 .../AlpineApkBackend/AlpineApkBackend.cpp     | 166 +++++++++++++-----
 .../AlpineApkBackend/AlpineApkBackend.h       |  10 +-
 2 files changed, 131 insertions(+), 45 deletions(-)

diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp
index 8ddcc8e9f..3641142eb 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp
@@ -24,6 +24,7 @@
 #include "AlpineApkTransaction.h"
 #include "AlpineApkSourcesBackend.h"
 #include "AlpineApkUpdater.h"
+#include "AppstreamDataDownloader.h"
 #include "alpineapk_backend_logging.h"  // generated by ECM
 
 #include "resources/SourcesModel.h"
@@ -85,16 +86,19 @@ AlpineApkBackend::AlpineApkBackend(QObject *parent)
     SourcesModel::global()->addSourcesBackend(new AlpineApkSourcesBackend(this));
 }
 
-void AlpineApkBackend::loadResources()
+// this fills in m_appStreamComponents
+void AlpineApkBackend::loadAppStreamComponents()
 {
-    qCDebug(LOG_ALPINEAPK) << "backend: loading AppStream metadata...";
-
     AppStream::Pool *appStreamPool = new AppStream::Pool();
     appStreamPool->setFlags(AppStream::Pool::FlagReadCollection |
                             AppStream::Pool::FlagReadMetainfo |
                             AppStream::Pool::FlagReadDesktopFiles);
     appStreamPool->setCacheFlags(AppStream::Pool::CacheFlagUseUser |
                                  AppStream::Pool::CacheFlagUseSystem);
+
+    // hey hey! cool stuff
+    appStreamPool->addMetadataLocation(AppstreamDataDownloader::appStreamCacheDir());
+
     if (!appStreamPool->load()) {
         qCWarning(LOG_ALPINEAPK) << "backend: Failed to load appstream data:"
                                  << appStreamPool->lastError();
@@ -103,36 +107,32 @@ void AlpineApkBackend::loadResources()
         qCDebug(LOG_ALPINEAPK) << "backend: loaded AppStream metadata OK:"
                                << m_appStreamComponents.size() << "components.";
         // collect all categories present in appstream metadata
-//        QSet<QString> collectedCategories;
-//        for (const AppStream::Component &component : m_appStreamComponents) {
-//            const QStringList cats = component.categories();
-//            for (const QString &cat : cats) {
-//                collectedCategories.insert(cat);
-//            }
-//        }
-//        for (const QString &cat : collectedCategories) {
-//            qCDebug(LOG_ALPINEAPK) << "    collected category: " << cat;
-//            m_collectedCategories << cat;
-//        }
+        // QSet<QString> collectedCategories;
+        // for (const AppStream::Component &component : m_appStreamComponents) {
+        //     const QStringList cats = component.categories();
+        //     for (const QString &cat : cats) {
+        //         collectedCategories.insert(cat);
+        //     }
+        // }
+        // for (const QString &cat : collectedCategories) {
+        //     qCDebug(LOG_ALPINEAPK) << "    collected category: " << cat;
+        //     m_collectedCategories << cat;
+        // }
     }
     delete appStreamPool;
-    appStreamPool = nullptr;
-
-    qCDebug(LOG_ALPINEAPK) << "backend: populating resources...";
-    emit this->passiveMessage(i18n("Loading, please wait..."));
-
-    if (m_apkdb.open(QtApk::QTAPK_OPENF_READONLY)) {
-        m_availablePackages = m_apkdb.getAvailablePackages();
-        m_installedPackages = m_apkdb.getInstalledPackages();
-        m_apkdb.close();
-    }
+}
 
+// this uses m_appStreamComponents and m_availablePackages
+//      to fill in m_resourcesAppstreamData
+void AlpineApkBackend::parseAppStreamMetadata()
+{
     if (m_availablePackages.size() > 0) {
-        for (const QtApk::Package &pkg: m_availablePackages) {
+
+        for (const QtApk::Package &pkg: qAsConst(m_availablePackages)) {
 
             // try to find appstream data for this package
             AppStream::Component appstreamComponent;
-            for (const auto& appsC : m_appStreamComponents) {
+            for (const auto& appsC : qAsConst(m_appStreamComponents)) {
                 // find result which package name is exactly the one we want
                 if (appsC.packageNames().contains(pkg.name)) {
                     // workaround for kate (Kate Sessions is found first, but
@@ -153,11 +153,6 @@ void AlpineApkBackend::loadResources()
             m_resourcesAppstreamData.insert(key, appstreamComponent);
         }
     }
-
-    qCDebug(LOG_ALPINEAPK) << "  available" << m_availablePackages.size()
-                           << "packages";
-    qCDebug(LOG_ALPINEAPK) << "  installed" << m_installedPackages.size()
-                           << "packages";
 }
 
 static AbstractResource::Type toDiscoverResourceType(const AppStream::Component &component)
@@ -180,28 +175,81 @@ static AbstractResource::Type toDiscoverResourceType(const AppStream::Component
     return resType;
 }
 
-void AlpineApkBackend::onLoadResourcesFinished()
+void AlpineApkBackend::fillResourcesAndApplyAppStreamData()
 {
-    qCDebug(LOG_ALPINEAPK) << "backend: appstream data loaded and sorted; fill in resources";
-
+    // now the tricky part - we need to reapply appstream component metadata to each resource
     if (m_availablePackages.size() > 0) {
         for (const QtApk::Package &pkg: m_availablePackages) {
             const QString key = pkg.name.toLower();
 
-            AppStream::Component &appstreamComponent = m_resourcesAppstreamData[key];
-            const AbstractResource::Type resType = toDiscoverResourceType(appstreamComponent);
+            AppStream::Component &appsComponent = m_resourcesAppstreamData[key];
+            const AbstractResource::Type resType = toDiscoverResourceType(appsComponent);
+
+            AlpineApkResource *res = m_resources.value(key, nullptr);
+            if (res == nullptr) {
+                // during first run of this function during initial load
+                //   m_resources hash is empty, so we need to insert new items
+                res = new AlpineApkResource(pkg, appsComponent, resType, this);
+                res->setCategoryName(QStringLiteral("alpine_packages"));
+                res->setOriginSource(QStringLiteral("apk"));
+                res->setSection(QStringLiteral("dummy"));
+                m_resources.insert(key, res);
+                QObject::connect(res, &AlpineApkResource::stateChanged,
+                                 this, &AlpineApkBackend::updatesCountChanged);
+            } else {
+                // this is not an initial run, just update existing resource
+                res->setAppStreamData(appsComponent);
+            }
+        }
+    }
+}
 
-            AlpineApkResource *res = new AlpineApkResource(pkg, appstreamComponent, resType, this);
-            res->setCategoryName(QStringLiteral("alpine_packages"));
-            res->setOriginSource(QStringLiteral("apk"));
-            res->setSection(QStringLiteral("dummy"));
+void AlpineApkBackend::reloadAppStreamMetadata()
+{
+    // mark us as "Loading..."
+    m_fetching = true;
+    emit fetchingChanged();
 
-            m_resources.insert(key, res);
-            QObject::connect(res, &AlpineApkResource::stateChanged,
-                             this, &AlpineApkBackend::updatesCountChanged);
-        }
+    loadAppStreamComponents();
+    parseAppStreamMetadata();
+    fillResourcesAndApplyAppStreamData();
+
+    // mark us as "done loading"
+    m_fetching = false;
+    emit fetchingChanged();
+}
+
+// this function is executed in the background thread
+void AlpineApkBackend::loadResources()
+{
+    Q_EMIT this->passiveMessage(i18n("Loading, please wait..."));
+
+    qCDebug(LOG_ALPINEAPK) << "backend: loading AppStream metadata...";
+
+    loadAppStreamComponents();
+
+    qCDebug(LOG_ALPINEAPK) << "backend: populating resources...";
+
+    if (m_apkdb.open(QtApk::QTAPK_OPENF_READONLY)) {
+        m_availablePackages = m_apkdb.getAvailablePackages();
+        m_installedPackages = m_apkdb.getInstalledPackages();
+        m_apkdb.close();
     }
 
+    parseAppStreamMetadata();
+
+    qCDebug(LOG_ALPINEAPK) << "  available" << m_availablePackages.size()
+                           << "packages";
+    qCDebug(LOG_ALPINEAPK) << "  installed" << m_installedPackages.size()
+                           << "packages";
+}
+
+void AlpineApkBackend::onLoadResourcesFinished()
+{
+    qCDebug(LOG_ALPINEAPK) << "backend: appstream data loaded and sorted; fill in resources";
+
+    fillResourcesAndApplyAppStreamData();
+
     // update "installed/not installed" state
     if (m_installedPackages.size() > 0) {
         for (const QtApk::Package &pkg: m_installedPackages) {
@@ -221,6 +269,36 @@ void AlpineApkBackend::onLoadResourcesFinished()
 
     // schedule check for updates 1 sec after we've loaded all resources
     QTimer::singleShot(1000, this, &AlpineApkBackend::checkForUpdates);
+
+    // AppStream appdata downloader can download updated metadata files
+    //    in a background thread. When potential download is finished,
+    //    appstream data will be reloaded.
+    m_appstreamDownloader = new AppstreamDataDownloader(nullptr);
+    QObject::connect(m_appstreamDownloader, &AppstreamDataDownloader::downloadFinished,
+                     this, &AlpineApkBackend::onAppstreamDataDownloaded, Qt::QueuedConnection);
+    m_appstreamDownloader->start();
+}
+
+void AlpineApkBackend::onAppstreamDataDownloaded()
+{
+    if (m_appstreamDownloader) {
+        if (m_appstreamDownloader->cacheWasUpdated()) {
+            // it means we need to reload previously loaded appstream metadata
+            // m_fetching is true if loadResources() is still executing
+            //   in a background thread
+            if (!m_fetching) {
+                qCDebug(LOG_ALPINEAPK) << "AppStream metadata was updated; re-applying it to all resources";
+                reloadAppStreamMetadata();
+            } else {
+                qCWarning(LOG_ALPINEAPK) << "AppStream metadata was updated, but cannot apply it: still fetching";
+                // it should not really happen, but if it happens,
+                //   then downloaded metadata will be used on the next
+                //   discover launch anyway.
+            }
+        }
+        delete m_appstreamDownloader;
+        m_appstreamDownloader = nullptr;
+    }
 }
 
 QVector<Category *> AlpineApkBackend::category() const
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.h b/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.h
index 2ec8b00b6..07b6b7be8 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.h
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.h
@@ -22,8 +22,9 @@
 #define AlpineApkBackend_H
 
 #include <resources/AbstractResourcesBackend.h>
-#include <QVariantList>
+
 #include <QFutureWatcher>
+#include <QVariantList>
 
 #include <QtApk>
 
@@ -32,6 +33,7 @@
 class AlpineApkReviewsBackend;
 class AlpineApkUpdater;
 class AlpineApkResource;
+class AppstreamDataDownloader;
 class KJob;
 class QTimer;
 
@@ -66,8 +68,13 @@ public Q_SLOTS:
 
 private Q_SLOTS:
     void finishCheckForUpdates();
+    void loadAppStreamComponents();
+    void parseAppStreamMetadata();
+    void reloadAppStreamMetadata();
+    void fillResourcesAndApplyAppStreamData();
     void loadResources();
     void onLoadResourcesFinished();
+    void onAppstreamDataDownloaded();
 
 public:
     QtApk::Database *apkdb() { return &m_apkdb; }
@@ -86,6 +93,7 @@ private:
     QList<AppStream::Component> m_appStreamComponents;
     // QVector<QString> m_collectedCategories;
     QFutureWatcher<void> m_voidFutureWatcher;
+    AppstreamDataDownloader *m_appstreamDownloader;
 };
 
 #endif // AlpineApkBackend_H
-- 
GitLab


From ea0f129c606e43734b5c8ce04b0d666a4d8ce842 Mon Sep 17 00:00:00 2001
From: Alexey Min <alexey.min@gmail.com>
Date: Mon, 1 Feb 2021 14:24:25 +0300
Subject: [PATCH 45/59] AlpineApkUpdater: fix progress reporting for system
 upgrade

Connect to progress updates from helper and implement
AlpineApkUpdater::progress() getter function.
---
 .../AlpineApkBackend/AlpineApkUpdater.cpp     | 23 +++++++++++++++----
 .../AlpineApkBackend/AlpineApkUpdater.h       |  3 ++-
 2 files changed, 21 insertions(+), 5 deletions(-)

diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp
index 0bbbce1c0..30399369b 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp
@@ -91,8 +91,7 @@ bool AlpineApkUpdater::hasUpdates() const
 
 qreal AlpineApkUpdater::progress() const
 {
-    qCDebug(LOG_ALPINEAPK) << Q_FUNC_INFO;
-    return 0.0;
+    return m_upgradeProgress;
 }
 
 void AlpineApkUpdater::removeResources(const QList<AbstractResource *> &apps)
@@ -171,10 +170,15 @@ void AlpineApkUpdater::start()
     // run upgrade with elevated privileges
     KAuth::ExecuteJob *reply = ActionFactory::createUpgradeAction();
     if (!reply) return;
+
     QObject::connect(reply, &KAuth::ExecuteJob::result,
                      this, &AlpineApkUpdater::handleKAuthUpgradeHelperReply);
+    // qOverload is needed because of conflict with getter named percent()
+    QObject::connect(reply, QOverload<KJob *, unsigned long>::of(&KAuth::ExecuteJob::percent),
+                     this, &AlpineApkUpdater::handleKAuthUpgradeHelperProgress);
 
     m_progressing = true;
+    m_upgradeProgress = 0.0;
     Q_EMIT progressingChanged(m_progressing);
 
     reply->start();
@@ -239,6 +243,17 @@ void AlpineApkUpdater::handleKAuthUpdateHelperProgress(KJob *job, unsigned long
     Q_EMIT progressChanged(static_cast<qreal>(percent));
 }
 
+void AlpineApkUpdater::handleKAuthUpgradeHelperProgress(KJob *job, unsigned long percent)
+{
+    Q_UNUSED(job)
+    qCDebug(LOG_ALPINEAPK) << "    upgrade progress: " << percent;
+    qreal newProgress = static_cast<qreal>(percent);
+    if (newProgress != m_upgradeProgress) {
+        m_upgradeProgress = newProgress;
+        Q_EMIT progressChanged(m_upgradeProgress);
+    }
+}
+
 void AlpineApkUpdater::handleKAuthUpgradeHelperReply(KJob *job)
 {
     KAuth::ExecuteJob *reply = static_cast<KAuth::ExecuteJob *>(job);
@@ -258,8 +273,8 @@ void AlpineApkUpdater::handleKAuthUpgradeHelperReply(KJob *job)
         handleKAuthHelperError(reply, replyData);
     }
 
-    // we are not in the state "Fetching updates" now, update UI
-    Q_EMIT checkForUpdatesFinished();
+    m_progressing = false;
+    Q_EMIT progressingChanged(m_progressing);
 }
 
 void AlpineApkUpdater::handleKAuthHelperError(
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.h b/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.h
index 0ee2fcb47..6ca3ce073 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.h
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.h
@@ -159,6 +159,7 @@ public Q_SLOTS:
     void handleKAuthUpdateHelperProgress(KJob *job, unsigned long percent);
     // upgrade
     void handleKAuthUpgradeHelperReply(KJob *job);
+    void handleKAuthUpgradeHelperProgress(KJob *job, unsigned long percent);
 
     //void transactionRemoved(Transaction* t);
     //void cleanup();
@@ -186,7 +187,7 @@ private:
 //    QSet<AbstractResource*> m_upgradeable;
 //    QSet<AbstractResource*> m_pendingResources;
     bool m_progressing = false;
-//    qreal m_progress;
+    qreal m_upgradeProgress = 0.0;
 //    QDateTime m_lastUpdate;
 //    QTimer m_timer;
 //    bool m_canCancel = false;
-- 
GitLab


From b3002d652aaad5980938bdda799700c543e3e40c Mon Sep 17 00:00:00 2001
From: Alexey Min <alexey.min@gmail.com>
Date: Mon, 1 Feb 2021 14:26:29 +0300
Subject: [PATCH 46/59] AlpineApkUpdater: remove useless debug prints and
 comments

---
 .../backends/AlpineApkBackend/AlpineApkUpdater.cpp        | 8 --------
 1 file changed, 8 deletions(-)

diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp
index 30399369b..14df959cd 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp
@@ -40,8 +40,6 @@ AlpineApkUpdater::AlpineApkUpdater(AbstractResourcesBackend *parent)
 
 void AlpineApkUpdater::prepare()
 {
-    qCDebug(LOG_ALPINEAPK) << Q_FUNC_INFO;
-
     QtApk::Database *db = m_backend->apkdb();
 
     if (db->isOpen()) {
@@ -85,7 +83,6 @@ void AlpineApkUpdater::prepare()
 
 bool AlpineApkUpdater::hasUpdates() const
 {
-    // qCDebug(LOG_ALPINEAPK) << Q_FUNC_INFO << m_updatesCount;
     return (m_updatesCount > 0);
 }
 
@@ -96,22 +93,18 @@ qreal AlpineApkUpdater::progress() const
 
 void AlpineApkUpdater::removeResources(const QList<AbstractResource *> &apps)
 {
-    // qCDebug(LOG_ALPINEAPK) << Q_FUNC_INFO;
     const QSet<AbstractResource *> checkSet = kToSet(apps);
     m_markedToUpdate -= checkSet;
 }
 
 void AlpineApkUpdater::addResources(const QList<AbstractResource *> &apps)
 {
-    //Q_UNUSED(apps)
-    //qCDebug(LOG_ALPINEAPK) << Q_FUNC_INFO;
     const QSet<AbstractResource *> checkSet = kToSet(apps);
     m_markedToUpdate += checkSet;
 }
 
 QList<AbstractResource *> AlpineApkUpdater::toUpdate() const
 {
-    // qCDebug(LOG_ALPINEAPK) << Q_FUNC_INFO;
     return m_allUpdateable.values();
 }
 
@@ -191,7 +184,6 @@ void AlpineApkUpdater::proceed()
 
 int AlpineApkUpdater::updatesCount()
 {
-    // qDebug(LOG_ALPINEAPK) << Q_FUNC_INFO << m_updatesCount;
     return m_updatesCount;
 }
 
-- 
GitLab


From c1f1aa832c229fad93aa16644805919606c9a14a Mon Sep 17 00:00:00 2001
From: Alexey Min <alexey.min@gmail.com>
Date: Wed, 24 Feb 2021 14:51:27 +0300
Subject: [PATCH 47/59] AlpineApkSourcesBackend: port away from QAction

follow-up to 4bf3f59adb82e2ef38df98073ee39e6d20e56921
fixed after b5e913d2231a3d899b902a015bfa6be04524d8a3
---
 .../AlpineApkBackend/AlpineApkSourcesBackend.cpp    | 13 ++++++-------
 .../AlpineApkBackend/AlpineApkSourcesBackend.h      |  9 ++++++---
 2 files changed, 12 insertions(+), 10 deletions(-)

diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkSourcesBackend.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkSourcesBackend.cpp
index a126483a9..c10ebdcb5 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkSourcesBackend.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkSourcesBackend.cpp
@@ -18,6 +18,8 @@
  *   along with this program.  If not, see <http://www.gnu.org/licenses/>. *
  ***************************************************************************/
 
+#include "resources/DiscoverAction.h"
+
 #include "AlpineApkSourcesBackend.h"
 #include "AlpineApkAuthActionFactory.h"
 #include "alpineapk_backend_logging.h"  // generated by ECM
@@ -36,16 +38,13 @@
 AlpineApkSourcesBackend::AlpineApkSourcesBackend(AbstractResourcesBackend *parent)
     : AbstractSourcesBackend(parent)
     , m_sourcesModel(new QStandardItemModel(this))
-    , m_refreshAction(new QAction(QIcon::fromTheme(QStringLiteral("view-refresh")),
-                                  QStringLiteral("Reload"), this))
-    , m_saveAction(new QAction(QIcon::fromTheme(QStringLiteral("document-save")),
-                                  QStringLiteral("Save"), this))
-    // ^^ unfortunately QML side ignores icons for custom actions
+    , m_refreshAction(new DiscoverAction(QStringLiteral("view-refresh"), QStringLiteral("Reload"), this))
+    , m_saveAction(new DiscoverAction(QStringLiteral("document-save"), QStringLiteral("Save"), this))
 {
     loadSources();
-    QObject::connect(m_refreshAction, &QAction::triggered,
+    QObject::connect(m_refreshAction, &DiscoverAction::triggered,
                      this, &AlpineApkSourcesBackend::loadSources);
-    QObject::connect(m_saveAction, &QAction::triggered,
+    QObject::connect(m_saveAction, &DiscoverAction::triggered,
                      this, &AlpineApkSourcesBackend::saveSources);
     // track enabling/disabling repo source
     QObject::connect(m_sourcesModel, &QStandardItemModel::itemChanged,
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkSourcesBackend.h b/libdiscover/backends/AlpineApkBackend/AlpineApkSourcesBackend.h
index eacda22dc..c655d3d16 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkSourcesBackend.h
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkSourcesBackend.h
@@ -21,11 +21,14 @@
 #ifndef ALPINEAPKSOURCESBACKEND_H
 #define ALPINEAPKSOURCESBACKEND_H
 
-#include <resources/AbstractSourcesBackend.h>
+#include "resources/AbstractSourcesBackend.h"
+
 #include <QStandardItemModel>
 
 #include <QtApkRepository.h>
 
+class DiscoverAction;
+
 class AlpineApkSourcesBackend : public AbstractSourcesBackend
 {
 public:
@@ -49,8 +52,8 @@ private:
     void onItemChanged(QStandardItem* item);
 
     QStandardItemModel *m_sourcesModel = nullptr;
-    QAction *m_refreshAction = nullptr;
-    QAction *m_saveAction = nullptr;
+    DiscoverAction *m_refreshAction = nullptr;
+    DiscoverAction *m_saveAction = nullptr;
     QVector<QtApk::Repository> m_repos;
 };
 
-- 
GitLab


From 168a7d43b66199e1f244066101e5f779ca3ea173 Mon Sep 17 00:00:00 2001
From: Alexey Min <alexey.min@gmail.com>
Date: Thu, 25 Feb 2021 07:39:50 +0300
Subject: [PATCH 48/59] AlpineApkBackend: Use shorter backend name: it's shown
 in Install button

See https://invent.kde.org/plasma/discover/-/commit/
ce89d530de3414f25bb4b48a1933a52bf37be922
---
 libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp
index 3641142eb..88b37cf0e 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp
@@ -485,7 +485,7 @@ void AlpineApkBackend::finishCheckForUpdates()
 
 QString AlpineApkBackend::displayName() const
 {
-    return i18nc("Backend plugin display name", "Alpine APK backend");
+    return i18nc("Backend plugin display name", "Alpine APK");
 }
 
 bool AlpineApkBackend::hasApplications() const
-- 
GitLab


From 5ff45db851d8f451a66390f1301362c15e5e1099 Mon Sep 17 00:00:00 2001
From: Alexey Min <alexey.min@gmail.com>
Date: Thu, 25 Feb 2021 07:40:41 +0300
Subject: [PATCH 49/59] AlpineApkResource: use Alpine Linux logo as backend
 icon

---
 libdiscover/backends/AlpineApkBackend/AlpineApkResource.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkResource.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkResource.cpp
index 8f493a490..459347061 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkResource.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkResource.cpp
@@ -325,7 +325,7 @@ QString AlpineApkResource::author() const
 
 QString AlpineApkResource::sourceIcon() const
 {
-    return QStringLiteral("player-time");
+    return QStringLiteral("alpine-linux-logo-icon");
 }
 
 QDate AlpineApkResource::releaseDate() const
-- 
GitLab


From 31da99e7139d03942cac187c30e1ef34979cf760 Mon Sep 17 00:00:00 2001
From: Alexey Minnekhanov <alexeymin@postmarketos.org>
Date: Sat, 15 May 2021 05:52:54 +0300
Subject: [PATCH 50/59] KAuth helper: don't ask for password

Allow everyone to install/remove/upgrade packages without asking
for password. Polkit-kde auth dialog is not mobile friendly.

Fixes https://gitlab.com/postmarketOS/pmaports/-/issues/1036
---
 .../org.kde.discover.alpineapkbackend.actions                 | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/libdiscover/backends/AlpineApkBackend/org.kde.discover.alpineapkbackend.actions b/libdiscover/backends/AlpineApkBackend/org.kde.discover.alpineapkbackend.actions
index c9bb5f9f2..f9db78dbb 100644
--- a/libdiscover/backends/AlpineApkBackend/org.kde.discover.alpineapkbackend.actions
+++ b/libdiscover/backends/AlpineApkBackend/org.kde.discover.alpineapkbackend.actions
@@ -1,5 +1,5 @@
 [org.kde.discover.alpineapkbackend.pkgmgmt]
 Name=Package management
-Description=Install or remove packages, upgrade system
-Policy=auth_admin
+Description=Install/remove/upgrade packages
+Policy=yes
 Persistence=session
-- 
GitLab


From 97c9203885e942220886120f9cfec9ea4e3f69d7 Mon Sep 17 00:00:00 2001
From: Alexey Minnekhanov <alexeymin@postmarketos.org>
Date: Sun, 20 Jun 2021 22:58:29 +0300
Subject: [PATCH 51/59] HACK: AlpineApkResource: Workaround broken Alpine
 metadata

thumbnails from Alpine metadata are unusable
---
 .../AlpineApkBackend/AlpineApkResource.cpp       | 16 +++++++++++++++-
 1 file changed, 15 insertions(+), 1 deletion(-)

diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkResource.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkResource.cpp
index 459347061..d7ac49012 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkResource.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkResource.cpp
@@ -220,7 +220,21 @@ void AlpineApkResource::fetchChangelog()
 void AlpineApkResource::fetchScreenshots()
 {
     if (hasAppStreamData()) {
-        const QPair<QList<QUrl>, QList<QUrl> > sc = AppStreamUtils::fetchScreenshots(m_appsC);
+        /*const*/ QPair<QList<QUrl>, QList<QUrl>> sc = AppStreamUtils::fetchScreenshots(m_appsC);
+        if (sc.first.size() > 0) {
+            const QUrl &url = sc.first.first();
+            if (url.isRelative()) {
+                // This is a hack to fix broken Alpine Linux appstream
+                // metadata: somehow generated thumbnails URLs are relative
+                // and always fail to load (not present in metadata at all)
+                // We work around this by using full url, which is veyr not
+                // good - they're fetched by http://...
+                // It would be great if thumbnails were hosted locally.
+                if (sc.second.size() > 0) {
+                    sc.first = sc.second;
+                }
+            }
+        }
         Q_EMIT screenshotsFetched(sc.first, sc.second);
     }
 }
-- 
GitLab


From a5a6c92ae2bcc6f78a7d080ddbd0a105b0937d11 Mon Sep 17 00:00:00 2001
From: Alexey Min <alexey.min@gmail.com>
Date: Sat, 17 Jul 2021 14:10:08 +0300
Subject: [PATCH 52/59] AlpineApk{Updater,Transaction}: Fix deprecation warn
 with KF5>=5.80

Use percentChanged() signal instead of ambiguous overloaded percent()
signal.
---
 .../AlpineApkBackend/AlpineApkTransaction.cpp         |  5 +++++
 .../backends/AlpineApkBackend/AlpineApkUpdater.cpp    | 11 +++++++++++
 2 files changed, 16 insertions(+)

diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkTransaction.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkTransaction.cpp
index 26bf1bd0d..0f346e843 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkTransaction.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkTransaction.cpp
@@ -29,6 +29,7 @@
 #include <QTimer>
 
 // KF5
+#include <kcoreaddons_version.h>
 #include <KAuthExecuteJob>
 #include <KLocalizedString>
 
@@ -95,7 +96,11 @@ void AlpineApkTransaction::startTransaction()
     });
 
     // get progress reports for this job
+#if KCOREADDONS_VERSION < QT_VERSION_CHECK(5,80,0)
     QObject::connect(reply, QOverload<KJob*, unsigned long>::of(&KAuth::ExecuteJob::percent), this,
+#else
+    QObject::connect(reply, &KAuth::ExecuteJob::percentChanged, this,
+#endif
                      [this](KJob *job, unsigned long percent) {
         Q_UNUSED(job)
         if (percent >= 40 && role() == InstallRole) {
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp
index 14df959cd..4b09c7d05 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp
@@ -25,6 +25,7 @@
 #include "alpineapk_backend_logging.h"
 #include "utils.h"
 
+#include <kcoreaddons_version.h>
 #include <KAuthExecuteJob>
 #include <KLocalizedString>
 
@@ -166,9 +167,14 @@ void AlpineApkUpdater::start()
 
     QObject::connect(reply, &KAuth::ExecuteJob::result,
                      this, &AlpineApkUpdater::handleKAuthUpgradeHelperReply);
+#if KCOREADDONS_VERSION < QT_VERSION_CHECK(5,80,0)
     // qOverload is needed because of conflict with getter named percent()
     QObject::connect(reply, QOverload<KJob *, unsigned long>::of(&KAuth::ExecuteJob::percent),
                      this, &AlpineApkUpdater::handleKAuthUpgradeHelperProgress);
+#else
+    QObject::connect(reply, &KAuth::ExecuteJob::percentChanged,
+                     this, &AlpineApkUpdater::handleKAuthUpgradeHelperProgress);
+#endif
 
     m_progressing = true;
     m_upgradeProgress = 0.0;
@@ -197,9 +203,14 @@ void AlpineApkUpdater::startCheckForUpdates()
     if (!reply) return;
     QObject::connect(reply, &KAuth::ExecuteJob::result,
                      this, &AlpineApkUpdater::handleKAuthUpdateHelperReply);
+#if KCOREADDONS_VERSION < QT_VERSION_CHECK(5,80,0)
     // qOverload is needed because of conflict with getter named percent()
     QObject::connect(reply, QOverload<KJob *, unsigned long>::of(&KAuth::ExecuteJob::percent),
                      this, &AlpineApkUpdater::handleKAuthUpdateHelperProgress);
+#else
+    QObject::connect(reply, &KAuth::ExecuteJob::percentChanged,
+                     this, &AlpineApkUpdater::handleKAuthUpdateHelperProgress);
+#endif
 
     m_progressing = true;
     Q_EMIT progressingChanged(m_progressing);
-- 
GitLab


From 3750c308f1a3e4c75d7f4558a8cf3a5ca4a66ad5 Mon Sep 17 00:00:00 2001
From: Alexey Minnekhanov <alexeymin@postmarketos.org>
Date: Fri, 21 Jan 2022 07:33:33 +0300
Subject: [PATCH 53/59] AppstreamDataDownloader: support new Alpine metadata
 file names

This is needed to avoid name clash caused by new file names.

Also remove old-format metadata files, to avoid conflicts
between metadata in new files and metadata in possibly
existing old files.
---
 .../AppstreamDataDownloader.cpp               | 91 ++++++++++++++-----
 .../AppstreamDataDownloader.h                 |  6 +-
 2 files changed, 72 insertions(+), 25 deletions(-)

diff --git a/libdiscover/backends/AlpineApkBackend/AppstreamDataDownloader.cpp b/libdiscover/backends/AlpineApkBackend/AppstreamDataDownloader.cpp
index e791b2e02..27c2ad679 100644
--- a/libdiscover/backends/AlpineApkBackend/AppstreamDataDownloader.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AppstreamDataDownloader.cpp
@@ -103,13 +103,48 @@ QString AppstreamDataDownloader::calcLocalFileSavePath(const QUrl &urlToDownload
 {
     // we are adding a prefix here to local file name to avoid possible
     //    file name clashes with files from other JSONs
+    // urlToDownload looks like:
+    //    "https://appstream.alpinelinux.org/data/edge/main/Components-main-aarch64.xml.gz"
+    //    "https://appstream.alpinelinux.org/data/edge/community/Components-community-aarch64.xml.gz"
+    // future update will change them to this form:
+    //   "https://appstream.alpinelinux.org/data/edge/main/Components-aarch64.xml.gz"
+    //   "https://appstream.alpinelinux.org/data/edge/community/Components-aarch64.xml.gz"
+    // so, file names will clash. We also need to have full URL to affect local file name
+    //   to avoid name clashes.
     const QString urlPrefix = m_urlPrefixes.value(urlToDownload.toString(), QString());
-    const QFileInfo urlInfo(urlToDownload.path());
-    const QString localCacheFile = AppstreamDataDownloader::appStreamCacheDir() + QDir::separator() + urlPrefix + QLatin1Char('_') + urlInfo.fileName();
-    // aka "~/.cache/discover/external_appstream_data/urlPrefix_fileName.xml.gz"
+    const QString urlPathHash = urlToDownload.path().replace(QLatin1Char('/'), QLatin1Char('_'));
+    const QString localCacheFile = AppstreamDataDownloader::appStreamCacheDir() + QDir::separator() + urlPrefix + QLatin1Char('_') + urlPathHash;
+
+    // new "~/.cache/discover/external_appstream_data/alpine-appstream-data__data_edge_main_Components-aarch64.xml.gz"
+
     return localCacheFile;
 }
 
+QString AppstreamDataDownloader::calcLocalFileSavePathOld(const QUrl &urlToDownload)
+{
+    // Calculate what file name was there for old format.
+    // We keep a list of "old" file names so we can delete them.
+    // "~/.cache/discover/external_appstream_data/alpine-appstream-data_Components-main-aarch64.xml.gz"
+    // ^                                        ^/|                   ^ ^
+    // | appstream cache dir -------------------|/|--- urlPrefix -----|_|-- file name
+    const QString urlPrefix = m_urlPrefixes.value(urlToDownload.toString(), QString());
+    const QString urlFileName = QFileInfo(urlToDownload.path()).fileName();
+    const QString oldFormatFileName = AppstreamDataDownloader::appStreamCacheDir() + QDir::separator() + urlPrefix + QLatin1Char('_') + urlFileName;
+    return oldFormatFileName;
+}
+
+void AppstreamDataDownloader::cleanupOldCachedFiles()
+{
+    if (m_oldFormatFileNames.isEmpty()) {
+        return;
+    }
+    qCDebug(LOG_ALPINEAPK) << "appstream_downloader: removing old files:";
+    for (const QString &oldFn : m_oldFormatFileNames) {
+        bool ok = QFile::remove(oldFn);
+        qCDebug(LOG_ALPINEAPK) << "    " << oldFn << (ok ? "OK" : "Fail");
+    }
+}
+
 void AppstreamDataDownloader::start()
 {
     m_urls.clear();
@@ -138,7 +173,10 @@ void AppstreamDataDownloader::start()
     appStreamCacheDir(); // can create a cache dir if not exists
 
     const QDateTime dtNow = QDateTime::currentDateTime();
+
     m_urlsToDownload.clear();
+    m_oldFormatFileNames.clear();
+
     for (const QString &url : m_urls) {
         const QUrl urlToDownload(url, QUrl::TolerantMode);
         const QString localCacheFile = calcLocalFileSavePath(urlToDownload);
@@ -156,35 +194,39 @@ void AppstreamDataDownloader::start()
             qCDebug(LOG_ALPINEAPK) << " appstream metadata file: " << localFi.fileName()
                                    << " does not exist, queued for downloading";
         }
-    }
 
-    if (!m_urlsToDownload.isEmpty()) {
-        // some files are outdated; download is needed
-        qCDebug(LOG_ALPINEAPK) << "appstream_downloader: We will need to download "
-                               << m_urlsToDownload.size() << " file(s)";
-
-        const QString discoverVersion(QStringLiteral("plasma-discover %1").arg(DiscoverVersion::version));
-
-        m_jobs.clear();
-        for (const QString &sUrl : qAsConst(m_urlsToDownload)) {
-            const QUrl url(sUrl, QUrl::TolerantMode);
-            KIO::TransferJob *job = KIO::get(url, KIO::LoadType::Reload, KIO::JobFlag::HideProgressInfo);
-            job->addMetaData(QLatin1String("UserAgent"), discoverVersion);
-
-            m_jobTracker->registerJob(job);
-
-            QObject::connect(job, &KJob::result, this, &AppstreamDataDownloader::onJobResult);
-            QObject::connect(job, &KIO::TransferJob::data, this, &AppstreamDataDownloader::onJobData);
+        // create a set of possible cached files with old name format
+        const QString localCacheFileOld = calcLocalFileSavePathOld(urlToDownload);
+        m_oldFormatFileNames.insert(localCacheFileOld);
+    }
 
-            m_jobs.push_back(job);
-        }
-    } else {
+    if (m_urlsToDownload.isEmpty()) {
         // no need to download anything
         qCDebug(LOG_ALPINEAPK) << "appstream_downloader: All appstream data files "
                                   "are up to date, not downloading anything";
+        cleanupOldCachedFiles();
         Q_EMIT downloadFinished();
         return;
     }
+
+    // If we're here, some files are outdated; download is needed
+    qCDebug(LOG_ALPINEAPK) << "appstream_downloader: We will need to download " << m_urlsToDownload.size() << " file(s)";
+
+    const QString discoverVersion(QStringLiteral("plasma-discover %1").arg(DiscoverVersion::version));
+
+    m_jobs.clear();
+    for (const QString &sUrl : qAsConst(m_urlsToDownload)) {
+        const QUrl url(sUrl, QUrl::TolerantMode);
+        KIO::TransferJob *job = KIO::get(url, KIO::LoadType::Reload, KIO::JobFlag::HideProgressInfo);
+        job->addMetaData(QLatin1String("UserAgent"), discoverVersion);
+
+        m_jobTracker->registerJob(job);
+
+        QObject::connect(job, &KJob::result, this, &AppstreamDataDownloader::onJobResult);
+        QObject::connect(job, &KIO::TransferJob::data, this, &AppstreamDataDownloader::onJobData);
+
+        m_jobs.push_back(job);
+    }
 }
 
 void AppstreamDataDownloader::onJobData(KIO::Job *job, const QByteArray &data)
@@ -232,6 +274,7 @@ void AppstreamDataDownloader::onJobResult(KJob *job)
 
     if (m_jobs.isEmpty()) {
         qCDebug(LOG_ALPINEAPK) << "appstream_downloader: all downloads have finished!";
+        cleanupOldCachedFiles();
         Q_EMIT downloadFinished();
     }
 }
diff --git a/libdiscover/backends/AlpineApkBackend/AppstreamDataDownloader.h b/libdiscover/backends/AlpineApkBackend/AppstreamDataDownloader.h
index 1b3cc6e28..390f4f6e2 100644
--- a/libdiscover/backends/AlpineApkBackend/AppstreamDataDownloader.h
+++ b/libdiscover/backends/AlpineApkBackend/AppstreamDataDownloader.h
@@ -25,6 +25,7 @@
 #include <QHash>
 #include <QList>
 #include <QObject>
+#include <QSet>
 #include <QString>
 #include <QUrl>
 
@@ -135,8 +136,10 @@ Q_SIGNALS:
     void downloadFinished();
 
 private:
-    QString calcLocalFileSavePath(const QUrl &urlTodownload);
+    QString calcLocalFileSavePath(const QUrl &urlToDownload);
+    QString calcLocalFileSavePathOld(const QUrl &urlToDownload);
     void loadUrlsJson(const QString &path);
+    void cleanupOldCachedFiles();
 
 private Q_SLOTS:
     void onJobData(KIO::Job *job, const QByteArray &data);
@@ -146,6 +149,7 @@ protected:
     qint64 m_cacheExpireSeconds = 7 * 24 * 3600;
     QStringList m_urls;
     QStringList m_urlsToDownload;
+    QSet<QString> m_oldFormatFileNames;
     QHash<QString, QString> m_urlPrefixes;
     bool m_cacheWasUpdated = false;
 
-- 
GitLab


From 29e52e6fce29c0d2e9b2150586e1940dc6c7b320 Mon Sep 17 00:00:00 2001
From: Alexey Minnekhanov <alexeymin@postmarketos.org>
Date: Fri, 21 Jan 2022 07:43:23 +0300
Subject: [PATCH 54/59] Revert "HACK: AlpineApkResource: Workaround broken
 Alpine metadata"

This reverts commit e0a968f536bcc993cd6cf81c00a99f98fd1474b0.

I have a hope that this is no longer needed with new metadata.
---
 .../AlpineApkBackend/AlpineApkResource.cpp       | 16 +---------------
 1 file changed, 1 insertion(+), 15 deletions(-)

diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkResource.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkResource.cpp
index d7ac49012..459347061 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkResource.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkResource.cpp
@@ -220,21 +220,7 @@ void AlpineApkResource::fetchChangelog()
 void AlpineApkResource::fetchScreenshots()
 {
     if (hasAppStreamData()) {
-        /*const*/ QPair<QList<QUrl>, QList<QUrl>> sc = AppStreamUtils::fetchScreenshots(m_appsC);
-        if (sc.first.size() > 0) {
-            const QUrl &url = sc.first.first();
-            if (url.isRelative()) {
-                // This is a hack to fix broken Alpine Linux appstream
-                // metadata: somehow generated thumbnails URLs are relative
-                // and always fail to load (not present in metadata at all)
-                // We work around this by using full url, which is veyr not
-                // good - they're fetched by http://...
-                // It would be great if thumbnails were hosted locally.
-                if (sc.second.size() > 0) {
-                    sc.first = sc.second;
-                }
-            }
-        }
+        const QPair<QList<QUrl>, QList<QUrl> > sc = AppStreamUtils::fetchScreenshots(m_appsC);
         Q_EMIT screenshotsFetched(sc.first, sc.second);
     }
 }
-- 
GitLab


From a5b99b7eb9e1d02bfc61638f2300194fd3f914c1 Mon Sep 17 00:00:00 2001
From: Alexey Minnekhanov <alexeymin@postmarketos.org>
Date: Tue, 8 Feb 2022 21:41:05 +0300
Subject: [PATCH 55/59] Follow upstream changes to quit64 as size

follow-up to a1cf31faf1a8aeebf2786715590cf3bb8a5cd472
---
 libdiscover/backends/AlpineApkBackend/AlpineApkResource.cpp | 4 ++--
 libdiscover/backends/AlpineApkBackend/AlpineApkResource.h   | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkResource.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkResource.cpp
index 459347061..2c981ca90 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkResource.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkResource.cpp
@@ -72,9 +72,9 @@ QString AlpineApkResource::comment()
     return m_pkg.description;
 }
 
-int AlpineApkResource::size()
+quint64 AlpineApkResource::size()
 {
-    return static_cast<int>(m_pkg.size);
+    return m_pkg.size;
 }
 
 QUrl AlpineApkResource::homepage()
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkResource.h b/libdiscover/backends/AlpineApkBackend/AlpineApkResource.h
index 5304a8773..fec02a994 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkResource.h
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkResource.h
@@ -44,7 +44,7 @@ public:
     QString availableVersion() const override;
     QString installedVersion() const override;
     QJsonArray licenses() override;
-    int size() override;
+    quint64 size() override;
     QUrl homepage() override;
     QUrl helpURL() override;
     QUrl bugURL() override;
-- 
GitLab


From 3e3726a5f58f5f8c97e4b91f145541d2eb86988b Mon Sep 17 00:00:00 2001
From: Alexey Minnekhanov <alexeymin@postmarketos.org>
Date: Sat, 16 Apr 2022 08:42:09 +0300
Subject: [PATCH 56/59] AlpineApkBackend: compatibility with AppStreamQt 0.15
 or later

use newer API and enum values
---
 .../backends/AlpineApkBackend/AlpineApkBackend.cpp    | 11 +++++++++--
 1 file changed, 9 insertions(+), 2 deletions(-)

diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp
index 88b37cf0e..f53947324 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp
@@ -34,6 +34,7 @@
 #include <KLocalizedString>
 
 #include <AppStreamQt/pool.h>
+#include <AppStreamQt/version.h>
 
 #include <QAction>
 #include <QtConcurrentRun>
@@ -90,14 +91,20 @@ AlpineApkBackend::AlpineApkBackend(QObject *parent)
 void AlpineApkBackend::loadAppStreamComponents()
 {
     AppStream::Pool *appStreamPool = new AppStream::Pool();
+#if ASQ_CHECK_VERSION(0, 15, 0)
+    // use newer API and flags available only since 0.15.0
+    appStreamPool->setFlags(AppStream::Pool::FlagLoadOsCollection | AppStream::Pool::FlagLoadOsMetainfo | AppStream::Pool::FlagLoadOsDesktopFiles);
+
+    // AS_FORMAT_STYLE_COLLECTION - Parse AppStream metadata collections (shipped by software distributors)
+    appStreamPool->addExtraDataLocation(AppstreamDataDownloader::appStreamCacheDir(), AppStream::Metadata::FormatStyleCollection);
+#else
     appStreamPool->setFlags(AppStream::Pool::FlagReadCollection |
                             AppStream::Pool::FlagReadMetainfo |
                             AppStream::Pool::FlagReadDesktopFiles);
     appStreamPool->setCacheFlags(AppStream::Pool::CacheFlagUseUser |
                                  AppStream::Pool::CacheFlagUseSystem);
-
-    // hey hey! cool stuff
     appStreamPool->addMetadataLocation(AppstreamDataDownloader::appStreamCacheDir());
+#endif
 
     if (!appStreamPool->load()) {
         qCWarning(LOG_ALPINEAPK) << "backend: Failed to load appstream data:"
-- 
GitLab


From 828271cfed39211f5bad7d88bfe0f93842bf72c5 Mon Sep 17 00:00:00 2001
From: Alexey Minnekhanov <alexeymin@postmarketos.org>
Date: Sun, 5 Jun 2022 22:04:17 +0300
Subject: [PATCH 57/59] Switch to new-style KAuth headers (KF 5.92
 compatibility)

KAuthAction -> KAuth/Action, etc
---
 .../backends/AlpineApkBackend/AlpineApkAuthActionFactory.h  | 6 +++---
 .../backends/AlpineApkBackend/AlpineApkAuthHelper.cpp       | 2 +-
 libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.h | 3 ++-
 .../backends/AlpineApkBackend/AlpineApkSourcesBackend.cpp   | 2 +-
 .../backends/AlpineApkBackend/AlpineApkTransaction.cpp      | 4 ++--
 libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp  | 4 ++--
 6 files changed, 11 insertions(+), 10 deletions(-)

diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkAuthActionFactory.h b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthActionFactory.h
index ff5667f85..b43ca7e24 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkAuthActionFactory.h
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthActionFactory.h
@@ -24,9 +24,9 @@
 #include <QString>
 #include <QVariant>
 
-#include <KAuthAction>
-#include <KAuthActionReply>
-#include <KAuthExecuteJob>
+#include <KAuth/Action>
+#include <KAuth/ActionReply>
+#include <KAuth/ExecuteJob>
 
 namespace ActionFactory {
 
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp
index 97affc013..14d559ba5 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp
@@ -25,7 +25,7 @@
 #include <QScopedPointer>
 #include <QFile>
 
-#include <KAuthHelperSupport>
+#include <KAuth/HelperSupport>
 #include <kauth_version.h>
 
 #include "AlpineApkAuthHelper.h"
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.h b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.h
index 240e6ed35..55f81d16b 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.h
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.h
@@ -21,7 +21,8 @@
 #include <QEventLoop>
 #include <QObject>
 #include <QVariant>
-#include <KAuthActionReply>
+
+#include <KAuth/ActionReply>
 
 #include <QtApk>
 
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkSourcesBackend.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkSourcesBackend.cpp
index c10ebdcb5..073430894 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkSourcesBackend.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkSourcesBackend.cpp
@@ -29,7 +29,7 @@
 #include <QVector>
 
 // KF5
-#include <KAuthExecuteJob>
+#include <KAuth/ExecuteJob>
 #include <KLocalizedString>
 
 // libapk-qt
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkTransaction.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkTransaction.cpp
index 0f346e843..de82546ca 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkTransaction.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkTransaction.cpp
@@ -29,9 +29,9 @@
 #include <QTimer>
 
 // KF5
-#include <kcoreaddons_version.h>
-#include <KAuthExecuteJob>
+#include <KAuth/ExecuteJob>
 #include <KLocalizedString>
+#include <kcoreaddons_version.h>
 
 AlpineApkTransaction::AlpineApkTransaction(AlpineApkResource *res, Role role)
     : AlpineApkTransaction(res, {}, role)
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp
index 4b09c7d05..b36ffb987 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp
@@ -25,9 +25,9 @@
 #include "alpineapk_backend_logging.h"
 #include "utils.h"
 
-#include <kcoreaddons_version.h>
-#include <KAuthExecuteJob>
+#include <KAuth/ExecuteJob>
 #include <KLocalizedString>
+#include <kcoreaddons_version.h>
 
 #include <QtApk>
 
-- 
GitLab


From 30ebbc785110be182de0d47ad21425a9ad269a68 Mon Sep 17 00:00:00 2001
From: Alexey Minnekhanov <alexeymin@postmarketos.org>
Date: Sun, 18 Sep 2022 17:21:43 +0300
Subject: [PATCH 58/59] AlpineApkReviewsBackend: compat with upstream changes

Adapt to changes in ee3d6e9c912d901174a44fe96136583086cd6932: add
overrides for new abstract methods, move userName() to protected
---
 .../AlpineApkReviewsBackend.h                 | 20 ++++++++++++++++---
 1 file changed, 17 insertions(+), 3 deletions(-)

diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkReviewsBackend.h b/libdiscover/backends/AlpineApkBackend/AlpineApkReviewsBackend.h
index 435f845ba..523dd4cb9 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkReviewsBackend.h
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkReviewsBackend.h
@@ -32,7 +32,6 @@ class AlpineApkReviewsBackend : public AbstractReviewsBackend
 public:
     explicit AlpineApkReviewsBackend(AlpineApkBackend *parent = nullptr);
 
-    QString userName() const override { return QStringLiteral("dummy"); }
     void login() override {}
     void logout() override {}
     void registerAndLogin() override {}
@@ -42,11 +41,26 @@ public:
     void deleteReview(Review *) override {}
     void fetchReviews(AbstractResource *app, int page = 1) override;
     bool isFetching() const override { return false; }
-    bool isReviewable() const override { return false; }
-    void submitReview(AbstractResource *, const QString &, const QString &, const QString &) override {}
+    bool isReviewable() const override
+    {
+        return false;
+    }
     void flagReview(Review *, const QString&, const QString&) override {}
     void submitUsefulness(Review *, bool) override {}
     bool isResourceSupported(AbstractResource *) const override { return false; }
+    bool supportsNameChange() const override
+    {
+        return false;
+    }
+
+protected:
+    void sendReview(AbstractResource *, const QString &, const QString &, const QString &, const QString &) override
+    {
+    }
+    QString userName() const override
+    {
+        return QStringLiteral("dummy");
+    }
 };
 
 #endif // ALPINEAPKREVIEWSBACKEND_H
-- 
GitLab


From 190f2f7099e79fa6a6b7fc94b8a27ed996fceae1 Mon Sep 17 00:00:00 2001
From: Alexey Minnekhanov <alexeymin@postmarketos.org>
Date: Sun, 18 Sep 2022 17:29:35 +0300
Subject: [PATCH 59/59] AlpineApkBackend: compat with upstream

Fixup after 78277138749a9cd4b644f2814822a1b392d259c8 "category
decorations are not a thing anymore"
---
 .../AlpineApkBackend/AlpineApkBackend.cpp     | 49 +++++++++----------
 1 file changed, 23 insertions(+), 26 deletions(-)

diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp
index f53947324..5bcc38556 100644
--- a/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp
@@ -315,36 +315,33 @@ QVector<Category *> AlpineApkBackend::category() const
 
     // Display a single root category
     // we could add more, but Alpine apk does not have this concept
-    static Category *s_rootCat = new Category(
-            i18nc("Root category name", "Alpine Linux packages"),
-            QStringLiteral("package-x-generic"), // icon
-            { s_apkFlt },      // orFilters - include packages that match filter
-            { displayName() }, // pluginName
-            {},                // subCategories - none
-            QUrl(),            // decoration (what is it?)
-            false              // isAddons
+    static Category *s_rootCat = new Category(i18nc("Root category name", "Alpine Linux packages"),
+                                              QStringLiteral("package-x-generic"), // icon
+                                              {s_apkFlt}, // orFilters - include packages that match filter
+                                              {displayName()}, // pluginName
+                                              {}, // subCategories - none
+                                              false // isAddons
     );
 
     return { s_rootCat };
 
-//    static QVector<Category *> s_cats;
-//    if (s_cats.isEmpty()) {
-//        // fill only once
-//        s_cats << s_rootCat;
-//        for (const QString &scat : m_collectedCategories) {
-//            Category *cat = new Category(
-//                        scat,   // name
-//                        QStringLiteral("package-x-generic"), // icon
-//                        {},     // orFilters
-//                        { displayName() }, // pluginName
-//                        {},     // subcategories
-//                        QUrl(), // decoration
-//                        false   // isAddons
-//            );
-//            s_cats << cat;
-//        }
-//    }
-//    return s_cats;
+    //    static QVector<Category *> s_cats;
+    //    if (s_cats.isEmpty()) {
+    //        // fill only once
+    //        s_cats << s_rootCat;
+    //        for (const QString &scat : m_collectedCategories) {
+    //            Category *cat = new Category(
+    //                        scat,   // name
+    //                        QStringLiteral("package-x-generic"), // icon
+    //                        {},     // orFilters
+    //                        { displayName() }, // pluginName
+    //                        {},     // subcategories
+    //                        false   // isAddons
+    //            );
+    //            s_cats << cat;
+    //        }
+    //    }
+    //    return s_cats;
     // ^^ causes deep hang in discover in recalculating QML bindings
 }
 
-- 
GitLab

