From 6fbb948082bad66a821c8332e2fb9cb7965065f8 Mon Sep 17 00:00:00 2001
From: Alexey Minnekhanov <alexeymin@postmarketos.org>
Date: Sun, 12 Jan 2020 01:02:39 +0300
Subject: [PATCH] Add support for Alpine Linux apk backend

Alpine Package Keeper (apk) is package manager
for Alpine Linux.
---
 discover/FeaturedModel.cpp                    |   6 +-
 .../AlpineApkAuthActionFactory.cpp            | 118 ++++
 .../AlpineApkAuthActionFactory.h              |  41 ++
 .../AlpineApkBackend/AlpineApkAuthHelper.cpp  | 302 +++++++++++
 .../AlpineApkBackend/AlpineApkAuthHelper.h    |  66 +++
 .../AlpineApkBackend/AlpineApkBackend.cpp     | 503 ++++++++++++++++++
 .../AlpineApkBackend/AlpineApkBackend.h       |  99 ++++
 .../AlpineApkBackend/AlpineApkResource.cpp    | 341 ++++++++++++
 .../AlpineApkBackend/AlpineApkResource.h      |  93 ++++
 .../AlpineApkReviewsBackend.cpp               |  35 ++
 .../AlpineApkReviewsBackend.h                 |  52 ++
 .../AlpineApkSourcesBackend.cpp               | 195 +++++++
 .../AlpineApkSourcesBackend.h                 |  57 ++
 .../AlpineApkBackend/AlpineApkTransaction.cpp | 141 +++++
 .../AlpineApkBackend/AlpineApkTransaction.h   |  49 ++
 .../AlpineApkBackend/AlpineApkUpdater.cpp     | 295 ++++++++++
 .../AlpineApkBackend/AlpineApkUpdater.h       | 197 +++++++
 .../AppstreamDataDownloader.cpp               | 303 +++++++++++
 .../AppstreamDataDownloader.h                 | 139 +++++
 .../backends/AlpineApkBackend/CMakeLists.txt  |  85 +++
 .../org.kde.discover.alpineapkbackend.actions |   5 +
 libdiscover/backends/CMakeLists.txt           |  10 +
 22 files changed, 3129 insertions(+), 3 deletions(-)
 create mode 100644 libdiscover/backends/AlpineApkBackend/AlpineApkAuthActionFactory.cpp
 create mode 100644 libdiscover/backends/AlpineApkBackend/AlpineApkAuthActionFactory.h
 create mode 100644 libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp
 create mode 100644 libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.h
 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/AlpineApkReviewsBackend.cpp
 create mode 100644 libdiscover/backends/AlpineApkBackend/AlpineApkReviewsBackend.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/AlpineApkUpdater.cpp
 create mode 100644 libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.h
 create mode 100644 libdiscover/backends/AlpineApkBackend/AppstreamDataDownloader.cpp
 create mode 100644 libdiscover/backends/AlpineApkBackend/AppstreamDataDownloader.h
 create mode 100644 libdiscover/backends/AlpineApkBackend/CMakeLists.txt
 create mode 100644 libdiscover/backends/AlpineApkBackend/org.kde.discover.alpineapkbackend.actions

diff --git a/discover/FeaturedModel.cpp b/discover/FeaturedModel.cpp
index 1393e206..038f22c1 100644
--- a/discover/FeaturedModel.cpp
+++ b/discover/FeaturedModel.cpp
@@ -98,13 +98,13 @@ void FeaturedModel::refresh()
     setUris(uris);
 }
 
-void FeaturedModel::setUris(const QVector<QUrl>& uris)
+void FeaturedModel::setUris(const QVector<QUrl> &uris)
 {
     if (!m_backend)
         return;
 
-    QSet<ResultsStream*> streams;
-    foreach(const auto &uri, uris) {
+    QSet<ResultsStream *> streams;
+    for (const QUrl &uri: uris) {
         AbstractResourcesBackend::Filters filter;
         filter.resourceUrl = uri;
         streams << m_backend->search(filter);
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkAuthActionFactory.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthActionFactory.cpp
new file mode 100644
index 00000000..972f8ec5
--- /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 00000000..ff5667f8
--- /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/AlpineApkAuthHelper.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp
new file mode 100644
index 00000000..97affc01
--- /dev/null
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp
@@ -0,0 +1,302 @@
+/***************************************************************************
+ *   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 <QLoggingCategory>
+#include <QSocketNotifier>
+#include <QScopedPointer>
+#include <QFile>
+
+#include <KAuthHelperSupport>
+#include <kauth_version.h>
+
+#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() {}
+
+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()) {
+        m_apkdb.setFakeRoot(fakeRoot);
+    }
+
+    // calculate flags to use during open
+    QtApk::DbOpenFlags fl = QtApk::QTAPK_OPENF_ENABLE_PROGRESSFD;
+    if (readwrite) {
+        fl |= QtApk::QTAPK_OPENF_READWRITE;
+    }
+
+    if (!m_apkdb.open(fl)) {
+        return false;
+    }
+    return true;
+}
+
+void AlpineApkAuthHelper::closeDatabase()
+{
+    // close database only if opened
+    if (m_apkdb.isOpen()) {
+        // this also stops bg thread
+        m_apkdb.close();
+    }
+}
+
+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);
+
+    // receive error messages
+    QObject::connect(trans, &QtApk::Transaction::errorOccured,
+                     this, &AlpineApkAuthHelper::onTransactionError);
+
+    // 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)
+{
+    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;
+    // construct error message to use in helper reply
+    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();
+}
+
+// single entry point for all package management actions
+ActionReply AlpineApkAuthHelper::pkgmgmt(const QVariantMap &args)
+{
+    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_trans_ok = true;
+    QtApk::Transaction *trans = m_apkdb.updatePackageIndex();
+    setupTransactionPostCreate(trans);
+
+    trans->start();
+    m_loop->exec();
+
+    if (m_trans_ok) {
+        int updatesCount = m_apkdb.upgradeablePackagesCount();
+        m_actionReply = ActionReply::SuccessReply();
+        m_actionReply.setData({
+            { QLatin1String("updatesCount"), updatesCount }
+        });
+    }
+}
+
+void AlpineApkAuthHelper::add(const QVariantMap &args)
+{
+    if (!openDatabase(args)) {
+        m_actionReply.setErrorDescription(QStringLiteral("Failed to open database!"));
+        return;
+    }
+
+    const QString pkgName = args.value(QLatin1String("pkgName"), QString()).toString();
+    if (pkgName.isEmpty()) {
+        m_actionReply.setErrorDescription(QStringLiteral("Specify pkgName for adding!"));
+        return;
+    }
+
+    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();
+    }
+}
+
+void AlpineApkAuthHelper::del(const QVariantMap &args)
+{
+    if (!openDatabase(args)) {
+        m_actionReply.setErrorDescription(QStringLiteral("Failed to open database!"));
+        return;
+    }
+
+    const QString pkgName = args.value(QLatin1String("pkgName"), QString()).toString();
+    if (pkgName.isEmpty()) {
+        m_actionReply.setErrorDescription(QStringLiteral("Specify pkgName for removing!"));
+        return;
+    }
+
+    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();
+    }
+}
+
+void AlpineApkAuthHelper::upgrade(const QVariantMap &args)
+{
+    if (!openDatabase(args)) {
+        m_actionReply.setErrorDescription(QStringLiteral("Failed to open database!"));
+        return;
+    }
+
+    bool onlySimulate = args.value(QLatin1String("onlySimulate"), false).toBool();
+    QtApk::DbUpgradeFlags flags = QtApk::QTAPK_UPGRADE_DEFAULT;
+    if (onlySimulate) {
+        flags = QtApk::QTAPK_UPGRADE_SIMULATE;
+        qCDebug(LOG_AUTHHELPER) << "Simulating upgrade run.";
+    }
+
+    m_trans_ok = true;
+
+    QtApk::Transaction *trans = m_apkdb.upgrade(flags);
+    setupTransactionPostCreate(trans);
+
+    trans->start();
+    m_loop->exec();
+
+    if (m_trans_ok) {
+        m_actionReply = ActionReply::SuccessReply();
+        QVariantMap replyData;
+        const QVector<QtApk::ChangesetItem> ch = m_lastChangeset.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);
+        m_actionReply.setData(replyData);
+    }
+}
+
+void AlpineApkAuthHelper::repoconfig(const QVariantMap &args)
+{
+    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!"));
+    }
+}
+
+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 00000000..240e6ed3
--- /dev/null
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.h
@@ -0,0 +1,66 @@
+/***************************************************************************
+ *   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 <QEventLoop>
+#include <QObject>
+#include <QVariant>
+#include <KAuthActionReply>
+
+#include <QtApk>
+
+using namespace KAuth;
+
+class AlpineApkAuthHelper : public QObject
+{
+    Q_OBJECT
+public:
+    AlpineApkAuthHelper();
+    ~AlpineApkAuthHelper() override;
+
+public Q_SLOTS:
+    // 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);
+    void onTransactionFinished();
+
+private:
+    QtApk::DatabaseAsync m_apkdb; // runs transactions in bg thread
+    QtApk::Transaction *m_currentTransaction = nullptr;
+    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
+};
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp
new file mode 100644
index 00000000..4bfe165b
--- /dev/null
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp
@@ -0,0 +1,503 @@
+/***************************************************************************
+ *   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 "AlpineApkReviewsBackend.h"
+#include "AlpineApkTransaction.h"
+#include "AlpineApkSourcesBackend.h"
+#include "AlpineApkUpdater.h"
+#include "AppstreamDataDownloader.h"
+#include "alpineapk_backend_logging.h"  // generated by ECM
+
+#include "resources/SourcesModel.h"
+#include "Transaction/Transaction.h"
+#include "Category/Category.h"
+
+#include <KLocalizedString>
+
+#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)
+
+AlpineApkBackend::AlpineApkBackend(QObject *parent)
+    : AbstractResourcesBackend(parent)
+    , m_updater(new AlpineApkUpdater(this))
+    , m_reviews(new AlpineApkReviewsBackend(this))
+    , m_updatesTimeoutTimer(new QTimer(this))
+{
+#ifndef QT_DEBUG
+    const_cast<QLoggingCategory &>(LOG_ALPINEAPK()).setEnabled(QtDebugMsg, false);
+#endif
+
+    // 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);
+
+    // 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(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));
+}
+
+// this fills in m_appStreamComponents
+void AlpineApkBackend::loadAppStreamComponents()
+{
+    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::getAppStreamCacheDir());
+
+    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;
+}
+
+// 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: qAsConst(m_availablePackages)) {
+
+            // try to find appstream data for this package
+            AppStream::Component appstreamComponent;
+            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
+                    //   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);
+        }
+    }
+}
+
+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::fillResourcesAndApplyAppStreamData()
+{
+    // 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 &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);
+            }
+        }
+    }
+}
+
+void AlpineApkBackend::reloadAppStreamMetadata()
+{
+    // mark us as "Loading..."
+    m_fetching = true;
+    emit fetchingChanged();
+
+    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) {
+            const QString key = pkg.name.toLower();
+            if (m_resources.contains(key)) {
+                m_resources.value(key)->setState(AbstractResource::Installed);
+            }
+        }
+    }
+
+    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);
+
+    // 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
+{
+    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 *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
+    );
+
+    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
+{
+    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 *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(resource->name().contains(filter.search, Qt::CaseInsensitive)
+                    || resource->comment().contains(filter.search, Qt::CaseInsensitive)) {
+                ret += resource;
+            }
+        }
+    }
+    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 URLs - handle "featured" apps
+    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);
+    }
+
+    // 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"), {});
+    }
+    return new ResultsStream(QStringLiteral("AlpineApkStream"), { result });
+}
+
+AbstractBackendUpdater *AlpineApkBackend::backendUpdater() const
+{
+    return m_updater;
+}
+
+AbstractReviewsBackend *AlpineApkBackend::reviewsBackend() const
+{
+    return m_reviews;
+}
+
+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);
+}
+
+int AlpineApkBackend::fetchingUpdatesProgress() const
+{
+    if (!m_fetching) return 100;
+    return m_fetchProgress;
+}
+
+void AlpineApkBackend::checkForUpdates()
+{
+    if (m_fetching) {
+        qCDebug(LOG_ALPINEAPK) << "backend: checkForUpdates(): already fetching";
+        return;
+    }
+
+    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 i18nc("Backend plugin display name", "Alpine APK backend");
+}
+
+bool AlpineApkBackend::hasApplications() const
+{
+    return true;
+}
+
+void AlpineApkBackend::setFetchingUpdatesProgress(int percent)
+{
+    m_fetchProgress = percent;
+    emit fetchingUpdatesProgressChanged();
+}
+
+// needed because DISCOVER_BACKEND_PLUGIN(AlpineApkBackend) contains Q_OBJECT
+#include "AlpineApkBackend.moc"
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.h b/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.h
new file mode 100644
index 00000000..07b6b7be
--- /dev/null
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.h
@@ -0,0 +1,99 @@
+/***************************************************************************
+ *   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 <QFutureWatcher>
+#include <QVariantList>
+
+#include <QtApk>
+
+#include <AppStreamQt/component.h>
+
+class AlpineApkReviewsBackend;
+class AlpineApkUpdater;
+class AlpineApkResource;
+class AppstreamDataDownloader;
+class KJob;
+class QTimer;
+
+class AlpineApkBackend : public AbstractResourcesBackend
+{
+    Q_OBJECT
+
+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; }
+    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 setFetchingUpdatesProgress(int percent);
+
+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; }
+
+private:
+    QHash<QString, AlpineApkResource *> m_resources;
+    QHash<QString, AppStream::Component> m_resourcesAppstreamData;
+    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_fetchProgress = 0;
+    QTimer *m_updatesTimeoutTimer;
+    QList<AppStream::Component> m_appStreamComponents;
+    // QVector<QString> m_collectedCategories;
+    QFutureWatcher<void> m_voidFutureWatcher;
+    AppstreamDataDownloader *m_appstreamDownloader;
+};
+
+#endif // AlpineApkBackend_H
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkResource.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkResource.cpp
new file mode 100644
index 00000000..8f493a49
--- /dev/null
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkResource.cpp
@@ -0,0 +1,341 @@
+/***************************************************************************
+ *   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 "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(typ)
+    , m_pkg(apkPkg)
+    , m_appsC(component)
+{
+}
+
+QList<PackageState> AlpineApkResource::addonsInformation()
+{
+    return m_addons;
+}
+
+QString AlpineApkResource::availableVersion() const
+{
+    return m_availableVersion;
+}
+
+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;
+}
+
+int AlpineApkResource::size()
+{
+    return static_cast<int>(m_pkg.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");
+}
+
+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()
+{
+    if (hasAppStreamData()) {
+        return m_appsC.description();
+    }
+    return m_pkg.description;
+}
+
+QString AlpineApkResource::name() const
+{
+    if (hasAppStreamData()) {
+        return m_appsC.name();
+    }
+    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()
+{
+    if (hasAppStreamData()) {
+        emit changelogFetched(AppStreamUtils::changelogToHtml(m_appsC));
+    }
+}
+
+void AlpineApkResource::fetchScreenshots()
+{
+    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)
+{
+    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::setAvailableVersion(const QString &av)
+{
+    m_availableVersion = av;
+}
+
+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()) {
+        return (m_appsC.kind() == AppStream::Component::KindDesktopApp &&
+                (m_state == AbstractResource::Installed || m_state == AbstractResource::Upgradeable));
+    }
+    return false;
+}
+
+void AlpineApkResource::invokeApplication() const
+{
+    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;
+}
+
+QString AlpineApkResource::sourceIcon() const
+{
+    return QStringLiteral("player-time");
+}
+
+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
new file mode 100644
index 00000000..5304a877
--- /dev/null
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkResource.h
@@ -0,0 +1,93 @@
+/***************************************************************************
+ *   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>
+#include <AppStreamQt/component.h>
+
+class AddonList;
+
+class AlpineApkResource : public AbstractResource
+{
+    Q_OBJECT
+
+public:
+    explicit AlpineApkResource(const QtApk::Package &apkPkg,
+                               AppStream::Component &component,
+                               AbstractResource::Type typ,
+                               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;
+    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;
+    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);
+    void setAvailableVersion(const QString &av);
+    void setAppStreamData(const AppStream::Component &component);
+
+private:
+    bool hasAppStreamData() const;
+
+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;
+    QList<PackageState> m_addons;
+    AppStream::Component m_appsC;
+};
+
+#endif // ALPINEAPKRESOURCE_H
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkReviewsBackend.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkReviewsBackend.cpp
new file mode 100644
index 00000000..fd7ad47f
--- /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 00000000..435f845b
--- /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/AlpineApkSourcesBackend.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkSourcesBackend.cpp
new file mode 100644
index 00000000..a126483a
--- /dev/null
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkSourcesBackend.cpp
@@ -0,0 +1,195 @@
+/***************************************************************************
+ *   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 "AlpineApkAuthActionFactory.h"
+#include "alpineapk_backend_logging.h"  // generated by ECM
+
+#include <QDebug>
+#include <QAction>
+#include <QVector>
+
+// KF5
+#include <KAuthExecuteJob>
+#include <KLocalizedString>
+
+// libapk-qt
+#include <QtApk>
+
+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
+{
+    loadSources();
+    QObject::connect(m_refreshAction, &QAction::triggered,
+                     this, &AlpineApkSourcesBackend::loadSources);
+    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()
+{
+    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)
+{
+    m_repos.append(QtApk::Repository(id, QString(), true));
+    fillModelFromRepos();
+    return true;
+}
+
+void AlpineApkSourcesBackend::loadSources()
+{
+    m_repos = QtApk::Database::getRepositories();
+    fillModelFromRepos();
+}
+
+void AlpineApkSourcesBackend::fillModelFromRepos()
+{
+    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);
+    }
+}
+
+void AlpineApkSourcesBackend::saveSources()
+{
+    const QVariant repoUrls = QVariant::fromValue<QVector<QtApk::Repository>>(m_repos);
+
+    // run with elevated privileges
+    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) {
+            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();
+    });
+
+    reply->start();
+}
+
+void AlpineApkSourcesBackend::onItemChanged(QStandardItem *item)
+{
+    // 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);
+    m_repos[idx.row()].enabled = (cs == Qt::Checked);
+    fillModelFromRepos();
+}
+
+bool AlpineApkSourcesBackend::removeSource(const QString &id)
+{
+    const QStandardItem *it = sourceForId(id);
+    if (!it) {
+        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 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;
+}
+
+bool AlpineApkSourcesBackend::supportsAdding() const
+{
+    return true;
+}
+
+bool AlpineApkSourcesBackend::canMoveSources() const
+{
+    return true;
+}
+
+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();
+    }
+
+    // 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
new file mode 100644
index 00000000..eacda22d
--- /dev/null
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkSourcesBackend.h
@@ -0,0 +1,57 @@
+/***************************************************************************
+ *   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>
+
+#include <QtApkRepository.h>
+
+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();
+    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
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkTransaction.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkTransaction.cpp
new file mode 100644
index 00000000..26bf1bd0
--- /dev/null
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkTransaction.cpp
@@ -0,0 +1,141 @@
+/***************************************************************************
+ *   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 "AlpineApkAuthActionFactory.h"
+#include "alpineapk_backend_logging.h"  // generated by ECM
+
+// Qt
+#include <QDebug>
+#include <QTimer>
+
+// KF5
+#include <KAuthExecuteJob>
+#include <KLocalizedString>
+
+AlpineApkTransaction::AlpineApkTransaction(AlpineApkResource *res, Role role)
+    : AlpineApkTransaction(res, {}, role)
+{
+}
+
+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(QueuedStatus);
+    // seems like Discover's transactions are supposed to start
+    // automatically; no dedicated method to start transaction?
+    startTransaction();
+}
+
+void AlpineApkTransaction::proceed()
+{
+    startTransaction();
+}
+
+void AlpineApkTransaction::cancel()
+{
+    setStatus(CancelledStatus);
+}
+
+void AlpineApkTransaction::startTransaction()
+{
+    KAuth::ExecuteJob *reply = nullptr;
+    switch(role()) {
+    case InstallRole:
+        reply = ActionFactory::createAddAction(m_resource->m_pkg.name);
+        break;
+    case RemoveRole:
+        reply = ActionFactory::createDelAction(m_resource->m_pkg.name);
+        break;
+    case ChangeAddonsRole:
+        qCWarning(LOG_ALPINEAPK) << "Addons are not supported by Alpine APK Backend!";
+        break;
+    }
+
+    if (!reply) {
+        return;
+    }
+
+    // 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 {
+        setStatus(CommittingStatus);
+    }
+
+    reply->start();
+}
+
+void AlpineApkTransaction::finishTransactionOK()
+{
+    AbstractResource::State newState;
+    switch(role()) {
+    case InstallRole:
+    case ChangeAddonsRole:
+        newState = AbstractResource::Installed;
+        break;
+    case RemoveRole:
+        newState = AbstractResource::None;
+        break;
+    }
+    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
new file mode 100644
index 00000000..cab1f6b9
--- /dev/null
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkTransaction.h
@@ -0,0 +1,49 @@
+/***************************************************************************
+ *   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 AlpineApkBackend;
+class AlpineApkResource;
+
+class AlpineApkTransaction : public Transaction
+{
+Q_OBJECT
+public:
+    AlpineApkTransaction(AlpineApkResource *res, Role role);
+    AlpineApkTransaction(AlpineApkResource *res, const AddonList &list, Role role);
+
+    void cancel() override;
+    void proceed() override;
+
+private Q_SLOTS:
+    void startTransaction();
+    void finishTransactionOK();
+    void finishTransactionWithError(const QString &errMsg);
+
+private:
+    AlpineApkResource *m_resource;
+    AlpineApkBackend *m_backend;
+};
+
+#endif // ALPINEAPKTRANSACTION_H
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp
new file mode 100644
index 00000000..14df959c
--- /dev/null
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp
@@ -0,0 +1,295 @@
+/***************************************************************************
+ *   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 "AlpineApkAuthActionFactory.h"
+#include "alpineapk_backend_logging.h"
+#include "utils.h"
+
+#include <KAuthExecuteJob>
+#include <KLocalizedString>
+
+#include <QtApk>
+
+
+AlpineApkUpdater::AlpineApkUpdater(AbstractResourcesBackend *parent)
+    : AbstractBackendUpdater(parent)
+    , m_backend(static_cast<AlpineApkBackend *>(parent))
+{
+    //
+}
+
+void AlpineApkUpdater::prepare()
+{
+    QtApk::Database *db = m_backend->apkdb();
+
+    if (db->isOpen()) {
+        return;
+    }
+
+    // readonly is fine for a simulation of upgrade
+    if (!db->open(QtApk::QTAPK_OPENF_READONLY)) {
+        emit passiveMessage(i18n("Failed to open APK database!"));
+        return;
+    }
+
+    if (!db->upgrade(QtApk::QTAPK_UPGRADE_SIMULATE, &m_upgradeable)) {
+        emit passiveMessage(i18n("Failed to get a list of packages to upgrade!"));
+        db->close();
+        return;
+    }
+    // close 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
+{
+    return (m_updatesCount > 0);
+}
+
+qreal AlpineApkUpdater::progress() const
+{
+    return m_upgradeProgress;
+}
+
+void AlpineApkUpdater::removeResources(const QList<AbstractResource *> &apps)
+{
+    const QSet<AbstractResource *> checkSet = kToSet(apps);
+    m_markedToUpdate -= checkSet;
+}
+
+void AlpineApkUpdater::addResources(const QList<AbstractResource *> &apps)
+{
+    const QSet<AbstractResource *> checkSet = kToSet(apps);
+    m_markedToUpdate += checkSet;
+}
+
+QList<AbstractResource *> AlpineApkUpdater::toUpdate() const
+{
+    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 false;
+}
+
+bool AlpineApkUpdater::isProgressing() const
+{
+    qCDebug(LOG_ALPINEAPK) << Q_FUNC_INFO << m_progressing;
+    return m_progressing;
+}
+
+bool AlpineApkUpdater::isMarked(AbstractResource *res) const
+{
+    return m_markedToUpdate.contains(res);
+}
+
+void AlpineApkUpdater::fetchChangelog() const
+{
+    qCDebug(LOG_ALPINEAPK) << Q_FUNC_INFO;
+}
+
+double AlpineApkUpdater::updateSize() const
+{
+    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;
+
+    // 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();
+}
+
+void AlpineApkUpdater::proceed()
+{
+    qCDebug(LOG_ALPINEAPK) << Q_FUNC_INFO;
+}
+
+int AlpineApkUpdater::updatesCount()
+{
+    return m_updatesCount;
+}
+
+void AlpineApkUpdater::startCheckForUpdates()
+{
+    QtApk::Database *db = m_backend->apkdb();
+
+    // run updates check with elevated privileges to access
+    //     system package manager files
+    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()
+    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();
+}
+
+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 {
+        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();
+}
+
+void AlpineApkUpdater::handleKAuthUpdateHelperProgress(KJob *job, unsigned long percent)
+{
+    Q_UNUSED(job)
+    qCDebug(LOG_ALPINEAPK) << "    fetch updates progress: " << percent;
+    Q_EMIT fetchingUpdatesProgressChanged(percent);
+    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);
+    const QVariantMap &replyData = reply->data();
+    if (reply->error() == 0) {
+        QVariant pkgsV = replyData.value(QLatin1String("changes"));
+        bool onlySimulate = replyData.value(QLatin1String("onlySimulate"), false).toBool();
+        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) {
+                qCDebug(LOG_ALPINEAPK)  << "    " << pkg.name << pkg.version;
+            }
+        }
+    } else {
+        handleKAuthHelperError(reply, replyData);
+    }
+
+    m_progressing = false;
+    Q_EMIT progressingChanged(m_progressing);
+}
+
+void AlpineApkUpdater::handleKAuthHelperError(
+        KAuth::ExecuteJob *reply,
+        const QVariantMap &replyData)
+{
+    // error message should be received as part of JSON reply from helper
+    QString message = replyData.value(QLatin1String("errorString"),
+                                            reply->errorString()).toString();
+    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);
+    }
+}
+
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.h b/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.h
new file mode 100644
index 00000000..6ca3ce07
--- /dev/null
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.h
@@ -0,0 +1,197 @@
+/***************************************************************************
+ *   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;
+namespace KAuth {
+    class ExecuteJob;
+}
+
+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 handleKAuthUpgradeHelperProgress(KJob *job, unsigned long percent);
+
+    //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(); }
+
+protected:
+    void handleKAuthHelperError(KAuth::ExecuteJob *reply, const QVariantMap &replyData);
+
+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_progressing = false;
+    qreal m_upgradeProgress = 0.0;
+//    QDateTime m_lastUpdate;
+//    QTimer m_timer;
+//    bool m_canCancel = false;
+};
+
+
+#endif // ALPINEAPKUPDATER_H
diff --git a/libdiscover/backends/AlpineApkBackend/AppstreamDataDownloader.cpp b/libdiscover/backends/AlpineApkBackend/AppstreamDataDownloader.cpp
new file mode 100644
index 00000000..16587994
--- /dev/null
+++ b/libdiscover/backends/AlpineApkBackend/AppstreamDataDownloader.cpp
@@ -0,0 +1,303 @@
+/***************************************************************************
+ *   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 <QEventLoop>
+#include <QFile>
+#include <QFileInfo>
+#include <QJsonArray>
+#include <QJsonDocument>
+#include <QJsonParseError>
+#include <QJsonObject>
+#include <QNetworkAccessManager>
+#include <QNetworkReply>
+#include <QNetworkRequest>
+#include <QStandardPaths>
+#include <QThreadPool>
+#include <QUrl>
+#include <QtConcurrentRun>
+
+#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"
+}
+
+#ifdef ALPINE_LINUX_BUILD
+/**
+ * @brief ApkAppstreamDataDownloader::getApkArch
+ * Reads current configured system apk architecture
+ * from "/etc/apk/arch" file.
+ * @return "x86_64" / "armhf" / "armv7" / "aarch64" and so on
+ */
+static QString getApkArch()
+{
+    static QString s_retArch;
+    if (!s_retArch.isEmpty()) {
+        return s_retArch;
+    }
+    QFile archFile(QStringLiteral("/etc/apk/arch"));
+    if (!archFile.open(QIODevice::ReadOnly)) {
+        // TODO: we could try to guess at compile time by checking presence of
+        // defines like __x86_64__ (check with: "gcc -march=native -dM -E - </dev/null")
+        // but that seems like outside of scope of this small function
+        return s_retArch;
+    }
+    s_retArch = QString::fromUtf8(archFile.readAll()).trimmed();
+    archFile.close();
+    return s_retArch;
+}
+
+static inline void replaceCARCH(QString &in, const QString &arch)
+{
+    // URLs in JSON look like this:
+    // https://appstream.alpinelinux.org/data/edge/main/Components-main-@CARCH@.xml.gz
+    // we need to replace @CARCH@ with a real arch value
+    in.replace(QLatin1String("@CARCH@"), arch);
+}
+#endif
+
+QString AppstreamDataDownloader::getAppStreamCacheDir()
+{
+    QString cachePath = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
+    // ^^ "~/.cache/discover"
+    cachePath += QStringLiteral("/appstream_data");
+    QDir cacheDir(cachePath);
+    if (!cacheDir.exists()) {
+        const bool ok = cacheDir.mkpath(QStringLiteral("."));
+        if (ok) {
+            qCDebug(LOG_ALPINEAPK) << "Created appstream data cache dir:" << cachePath;
+        } else {
+            qCWarning(LOG_ALPINEAPK) << "Failed to create appstream data cache dir:" << cachePath;
+        }
+    }
+    return cachePath;
+}
+
+AppstreamDataDownloader::AppstreamDataDownloader(QObject *parent)
+    : QObject(parent)
+{
+}
+
+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) {
+        QString url = urlValue.toString();
+#ifdef ALPINE_LINUX_BUILD
+        replaceCARCH(url, getApkArch());
+#endif
+        m_urls.append(url);
+        // prefixes are used to avoid name clashes with similar URL paths
+        //    from other JSON files. json file basename is used as prefix
+        m_urlPrefixes.insert(url, jsonBaseName);
+    }
+}
+
+QString AppstreamDataDownloader::getLocalFileSavePath(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::getAppStreamCacheDir()
+        + QDir::separator()
+        + urlPrefix + QLatin1Char('_')
+        + urlInfo.fileName();
+    // aka "~/.cache/discover/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?
+
+    getAppStreamCacheDir(); // 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 = getLocalFileSavePath(urlToDownload);
+        const QFileInfo localFi(localCacheFile);
+        if (localFi.exists()) {
+            int modifiedSecsAgo = localFi.lastModified().secsTo(dtNow);
+            if (modifiedSecsAgo >= m_cacheExpireSeconds) {
+                m_urlsToDownload.append(url);
+            }
+            qCDebug(LOG_ALPINEAPK) << " appstream metadata file: " << localFi.fileName()
+                                   << " was last modified " << modifiedSecsAgo << " seconds ago";
+        } 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.size() > 0) {
+        // some files are outdated; download is needed
+        qCDebug(LOG_ALPINEAPK) << "appstream_downloader: We will need to download "
+                               << m_urlsToDownload.size() << " file(s)";
+
+        // start downloader in a background thread
+        QFuture<void> downloaderFuture = QtConcurrent::run(
+            QThreadPool::globalInstance(), this, &AppstreamDataDownloader::download);
+
+        // directly connect signal to signal
+        QObject::connect(&m_voidFutureWatcher, &QFutureWatcher<void>::finished,
+                         this, &AppstreamDataDownloader::downloadFinished);
+        m_voidFutureWatcher.setFuture(downloaderFuture);
+    } 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;
+    }
+}
+
+// this function runs in background thread
+void AppstreamDataDownloader::download()
+{
+    QNetworkAccessManager nam;
+    QList<QNetworkReply *> replies;
+    QEventLoop loop;
+
+    // start a HTTP GET request for each URL
+    const QStringList urls = m_urlsToDownload;
+    const QString discoverVersion(QStringLiteral("plasma-discover %1").arg(DiscoverVersion::version));
+    for (const QString &url : urls) {
+        const QUrl uurl(url, QUrl::TolerantMode);
+        QNetworkRequest req(uurl);
+        req.setHeader(QNetworkRequest::UserAgentHeader, discoverVersion);
+        replies.push_back(nam.get(req));
+    }
+
+    for (QNetworkReply *rep : replies) {
+        // lambda that stops the loop when all requests have finished
+        // intentionaly use contextless lambda, it is not called otherwise
+        QObject::connect(rep, &QNetworkReply::finished, [&loop, &replies, rep] () {
+            const int numReplies = replies.size();
+            int numFinished = 0;
+            for (QNetworkReply *arep : replies) {
+                if (arep->isFinished()) {
+                    numFinished++;
+                }
+            }
+            if (numFinished >= numReplies) {
+                loop.quit();
+            }
+            qCDebug(LOG_ALPINEAPK).nospace()
+                << "appstream_downloader: " << rep->url()
+                << " request finished (" << numFinished << "/" << numReplies << ")";
+        });
+    }
+
+    // wait for all requests to finish
+    loop.exec();
+
+    qCDebug(LOG_ALPINEAPK) << "appstream_downloader: all downloads have finished!";
+
+    int numErrors = 0;
+    for (QNetworkReply *rep : replies) {
+        const QString localCacheFile = getLocalFileSavePath(rep->url());
+
+        if (rep->error() == QNetworkReply::NoError) {
+            // read received reply contents and save it to file
+            const QByteArray data = rep->readAll();
+            QFile fout(localCacheFile);
+            if (fout.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
+                fout.write(data);
+                fout.close();
+                m_cacheWasUpdated = true;
+                qCDebug(LOG_ALPINEAPK) << "appstream_downloader: saved: " << localCacheFile;
+            } else {
+                qCWarning(LOG_ALPINEAPK) << "appstream_downloader: failed to save:" << localCacheFile;
+            }
+        } else {
+            // download failed for some reason
+            QFileInfo urlinfo(rep->url().path());
+            qCWarning(LOG_ALPINEAPK) << "appstream_downloader: failed to download"
+                                     << urlinfo.fileName() << rep->errorString();
+            numErrors++;
+        }
+    }
+
+    // cleanup: delete all replies objects
+    for (QNetworkReply *arep : replies) {
+        arep->deleteLater();
+    }
+}
diff --git a/libdiscover/backends/AlpineApkBackend/AppstreamDataDownloader.h b/libdiscover/backends/AlpineApkBackend/AppstreamDataDownloader.h
new file mode 100644
index 00000000..05771982
--- /dev/null
+++ b/libdiscover/backends/AlpineApkBackend/AppstreamDataDownloader.h
@@ -0,0 +1,139 @@
+/***************************************************************************
+ *   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 <QFutureWatcher>
+#include <QHash>
+#include <QList>
+#include <QObject>
+#include <QString>
+#include <QUrl>
+
+/**
+ * @brief The AppstreamDataDownloader class
+ *
+ * @details The job of this class is to download appstream data
+ * gzipped XMLs from the Web server hosted somewhere (for
+ * Alpine Linux - at https://appstream.alpinelinux.org).
+ *
+ * Some distros (for example, Alpine Linux) do not provide
+ * appstream data as installable package, instead they host it
+ * on the internet and you have to download and install them
+ * manually.
+ *
+ * Logic behind this decision is beyond my understanding, but we
+ * have what we have... 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", ... ]
+ * }
+ * ---------------------
+ *
+ * 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/appstream_data" (aka QStandardPaths::CacheLocation).
+ * If files are already present in cache and not outdated,
+ * they are not downloaded again. Default cache expiration
+ * time is 2 days.
+ */
+class AppstreamDataDownloader: public QObject
+{
+    Q_OBJECT
+public:
+    explicit AppstreamDataDownloader(QObject *parent = nullptr);
+
+    /**
+     * @brief getAppstreamCacheDir
+     * @details 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 getAppStreamCacheDir();
+
+    /**
+     * @brief cacheWasUpdated
+     * @details 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 is files already present in cache are up to date.
+     */
+    bool cacheWasUpdated() const { return m_cacheWasUpdated; }
+
+    /**
+     * @brief getCacheExpirePeriodSecs
+     * @return cache expire timeout in seconds
+     */
+    qint64 getCacheExpirePeriodSecs() const { return m_cacheExpireSeconds; }
+
+    /**
+     * @brief setCacheExpirePeriodSecs
+     * @param secs - new cache expiration timeout, in seconds.
+     */
+    void setCacheExpirePeriodSecs(qint64 secs);
+
+public Q_SLOTS:
+    /**
+     * @brief start
+     * Start the background thread that does all the 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:
+    /**
+     * @brief downloadFinished
+     * This signal is emitted when download job is finished.
+     * To check if there were actual downloads performed, call
+     * cacheWasUpdated().
+     */
+    void downloadFinished();
+
+private:
+    QString getLocalFileSavePath(const QUrl &urlTodownload);
+    void loadUrlsJson(const QString &path);
+    void download();
+
+protected:
+    qint64 m_cacheExpireSeconds = 2 * 24 * 3600; // 2 days
+    QStringList m_urls;
+    QStringList m_urlsToDownload;
+    QHash<QString, QString> m_urlPrefixes;
+    QFutureWatcher<void> m_voidFutureWatcher;
+    bool m_cacheWasUpdated = false;
+};
+
+#endif
diff --git a/libdiscover/backends/AlpineApkBackend/CMakeLists.txt b/libdiscover/backends/AlpineApkBackend/CMakeLists.txt
new file mode 100644
index 00000000..8602dce7
--- /dev/null
+++ b/libdiscover/backends/AlpineApkBackend/CMakeLists.txt
@@ -0,0 +1,85 @@
+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
+    AlpineApkResource.h
+    AlpineApkReviewsBackend.cpp
+    AlpineApkReviewsBackend.h
+    AlpineApkSourcesBackend.cpp
+    AlpineApkSourcesBackend.h
+    AlpineApkUpdater.cpp
+    AlpineApkUpdater.h
+    AlpineApkTransaction.cpp
+    AlpineApkTransaction.h
+    AppstreamDataDownloader.h
+    AppstreamDataDownloader.cpp
+)
+
+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
+        Qt5::Concurrent
+        KF5::CoreAddons
+        KF5::ConfigCore
+        KF5::AuthCore
+        Discover::Common
+        ApkQt::ApkQt
+        AppStreamQt
+)
+
+# 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
+    ApkQt::ApkQt
+)
+
+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(
+    TARGETS alpineapk_kauth_helper
+    DESTINATION ${KAUTH_HELPER_INSTALL_DIR}
+)
+
+# 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/AlpineApkBackend/org.kde.discover.alpineapkbackend.actions b/libdiscover/backends/AlpineApkBackend/org.kde.discover.alpineapkbackend.actions
new file mode 100644
index 00000000..c9bb5f9f
--- /dev/null
+++ b/libdiscover/backends/AlpineApkBackend/org.kde.discover.alpineapkbackend.actions
@@ -0,0 +1,5 @@
+[org.kde.discover.alpineapkbackend.pkgmgmt]
+Name=Package management
+Description=Install or remove packages, upgrade system
+Policy=auth_admin
+Persistence=session
diff --git a/libdiscover/backends/CMakeLists.txt b/libdiscover/backends/CMakeLists.txt
index 5f87f639..18947339 100644
--- a/libdiscover/backends/CMakeLists.txt
+++ b/libdiscover/backends/CMakeLists.txt
@@ -45,4 +45,14 @@ if(BUILD_FwupdBackend AND TARGET PkgConfig::Fwupd)
     add_subdirectory(FwupdBackend)
 endif()
 
+find_package(ApkQt CONFIG)
+set_package_properties(ApkQt PROPERTIES
+    DESCRIPTION "C++/Qt interface library for Alpine package keeper"
+    URL "https://www.alpinelinux.org"
+    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()
-- 
2.30.0