File indexing completed on 2024-04-28 16:42:51

0001 // SPDX-FileCopyrightText: 2020 Jonah BrĂ¼chert <jbb@kaidan.im>
0002 // SPDX-FileCopyrightText: 2020 Bhushan Shah <bshah@kde.org>
0003 // SPDX-FileCopyrightText: 2021 Alexey Andreyev <aa13q@ya.ru>
0004 //
0005 // SPDX-License-Identifier: LicenseRef-KDE-Accepted-GPL
0006 
0007 #include "call-history-database.h"
0008 
0009 #include <QDebug>
0010 #include <QDir>
0011 #include <QSqlError>
0012 #include <QSqlQuery>
0013 #include <QStandardPaths>
0014 
0015 constexpr auto DATABASE_REVISION = 2; // Keep MIGRATE_TO_LATEST_FROM in sync
0016 #define MIGRATE_TO(n, current)                                                                                                                                 \
0017     if (current < n) {                                                                                                                                         \
0018         qDebug() << "Running migration" << #n;                                                                                                                 \
0019         _migrationV##n(current);                                                                                                                               \
0020     }
0021 #define MIGRATE_TO_LATEST_FROM(current) MIGRATE_TO(2, current)
0022 
0023 CallHistoryDatabase::CallHistoryDatabase(QObject *parent)
0024     : QObject(parent)
0025     , _database(QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), QStringLiteral("calls")))
0026 {
0027     const QString databaseLocation = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/plasmaphonedialer");
0028     if (!QDir().mkpath(databaseLocation)) {
0029         qDebug() << "Could not create the database directory at" << databaseLocation;
0030     }
0031 
0032     _database.setDatabaseName(databaseLocation + QStringLiteral("/calls.sqlite"));
0033     const bool open = _database.open();
0034 
0035     if (!open) {
0036         qWarning() << "Could not open call database" << _database.lastError();
0037     }
0038 
0039     _migrate();
0040 }
0041 
0042 void CallHistoryDatabase::exec(QSqlQuery &query)
0043 {
0044     if (query.lastQuery().isEmpty()) {
0045         // Sending empty queries doesn't make sense
0046         Q_UNREACHABLE();
0047     }
0048     if (!query.exec()) {
0049         qWarning() << "Query" << query.lastQuery() << "resulted in" << query.lastError();
0050     }
0051 }
0052 
0053 DialerTypes::CallDataVector CallHistoryDatabase::fetchCalls()
0054 {
0055     QSqlQuery fetchCalls(_database);
0056     fetchCalls.prepare(
0057         QStringLiteral("SELECT "
0058                        "id, protocol, account, provider, "
0059                        "communicationWith, direction, state, stateReason, "
0060                        "callAttemptDuration, startedAt, duration "
0061                        "FROM History ORDER BY startedAt DESC"));
0062     exec(fetchCalls);
0063 
0064     DialerTypes::CallDataVector calls;
0065     while (fetchCalls.next()) {
0066         DialerTypes::CallData call;
0067         call.id = fetchCalls.value(0).toString();
0068         call.protocol = fetchCalls.value(1).toString();
0069         call.account = fetchCalls.value(2).toString();
0070         call.provider = fetchCalls.value(3).toString();
0071 
0072         call.communicationWith = fetchCalls.value(4).toString();
0073         call.direction = fetchCalls.value(5).value<DialerTypes::CallDirection>();
0074         call.state = fetchCalls.value(6).value<DialerTypes::CallState>();
0075         call.stateReason = fetchCalls.value(7).value<DialerTypes::CallStateReason>();
0076 
0077         call.callAttemptDuration = fetchCalls.value(8).toInt();
0078         call.startedAt = QDateTime::fromString(fetchCalls.value(9).toString(), QStringLiteral("yyyy-MM-ddThh:mm:ss.zzz"));
0079         call.duration = fetchCalls.value(10).toInt();
0080         calls.append(call);
0081     }
0082     return calls;
0083 }
0084 
0085 void CallHistoryDatabase::addCall(const DialerTypes::CallData &callData)
0086 {
0087     QSqlQuery putCall(_database);
0088     putCall.prepare(
0089         QStringLiteral("INSERT INTO History "
0090                        "("
0091                        "protocol, account, provider, "
0092                        "communicationWith, direction, state, stateReason, "
0093                        "callAttemptDuration, startedAt, duration "
0094                        ") "
0095                        "VALUES ("
0096                        ":protocol, :account, :provider, "
0097                        ":communicationWith, :direction, :state, :stateReason, "
0098                        ":callAttemptDuration, :startedAt, :duration "
0099                        ")"));
0100     putCall.bindValue(QStringLiteral(":protocol"), callData.protocol);
0101     putCall.bindValue(QStringLiteral(":account"), callData.account);
0102     putCall.bindValue(QStringLiteral(":provider"), callData.provider);
0103 
0104     putCall.bindValue(QStringLiteral(":communicationWith"), callData.communicationWith);
0105     putCall.bindValue(QStringLiteral(":direction"), static_cast<int>(callData.direction));
0106     putCall.bindValue(QStringLiteral(":state"), static_cast<int>(callData.state));
0107     putCall.bindValue(QStringLiteral(":stateReason"), static_cast<int>(callData.stateReason));
0108 
0109     putCall.bindValue(QStringLiteral(":callAttemptDuration"), callData.callAttemptDuration);
0110     putCall.bindValue(QStringLiteral(":startedAt"), callData.startedAt);
0111     putCall.bindValue(QStringLiteral(":duration"), callData.duration);
0112     exec(putCall);
0113 
0114     Q_EMIT callsChanged();
0115 }
0116 
0117 void CallHistoryDatabase::clear()
0118 {
0119     QSqlQuery clearCalls(_database);
0120     clearCalls.prepare(QStringLiteral("DELETE FROM History"));
0121     exec(clearCalls);
0122 
0123     Q_EMIT callsChanged();
0124 }
0125 
0126 void CallHistoryDatabase::remove(const QString &id)
0127 {
0128     QSqlQuery remove(_database);
0129     remove.prepare(QStringLiteral("DELETE FROM History WHERE id=:id"));
0130     remove.bindValue(QStringLiteral(":id"), id);
0131     exec(remove);
0132 
0133     Q_EMIT callsChanged();
0134 }
0135 
0136 int CallHistoryDatabase::lastId() const
0137 {
0138     QSqlQuery fetch(_database);
0139     fetch.prepare(QStringLiteral("SELECT id FROM History ORDER BY id DESC LIMIT 1"));
0140     exec(fetch);
0141     fetch.first();
0142 
0143     return fetch.value(0).toInt();
0144 }
0145 
0146 QString CallHistoryDatabase::lastCall(const QString &number, int direction) const
0147 {
0148     QSqlQuery fetch(_database);
0149     fetch.prepare(QStringLiteral("SELECT startedAt FROM History WHERE communicationWith=:number and direction = :direction ORDER BY startedAt DESC LIMIT 1"));
0150     fetch.bindValue(QStringLiteral(":number"), number);
0151     fetch.bindValue(QStringLiteral(":direction"), direction);
0152     exec(fetch);
0153     fetch.first();
0154 
0155     return fetch.value(0).toString();
0156 }
0157 
0158 uint CallHistoryDatabase::_guessPreHistoricRevision()
0159 {
0160     uint result = 0;
0161     uint columnCount = 0;
0162     QSqlQuery tableInfo(_database);
0163     tableInfo.prepare(QStringLiteral("PRAGMA table_info('History')")); // http://sqlite.org/pragma.html
0164     exec(tableInfo);
0165     while (tableInfo.next()) {
0166         qDebug() << Q_FUNC_INFO << "found column" << tableInfo.value(1).toString();
0167         columnCount++;
0168     }
0169     if (columnCount == 11) {
0170         qDebug() << Q_FUNC_INFO << "found pre-historic revision v1";
0171         result = 1; // the db from testing version with the v1 scheme but without migrationId
0172     }
0173     return result;
0174 }
0175 
0176 void CallHistoryDatabase::_migrationV1(uint current)
0177 {
0178     if (current < 1) {
0179         QSqlQuery tempTable(_database);
0180         tempTable.prepare(QStringLiteral("CREATE TABLE temp_table AS SELECT * FROM History"));
0181         exec(tempTable);
0182 
0183         QSqlQuery dropOld(_database);
0184         dropOld.prepare(QStringLiteral("DROP TABLE History"));
0185         exec(dropOld);
0186 
0187         QSqlQuery createNew(_database);
0188         createNew.prepare(
0189             QStringLiteral("CREATE TABLE IF NOT EXISTS "
0190                            "History( "
0191                            "id INTEGER PRIMARY KEY AUTOINCREMENT, "
0192                            "protocol TEXT, "
0193                            "account TEXT, "
0194                            "provider TEXT, "
0195                            "communicationWith TEXT, "
0196                            "direction INTEGER, "
0197                            "state INTEGER, "
0198                            "stateReason INTEGER, "
0199                            "callAttemptDuration INTEGER, "
0200                            "startedAt DATETIME, "
0201                            "duration INTEGER "
0202                            ")"));
0203         exec(createNew);
0204 
0205         QSqlQuery copyTemp(_database);
0206         // clang-format off
0207         copyTemp.prepare(
0208             QStringLiteral("INSERT INTO History "
0209                             "("
0210                              "protocol, account, provider, "
0211                              "communicationWith, direction, state, stateReason, "
0212                              "callAttemptDuration, startedAt, duration "
0213                             ") "
0214                            "SELECT "
0215                             "'tel', 'unknown', 'unknown', "
0216                             "temp_table.number, "
0217                             "CASE temp_table.callType " // direction
0218                                 "WHEN 0 " // IncomingRejected
0219                                     "THEN 1 " // Incoming
0220                                 "WHEN 1 " // IncomingAccepted
0221                                     "THEN 1 " // Incoming
0222                                 "WHEN 2 " // Outgoing
0223                                     "THEN 2 " // Outgoing
0224                                 "ELSE 0 " // Unknown
0225                            "END, "
0226                            "7, " // state Terminated
0227                            "CASE temp_table.callType " // stateReason
0228                                 "WHEN 0 " // IncomingRejected
0229                                     "THEN 9 " // Deflected
0230                                 "WHEN 1 " // IncomingAccepted
0231                                     "THEN 3 " // Accepted
0232                                 "WHEN 2 " // Outgoing
0233                                     "THEN 4 " // TerminatedReason
0234                                 "ELSE 0 " // Unknown
0235                                     "END, "
0236                            "0, temp_table.time, temp_table.duration "
0237                            "FROM temp_table"));
0238         // clang-format on
0239         exec(copyTemp);
0240 
0241         QSqlQuery dropTemp(_database);
0242         dropTemp.prepare(QStringLiteral("DROP TABLE temp_table"));
0243         exec(dropTemp);
0244     }
0245 }
0246 
0247 void CallHistoryDatabase::_migrationV2(uint current)
0248 {
0249     MIGRATE_TO(1, current);
0250 }
0251 
0252 void CallHistoryDatabase::_migrate()
0253 {
0254     // Create migration table if necessary
0255     QSqlQuery createMetadata(_database);
0256     createMetadata.prepare(QStringLiteral("CREATE TABLE IF NOT EXISTS Metadata (migrationId INTEGER NOT NULL)"));
0257     exec(createMetadata);
0258 
0259     // Find out current revision
0260     QSqlQuery currentRevision(_database);
0261     currentRevision.prepare(QStringLiteral("SELECT migrationId FROM Metadata ORDER BY migrationId DESC LIMIT 1"));
0262     exec(currentRevision);
0263     currentRevision.first();
0264 
0265     uint revision = 0;
0266     if (currentRevision.isValid()) {
0267         revision = currentRevision.value(0).toUInt();
0268     }
0269 
0270     if (revision == 0) {
0271         revision = _guessPreHistoricRevision();
0272     }
0273 
0274     qDebug() << "current database revision" << revision;
0275 
0276     // Run migration if necessary
0277     if (revision >= DATABASE_REVISION) {
0278         return;
0279     }
0280 
0281     MIGRATE_TO_LATEST_FROM(revision);
0282 
0283     // Update migration info if necessary
0284     QSqlQuery update(_database);
0285     update.prepare(QStringLiteral("INSERT INTO Metadata (migrationId) VALUES (:migrationId)"));
0286     update.bindValue(QStringLiteral(":migrationId"), DATABASE_REVISION);
0287     exec(update);
0288 }