Commit e8750bad authored by LNJ's avatar LNJ 💬

Rewrite database models to not block the GUI

This rewrites parts of the main database class. All classes working on
the database have been moved to the new database thread, so inserting
records into the database isn't blocking the user interface anymore.
What also improved the performance *massively* is the use of SQLite
transactions, when inserting multiple records. So inserting is so fast
now that it actually wouldn't necessarily require another thread, but
with this we're safe in the future.

The message model and the roster model have been splitted up into a
database manager and the list model that is used to present cached data
to the user. The XMPP managers are connected to both the model and the
db classes, so both are updated in parallel.

There are also two new classes for the RosterItem and the Message,
because we need to cache those in the models in a vector. The Message
class inherits from QXmppMessage, so we can now use the same class for
sending messages and inserting messages into the database.

Closes #273.
parent a6ba2a0a
......@@ -5,11 +5,15 @@ set(KAIDAN_SOURCES
src/ClientWorker.cpp
src/AvatarFileStorage.cpp
src/Database.cpp
src/RosterItem.cpp
src/RosterModel.cpp
src/RosterDb.cpp
src/RosterManager.cpp
src/RegistrationManager.cpp
src/MessageHandler.cpp
src/Message.cpp
src/MessageModel.cpp
src/MessageDb.cpp
src/MessageHandler.cpp
src/Notifications.cpp
src/PresenceCache.cpp
src/DiscoveryManager.cpp
......@@ -21,9 +25,11 @@ set(KAIDAN_SOURCES
src/TransferCache.cpp
src/DownloadManager.cpp
src/QmlUtils.cpp
src/Utils.cpp
# needed to trigger moc generation
# needed to trigger moc generation / to be displayed in IDEs
src/Enums.h
src/Globals.h
# kaidan QXmpp extensions (need to be merged into QXmpp upstream)
src/qxmpp-exts/QXmppHttpUploadIq.cpp
......@@ -31,7 +37,7 @@ set(KAIDAN_SOURCES
src/qxmpp-exts/QXmppUploadManager.cpp
src/qxmpp-exts/QXmppColorGenerator.cpp
# hsluv-c required for color generation
# hsluv-c required for color generation
src/hsluv-c/hsluv.c
)
......
......@@ -80,24 +80,12 @@ ClientWorker::ClientWorker(Caches *caches, Kaidan *kaidan, bool enableLogging, Q
client->versionManager().setClientVersion(VERSION_STRING);
client->versionManager().setClientOs(QSysInfo::prettyProductName());
#if QXMPP_VERSION >= 0x000904
#if QXMPP_VERSION >= QT_VERSION_CHECK(1, 0, 0)
// Client State Indication
connect(app, &QGuiApplication::applicationStateChanged, this, &ClientWorker::setCsiState);
#endif
}
ClientWorker::~ClientWorker()
{
delete client;
delete logger;
delete rosterManager;
delete msgHandler;
delete discoManager;
delete vCardManager;
delete uploadManager;
delete downloadManager;
}
void ClientWorker::main()
{
// initialize random generator
......@@ -199,7 +187,7 @@ QString ClientWorker::generateRandomString(unsigned int length) const
return randomString;
}
#if QXMPP_VERSION >= 0x000904 // after QXmpp v0.9.4
#if QXMPP_VERSION >= QT_VERSION_CHECK(1, 0, 0)
void ClientWorker::setCsiState(Qt::ApplicationState state)
{
if (state == Qt::ApplicationActive)
......
......@@ -34,7 +34,6 @@
// Qt
#include <QObject>
#include <QTimer>
#include <QThread>
#include <QSettings>
class QGuiApplication;
// QXmpp
......@@ -61,24 +60,6 @@ class DownloadManager;
using namespace Enums;
class ClientThread : public QThread
{
Q_OBJECT
friend ClientWorker;
public:
ClientThread()
{
setObjectName("QXmppClient");
}
protected:
void run() override
{
exec();
}
};
/**
* The ClientWorker is used as a QObject-based worker on the ClientThread.
*/
......@@ -88,14 +69,16 @@ class ClientWorker : public QObject
public:
struct Caches {
Caches(Database *database, QObject *parent = nullptr)
: msgModel(new MessageModel(database->getDatabase(), parent)),
rosterModel(new RosterModel(database->getDatabase(), parent)),
Caches(Kaidan *kaidan, RosterDb *rosterDb, MessageDb *msgDb,
QObject *parent = nullptr)
: msgModel(new MessageModel(kaidan, msgDb, parent)),
rosterModel(new RosterModel(rosterDb, parent)),
avatarStorage(new AvatarFileStorage(parent)),
presCache(new PresenceCache(parent)),
transferCache(new TransferCache(parent)),
settings(new QSettings(APPLICATION_NAME, APPLICATION_NAME))
{
rosterModel->setMessageModel(msgModel);
}
~Caches()
......@@ -134,8 +117,6 @@ public:
ClientWorker(Caches *caches, Kaidan *kaidan, bool enableLogging, QGuiApplication *app,
QObject *parent = nullptr);
~ClientWorker();
public slots:
/**
* Main function of the client thread
......@@ -178,7 +159,7 @@ private slots:
*/
void onConnectionError(QXmppClient::Error error);
#if QXMPP_VERSION >= 0x000904 // after QXmpp v0.9.4
#if QXMPP_VERSION >= QT_VERSION_CHECK(1, 0, 0)
/**
* Uses the QGuiApplication state to reduce network traffic when window is minimized
*/
......
This diff is collapsed.
......@@ -36,6 +36,10 @@
class QSqlQuery;
/**
* The Database class manages the SQL database. It opens the database and converts old
* formats.
*/
class Database : public QObject
{
Q_OBJECT
......@@ -44,15 +48,52 @@ public:
Database(QObject *parent = nullptr);
~Database();
QSqlDatabase* getDatabase();
bool needToConvert();
void convertDatabase();
/**
* Opens the database for reading and writing and guarantees the database to be
* up-to-date.
*/
void openDatabase();
/**
* Begins a transaction if none has been started.
*/
void transaction();
/**
* Commits the transaction if every transaction has been finished.
*/
void commit();
private:
/**
* @return true if the database has to be converted using @c convertDatabase()
* because the database is not up-to-date.
*/
bool needToConvert();
/**
* Converts the database to latest model.
*/
void convertDatabase();
/**
* Loads the database information and detects the database version.
*/
void loadDatabaseInfo();
/**
* Creates the database information table which contains the database version.
*/
void createDbInfoTable();
/**
* Creates a new database without content.
*/
void createNewDatabase();
/*
* Upgrades the database to the next version.
*/
void convertDatabaseToV2();
void convertDatabaseToV3();
void convertDatabaseToV4();
......@@ -62,10 +103,18 @@ private:
void convertDatabaseToV8();
void convertDatabaseToV9();
void convertDatabaseToV10();
void execQuery(QSqlQuery &query);
QSqlDatabase database;
int version;
QSqlDatabase m_database;
/**
* -1 : Database not loaded.
* 0 : Database not existent.
* 1 : Old database before Kaidan v0.3.
* > 1 : Database version.
*/
int m_version = -1;
int m_transactions = 0;
};
#endif // DATABASE_H
......@@ -34,6 +34,8 @@
#include "TransferCache.h"
#include "MessageModel.h"
#include "Globals.h"
// C++
#include <utility>
// Qt
#include "QDir"
#include "QStandardPaths"
......@@ -64,7 +66,7 @@ DownloadManager::~DownloadManager()
delete thread;
}
void DownloadManager::startDownload(const QString msgId, const QString url)
void DownloadManager::startDownload(const QString &msgId, const QString &url)
{
// don't download the same file twice and in parallel
if (downloads.keys().contains(msgId)) {
......@@ -83,35 +85,43 @@ void DownloadManager::startDownload(const QString msgId, const QString url)
dl->moveToThread(thread);
downloads[msgId] = dl;
connect(dl, &DownloadJob::finished, this, [this, dl, msgId]() {
MessageModel::Message msgUpdate;
msgUpdate.mediaLocation = dl->downloadLocation();
emit model->updateMessageRequested(msgId, msgUpdate);
connect(dl, &DownloadJob::finished, this, [=]() {
const QString mediaLocation = dl->downloadLocation();
emit model->updateMessageRequested(msgId, [=] (Message &msg) {
msg.setMediaLocation(mediaLocation);
});
abortDownload(msgId);
});
connect(dl, &DownloadJob::failed, this, [this, msgId]() {
connect(dl, &DownloadJob::failed, this, [=]() {
abortDownload(msgId);
});
emit dl->startDownloadRequested();
}
void DownloadManager::abortDownload(const QString msgId)
void DownloadManager::abortDownload(const QString &msgId)
{
DownloadJob *job = downloads.value(msgId);
if (job != nullptr)
delete job;
delete job;
downloads.remove(msgId);
emit transferCache->removeJobRequested(msgId);
}
DownloadJob::DownloadJob(QString msgId, QUrl source, QString filePath,
DownloadJob::DownloadJob(QString msgId,
QUrl source,
QString filePath,
QNetworkAccessManager *netMngr,
TransferCache *transferCache, Kaidan *kaidan)
: QObject(nullptr), msgId(msgId), source(source), filePath(filePath),
netMngr(netMngr), transferCache(transferCache), kaidan(kaidan), file()
TransferCache *transferCache,
Kaidan *kaidan)
: QObject(nullptr),
msgId(std::move(msgId)),
source(std::move(source)),
filePath(std::move(filePath)),
netMngr(netMngr),
transferCache(transferCache),
kaidan(kaidan)
{
connect(this, &DownloadJob::startDownloadRequested,
this, &DownloadJob::startDownload);
......@@ -149,19 +159,19 @@ void DownloadJob::startDownload()
this, [this] (qint64 bytesReceived, qint64 bytesTotal) {
emit transferCache->setJobProgressRequested(msgId, bytesReceived, bytesTotal);
});
connect(reply, &QNetworkReply::finished, this, [this] () {
connect(reply, &QNetworkReply::finished, this, [=] () {
emit transferCache->removeJobRequested(msgId);
emit finished();
});
connect(reply, QOverload<QNetworkReply::NetworkError>::of(&QNetworkReply::error),
[this, reply] () {
this, [=] () {
emit transferCache->removeJobRequested(msgId);
qWarning() << "Couldn't download file:" << reply->errorString();
emit kaidan->passiveNotificationRequested(
tr("Download failed: %1").arg(reply->errorString()));
emit finished();
});
connect(reply, &QNetworkReply::readyRead, this, [this, reply](){
connect(reply, &QNetworkReply::readyRead, this, [=](){
file.write(reply->readAll());
});
}
......
......@@ -46,8 +46,11 @@ class DownloadJob : public QObject
{
Q_OBJECT
public:
DownloadJob(QString msgId, QUrl source, QString filePath,
QNetworkAccessManager *netMngr, TransferCache *transferCache,
DownloadJob(QString msgId,
QUrl source,
QString filePath,
QNetworkAccessManager *netMngr,
TransferCache *transferCache,
Kaidan *kaidan);
QString downloadLocation() const;
......@@ -99,8 +102,8 @@ signals:
void abortDownloadRequested(const QString msgId);
public slots:
void startDownload(const QString msgId, const QString url);
void abortDownload(const QString msgId);
void startDownload(const QString &msgId, const QString &url);
void abortDownload(const QString &msgId);
private:
DownloadThread *thread;
......
......@@ -31,6 +31,8 @@
#ifndef GLOBALS_H
#define GLOBALS_H
#include <QString>
// Application information
#define APPLICATION_DESCRIPTION "A simple, user-friendly Jabber/XMPP client"
......@@ -47,9 +49,12 @@ const QString KAIDAN_RESOURCE_RANDOM_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefgh
#define NS_CARBONS "urn:xmpp:carbons:2"
#define NS_REGISTER "jabber:iq:register"
/**
* Map of JIDs to contact names
*/
typedef QHash<QString, QString> ContactMap;
// SQL
#define DB_CONNECTION "kaidan-messages"
#define DB_FILENAME "messages.sqlite3"
#define DB_MSG_QUERY_LIMIT 20
#define DB_TABLE_INFO "dbinfo"
#define DB_TABLE_ROSTER "Roster"
#define DB_TABLE_MESSAGES "Messages"
#endif // GLOBALS_H
......@@ -34,45 +34,59 @@
#include <QDebug>
#include <QGuiApplication>
#include <QSettings>
#include <QThread>
// QXmpp
#include <QXmppClient.h>
#include "qxmpp-exts/QXmppColorGenerator.h"
#include <QXmppClient.h>
// Kaidan
#include "AvatarFileStorage.h"
#include "Database.h"
#include "RosterModel.h"
#include "MessageDb.h"
#include "MessageModel.h"
#include "PresenceCache.h"
#include "QmlUtils.h"
#include "RosterDb.h"
#include "RosterModel.h"
Kaidan *Kaidan::s_instance = nullptr;
Kaidan::Kaidan(QGuiApplication *app, bool enableLogging, QObject *parent)
: QObject(parent), m_utils(new QmlUtils(this)), database(new Database())
: QObject(parent),
m_utils(new QmlUtils(this)),
m_database(new Database()),
m_dbThrd(new QThread()),
m_msgDb(new MessageDb()),
m_rosterDb(new RosterDb(m_database)),
m_cltThrd(new QThread())
{
Q_ASSERT(!Kaidan::s_instance);
Kaidan::s_instance = this;
// Database setup
database->openDatabase();
if (database->needToConvert())
database->convertDatabase();
m_database->moveToThread(m_dbThrd);
m_msgDb->moveToThread(m_dbThrd);
m_rosterDb->moveToThread(m_dbThrd);
connect(m_dbThrd, &QThread::started, m_database, &Database::openDatabase);
m_dbThrd->setObjectName("SqlDatabase");
m_dbThrd->start();
// Caching components
caches = new ClientWorker::Caches(database, this);
m_caches = new ClientWorker::Caches(this, m_rosterDb, m_msgDb, this);
// Connect the avatar changed signal of the avatarStorage with the NOTIFY signal
// of the Q_PROPERTY for the avatar storage (so all avatars are updated in QML)
connect(caches->avatarStorage, &AvatarFileStorage::avatarIdsChanged,
connect(m_caches->avatarStorage, &AvatarFileStorage::avatarIdsChanged,
this, &Kaidan::avatarStorageChanged);
//
// Load settings
//
creds.jid = caches->settings->value(KAIDAN_SETTINGS_AUTH_JID).toString();
creds.jidResource = caches->settings->value(KAIDAN_SETTINGS_AUTH_RESOURCE)
creds.jid = m_caches->settings->value(KAIDAN_SETTINGS_AUTH_JID).toString();
creds.jidResource = m_caches->settings->value(KAIDAN_SETTINGS_AUTH_RESOURCE)
.toString();
creds.password = QString(QByteArray::fromBase64(caches->settings->value(
creds.password = QString(QByteArray::fromBase64(m_caches->settings->value(
KAIDAN_SETTINGS_AUTH_PASSWD).toString().toUtf8()));
// use Kaidan as resource, if no set
if (creds.jidResource.isEmpty())
......@@ -83,20 +97,21 @@ Kaidan::Kaidan(QGuiApplication *app, bool enableLogging, QObject *parent)
// Start ClientWorker on new thread
//
cltThrd = new ClientThread();
client = new ClientWorker(caches, this, enableLogging, app);
client->setCredentials(creds);
connect(client, &ClientWorker::disconnReasonChanged, this, &Kaidan::setDisconnReason);
m_client = new ClientWorker(m_caches, this, enableLogging, app);
m_client->setCredentials(creds);
m_client->moveToThread(m_cltThrd);
connect(m_client, &ClientWorker::disconnReasonChanged, this, &Kaidan::setDisconnReason);
connect(m_cltThrd, &QThread::started, m_client, &ClientWorker::main);
client->moveToThread(cltThrd);
connect(cltThrd, &QThread::started, client, &ClientWorker::main);
cltThrd->start();
m_client->setObjectName("XmppClient");
m_cltThrd->start();
}
Kaidan::~Kaidan()
{
delete caches;
delete database;
delete m_caches;
delete m_database;
Kaidan::s_instance = nullptr;
}
......@@ -113,25 +128,22 @@ void Kaidan::mainConnect()
if (connectionState != ConnectionState::StateDisconnected) {
qWarning() << "[main] Tried to connect, even if still connected!"
<< "Requesting disconnect.";
emit client->disconnectRequested();
emit m_client->disconnectRequested();
}
emit client->credentialsUpdated(creds);
emit client->connectRequested();
// update own JID to display correct messages
caches->msgModel->setOwnJid(creds.jid);
emit m_client->credentialsUpdated(creds);
emit m_client->connectRequested();
}
void Kaidan::mainDisconnect(bool openLogInPage)
{
// disconnect the client if connected or connecting
if (connectionState != ConnectionState::StateDisconnected)
emit client->disconnectRequested();
emit m_client->disconnectRequested();
if (openLogInPage) {
// clear password
caches->settings->remove(KAIDAN_SETTINGS_AUTH_PASSWD);
m_caches->settings->remove(KAIDAN_SETTINGS_AUTH_PASSWD);
setPassword(QString());
// trigger log in page
emit newCredentialsNeeded();
......@@ -140,7 +152,7 @@ void Kaidan::mainDisconnect(bool openLogInPage)
void Kaidan::setConnectionState(QXmppClient::State state)
{
this->connectionState = (ConnectionState) state;
this->connectionState = static_cast<ConnectionState>(state);
emit connectionStateChanged();
// Open the possibly cached URI when connected.
......@@ -173,7 +185,7 @@ void Kaidan::setJidResource(const QString &jidResource)
// to set the first try flag and can save it.
creds.jidResource = jidResource;
caches->settings->setValue(KAIDAN_SETTINGS_AUTH_RESOURCE, jidResource);
m_caches->settings->setValue(KAIDAN_SETTINGS_AUTH_RESOURCE, jidResource);
}
void Kaidan::setPassword(const QString &password)
......@@ -183,17 +195,6 @@ void Kaidan::setPassword(const QString &password)
creds.isFirstTry = true;
}
void Kaidan::setChatPartner(const QString &chatPartner)
{
// check if different
if (this->chatPartner == chatPartner)
return;
this->chatPartner = chatPartner;
emit chatPartnerChanged(chatPartner);
caches->msgModel->applyRecipientFilter(chatPartner);
}
quint8 Kaidan::getDisconnReason() const
{
return static_cast<quint8>(disconnReason);
......
......@@ -53,8 +53,8 @@ using namespace Enums;
* @brief This class will initiate the complete back-end, including the @see Database
* connection, viewing models (@see MessageModel, @see RosterModel), etc.
*
* This class will run in the main thread, only the XMPP connection runs in another
* thread (@see ClientThread).
* This class will run in the main thread, the XMPP connection and the database managers
* run in other threads.
*/
class Kaidan : public QObject
{
......@@ -72,7 +72,6 @@ class Kaidan : public QObject
Q_PROPERTY(QString jid READ getJid WRITE setJid NOTIFY jidChanged)
Q_PROPERTY(QString jidResource READ getJidResource WRITE setJidResource NOTIFY jidResourceChanged)
Q_PROPERTY(QString password READ getPassword WRITE setPassword NOTIFY passwordChanged)
Q_PROPERTY(QString chatPartner READ getChatPartner WRITE setChatPartner NOTIFY chatPartnerChanged)
Q_PROPERTY(bool uploadServiceFound READ getUploadServiceFound NOTIFY uploadServiceFoundChanged)
public:
......@@ -158,49 +157,34 @@ public:
return creds.password;
}
/**
* Set the currently opened chat
*
* This will set a filter on the database to only view the related messages.
*/
void setChatPartner(const QString &jid);
/**
* Get the currrently opened chat
*/
QString getChatPartner() const
{
return chatPartner;
}
RosterModel* getRosterModel() const
{
return caches->rosterModel;
return m_caches->rosterModel;
}
MessageModel* getMessageModel() const
{
return caches->msgModel;
return m_caches->msgModel;
}
AvatarFileStorage* getAvatarStorage() const
{
return caches->avatarStorage;
return m_caches->avatarStorage;
}
PresenceCache* getPresenceCache() const
{
return caches->presCache;
return m_caches->presCache;
}
TransferCache* getTransferCache() const
{
return caches->transferCache;
return m_caches->transferCache;
}
QSettings* getSettings() const
{
return caches->settings;
return m_caches->settings;
}
QmlUtils* getUtils() const
......@@ -253,11 +237,6 @@ signals:
*/
void passwordChanged();
/**
* Emitted when the currently opnened chat has changed
*/
void chatPartnerChanged(QString chatPartner);
/**
* Emitted when there are no (correct) credentials and new are needed
*
......@@ -407,13 +386,15 @@ private:
void connectDatabases();
QmlUtils *m_utils;
Database *database;
ClientWorker::Caches *caches;
ClientThread *cltThrd;
ClientWorker *client;
Database *m_database;
QThread *m_dbThrd;
MessageDb *m_msgDb;
RosterDb *m_rosterDb;
QThread *m_cltThrd;
ClientWorker::Caches *m_caches;
ClientWorker *m_client;
ClientWorker::Credentials creds;
QString chatPartner;
QString openUriCache;
bool uploadServiceFound = false;
ConnectionState connectionState = ConnectionState::StateDisconnected;
......
/*
* Kaidan - A user-friendly XMPP client for every device!
*
* Copyright (C) 2016-2019 Kaidan developers and contributors
* (see the LICENSE file for a full list of copyright authors)
*
* Kaidan 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 3 of the License, or
* (at your option) any later version.
*
* In addition, as a special exception, the author of Kaidan gives
* permission to link the code of its release with the OpenSSL
* project's "OpenSSL" library (or with modified versions of it that
* use the same license as the "OpenSSL" library), and distribute the
* linked executables. You must obey the GNU General Public License in
* all respects for all of the code used other than "OpenSSL". If you
* modify this file, you may extend this exception to your version of
* the file, but you are not obligated to do so. If you do not wish to
* do so, delete this exception statement from your version.
*
* Kaidan 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 Kaidan. If not, see <http://www.gnu.org/licenses/>.
*/
#include "Message.h"
#include <QMimeType>
MessageType Message::mediaTypeFromMimeType(const QMimeType &type)
{
if (type.inherits("image/jpeg") || type.inherits("image/png") ||
type.inherits("image/gif"))
return MessageType::MessageImage;
if (type.inherits("audio/flac") || type.inherits("audio/mp4") ||
type.inherits("audio/ogg") || type.inherits("audio/wav") ||
type.inherits("audio/mpeg") || type.inherits("audio/webm"))
return MessageType::MessageAudio;
if (type.inherits("video/mpeg") || type.inherits("video/x-msvideo") ||
type.inherits("video/quicktime") || type.inherits("video/mp4") ||
type.inherits("video/x-matroska"))
return MessageType::MessageVideo;
if (type.inherits("text/plain"))
return MessageType::MessageDocument;
return MessageType::MessageFile;
}
bool Message::operator==(const Message &m) const
{
return m.id() == id() &&
m.body() == body() &&
m.from() == from() &&
m.to() == to() &&
m.type() == type() &&
m.stamp() == stamp() &&
m.outOfBandUrl() == outOfBandUrl() &&
m.isSent() == isSent() &&
m.isDelivered() == isDelivered() &&
m.mediaType() == mediaType() &&
m.mediaContentType() == mediaContentType() &&
m.mediaLocation() == mediaLocation() &&
m.isEdited() == isEdited() &&
m.spoilerHint() == spoilerHint() &&
m.isSpoiler() == isSpoiler();
}
bool Message::operator!=(const Message &m) const
{
return !operator==(m);
}
MessageType Message::mediaType() const
{
return m_mediaType;
}
void Message::setMediaType(MessageType mediaType)
{
m_mediaType = mediaType;
}
bool Message::sentByMe() const
{
return m_sentByMe;
}
void Message::setSentByMe(bool sentByMe)
{