presentation/QuickContactGraphSuggestions.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | // Relationship suggestions of QuickContactGraph (Sprint 103 S103.5.8, I-8): | ||
| 2 | // opt-in, computed on a worker from contacts.db, the mail cache and the | ||
| 3 | // calendar (RelationSuggestions); only dismissals are stored. | ||
| 4 | |||
| 5 | #include "QuickContactGraph.h" | ||
| 6 | |||
| 7 | #include "data/ContactStore.h" | ||
| 8 | #include "data/RelationSuggestions.h" | ||
| 9 | |||
| 10 | #include <QDateTime> | ||
| 11 | #include <QElapsedTimer> | ||
| 12 | #include <QLoggingCategory> | ||
| 13 | #include <QPointer> | ||
| 14 | #include <QSettings> | ||
| 15 | #include <QSqlDatabase> | ||
| 16 | #include <QUuid> | ||
| 17 | #include <algorithm> | ||
| 18 | |||
| 19 | Q_DECLARE_LOGGING_CATEGORY(lcGraph) | ||
| 20 | |||
| 21 | namespace { | ||
| 22 | |||
| 23 | // A read-only connection of its own on the worker thread; "" when missing. | ||
| 24 | 10 | QSqlDatabase openReadOnly(const QString &path, const QString &name) { | |
| 25 |
1/1✓ Branch 5 → 6 taken 10 times.
|
10 | QSqlDatabase db = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), name); |
| 26 |
1/1✓ Branch 8 → 9 taken 10 times.
|
10 | db.setDatabaseName(path); |
| 27 |
1/1✓ Branch 12 → 13 taken 10 times.
|
10 | db.setConnectOptions(QStringLiteral("QSQLITE_OPEN_READONLY")); |
| 28 |
4/7✓ Branch 16 → 17 taken 10 times.
✗ Branch 16 → 19 not taken.
✓ Branch 17 → 18 taken 10 times.
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 20 taken 10 times.
✗ Branch 21 → 22 not taken.
✓ Branch 21 → 24 taken 10 times.
|
10 | if (path.isEmpty() || !db.open()) |
| 29 | ✗ | return {}; | |
| 30 |
1/1✓ Branch 24 → 25 taken 10 times.
|
10 | return db; |
| 31 | 10 | } | |
| 32 | |||
| 33 | } // namespace | ||
| 34 | |||
| 35 | 1603 | QuickContactGraph::~QuickContactGraph() { m_pool.waitForDone(); } | |
| 36 | |||
| 37 | 109 | RelationGraph::Layers QuickContactGraph::layers() const { | |
| 38 | 109 | RelationGraph::Layers layers; | |
| 39 | 109 | layers.suggestions = m_suggestions; | |
| 40 | 109 | return layers; | |
| 41 | } | ||
| 42 | |||
| 43 | 796 | void QuickContactGraph::setSuggestionSources(std::function<QString()> mailCachePath, const QString &calendarPath) { | |
| 44 | 796 | m_mailCachePath = std::move(mailCachePath); | |
| 45 | 796 | m_calendarPath = calendarPath; | |
| 46 | 796 | } | |
| 47 | |||
| 48 | 6 | void QuickContactGraph::setSuggestions(bool on) { | |
| 49 |
2/4✓ Branch 2 → 3 taken 6 times.
✗ Branch 2 → 4 not taken.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 6 times.
|
6 | if (on == m_suggestions || !m_suggestionsAllowed) |
| 50 | ✗ | return; | |
| 51 | 6 | m_suggestions = on; | |
| 52 |
2/2✓ Branch 5 → 6 taken 6 times.
✓ Branch 11 → 12 taken 6 times.
|
12 | QSettings().setValue(QStringLiteral("contacts/graphSuggestions"), on); |
| 53 |
8/8✓ Branch 16 → 17 taken 6 times.
✓ Branch 20 → 21 taken 6 times.
✓ Branch 21 → 22 taken 6 times.
✓ Branch 22 → 23 taken 3 times.
✓ Branch 22 → 24 taken 3 times.
✓ Branch 25 → 26 taken 6 times.
✓ Branch 29 → 18 taken 6 times.
✓ Branch 29 → 30 taken 6 times.
|
12 | qCInfo(lcGraph) << "Relationship suggestions" << (on ? "on" : "off"); |
| 54 |
2/2✓ Branch 30 → 31 taken 3 times.
✓ Branch 30 → 32 taken 3 times.
|
6 | if (on) { |
| 55 | 3 | computeSuggestions(); | |
| 56 | } else { | ||
| 57 | 3 | m_suggestionCounts.clear(); | |
| 58 | 3 | m_input.suggestions.clear(); | |
| 59 | 3 | ++m_suggestionRun; // a running worker's answer is dropped | |
| 60 | 3 | m_suggestionDone = m_suggestionRun; | |
| 61 | } | ||
| 62 | 6 | emit suggestionsChanged(); | |
| 63 |
2/6✗ Branch 37 → 38 not taken.
✓ Branch 37 → 39 taken 6 times.
✗ Branch 38 → 39 not taken.
✗ Branch 38 → 40 not taken.
✓ Branch 41 → 42 taken 6 times.
✗ Branch 41 → 43 not taken.
|
6 | if (!m_centerKey.isEmpty() || m_overview) |
| 64 | 6 | rebuild(); | |
| 65 | } | ||
| 66 | |||
| 67 | 6 | bool QuickContactGraph::suggestionsExplained() const { | |
| 68 |
3/3✓ Branch 2 → 3 taken 6 times.
✓ Branch 8 → 9 taken 6 times.
✓ Branch 9 → 10 taken 6 times.
|
12 | return QSettings().value(QStringLiteral("contacts/graphSuggestionsExplained"), false).toBool(); |
| 69 | } | ||
| 70 | |||
| 71 | 1 | void QuickContactGraph::setSuggestionsExplained(bool shown) { | |
| 72 |
2/2✓ Branch 2 → 3 taken 1 time.
✓ Branch 8 → 9 taken 1 time.
|
2 | QSettings().setValue(QStringLiteral("contacts/graphSuggestionsExplained"), shown); |
| 73 | 1 | emit suggestionsChanged(); | |
| 74 | 1 | } | |
| 75 | |||
| 76 | 5 | void QuickContactGraph::computeSuggestions() { | |
| 77 |
3/6✓ Branch 3 → 4 taken 5 times.
✗ Branch 3 → 5 not taken.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 5 times.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 5 times.
|
5 | if (m_databasePath.isEmpty() || !m_suggestionsAllowed) |
| 78 | ✗ | return; | |
| 79 | 5 | const int run = ++m_suggestionRun; | |
| 80 | 5 | const QString contactsPath = m_databasePath; | |
| 81 |
2/3✓ Branch 11 → 12 taken 5 times.
✗ Branch 11 → 13 not taken.
✓ Branch 12 → 14 taken 5 times.
|
5 | const QString mailPath = m_mailCachePath ? m_mailCachePath() : QString(); |
| 82 | 5 | const QString calendarPath = m_calendarPath; | |
| 83 |
1/1✓ Branch 15 → 16 taken 5 times.
|
5 | const QString me = meKey(); |
| 84 |
1/1✓ Branch 16 → 17 taken 5 times.
|
5 | QPointer<QuickContactGraph> self(this); |
| 85 |
1/9✓ Branch 22 → 23 taken 5 times.
✗ Branch 33 → 34 not taken.
✗ Branch 33 → 35 not taken.
✗ Branch 36 → 37 not taken.
✗ Branch 36 → 38 not taken.
✗ Branch 39 → 40 not taken.
✗ Branch 39 → 41 not taken.
✗ Branch 42 → 43 not taken.
✗ Branch 42 → 44 not taken.
|
5 | m_pool.start([self, run, contactsPath, mailPath, calendarPath, me] { |
| 86 | 5 | QElapsedTimer timer; | |
| 87 | 5 | timer.start(); | |
| 88 | 5 | QList<RelationSuggestions::Suggestion> found; | |
| 89 |
2/2✓ Branch 3 → 4 taken 5 times.
✓ Branch 4 → 5 taken 5 times.
|
5 | const QString suffix = QUuid::createUuid().toString(QUuid::WithoutBraces); |
| 90 | { | ||
| 91 |
1/1✓ Branch 5 → 6 taken 5 times.
|
5 | ContactStore store; |
| 92 |
2/3✓ Branch 6 → 7 taken 5 times.
✓ Branch 7 → 8 taken 5 times.
✗ Branch 7 → 46 not taken.
|
5 | if (store.open(contactsPath)) { |
| 93 |
1/1✓ Branch 8 → 9 taken 5 times.
|
5 | const QSqlDatabase contacts = store.database(); |
| 94 |
1/1✓ Branch 9 → 10 taken 5 times.
|
5 | const RelationSuggestions::Cards cards = RelationSuggestions::readCards(contacts, me); |
| 95 | 5 | const qint64 since = QDateTime::currentSecsSinceEpoch() - 2LL * 366 * 24 * 3600; // two years | |
| 96 | 5 | QHash<RelationSuggestions::Pair, RelationSuggestions::Counts> counts; | |
| 97 | { | ||
| 98 |
2/2✓ Branch 14 → 15 taken 5 times.
✓ Branch 15 → 16 taken 5 times.
|
5 | QSqlDatabase mail = openReadOnly(mailPath, QStringLiteral("suggest-mail-") + suffix); |
| 99 |
2/3✓ Branch 19 → 20 taken 5 times.
✓ Branch 20 → 21 taken 5 times.
✗ Branch 20 → 22 not taken.
|
5 | if (mail.isOpen()) |
| 100 |
1/1✓ Branch 21 → 22 taken 5 times.
|
5 | RelationSuggestions::countMails(mail, cards, since, &counts); |
| 101 |
2/2✓ Branch 25 → 26 taken 5 times.
✓ Branch 26 → 27 taken 5 times.
|
5 | QSqlDatabase calendar = openReadOnly(calendarPath, QStringLiteral("suggest-cal-") + suffix); |
| 102 |
2/3✓ Branch 30 → 31 taken 5 times.
✓ Branch 31 → 32 taken 5 times.
✗ Branch 31 → 33 not taken.
|
5 | if (calendar.isOpen()) |
| 103 |
1/1✓ Branch 32 → 33 taken 5 times.
|
5 | RelationSuggestions::countEvents(calendar, cards, since, &counts); |
| 104 | 5 | } | |
| 105 |
2/2✓ Branch 36 → 37 taken 5 times.
✓ Branch 37 → 38 taken 5 times.
|
10 | found = RelationSuggestions::select(counts, RelationSuggestions::relatedPairs(contacts), |
| 106 |
1/1✓ Branch 35 → 36 taken 5 times.
|
15 | RelationSuggestions::dismissedPairs(contacts, cards)); |
| 107 | 5 | } | |
| 108 | 5 | } | |
| 109 |
2/2✓ Branch 50 → 51 taken 5 times.
✓ Branch 51 → 52 taken 5 times.
|
5 | QSqlDatabase::removeDatabase(QStringLiteral("suggest-mail-") + suffix); |
| 110 |
2/2✓ Branch 58 → 59 taken 5 times.
✓ Branch 59 → 60 taken 5 times.
|
5 | QSqlDatabase::removeDatabase(QStringLiteral("suggest-cal-") + suffix); |
| 111 |
9/9✓ Branch 63 → 64 taken 5 times.
✓ Branch 67 → 68 taken 5 times.
✓ Branch 68 → 69 taken 5 times.
✓ Branch 70 → 71 taken 5 times.
✓ Branch 71 → 72 taken 5 times.
✓ Branch 73 → 74 taken 5 times.
✓ Branch 74 → 75 taken 5 times.
✓ Branch 78 → 65 taken 5 times.
✓ Branch 78 → 79 taken 5 times.
|
10 | qCInfo(lcGraph) << "Relationship suggestions:" << found.size() << "in" << timer.elapsed() << "ms"; |
| 112 |
1/1✓ Branch 82 → 83 taken 5 times.
|
5 | QMetaObject::invokeMethod( |
| 113 | 5 | self.data(), | |
| 114 |
0/2✗ Branch 153 → 154 not taken.
✗ Branch 153 → 155 not taken.
|
10 | [self, run, found] { |
| 115 |
4/8✓ Branch 3 → 4 taken 5 times.
✗ Branch 3 → 8 not taken.
✓ Branch 5 → 6 taken 5 times.
✗ Branch 5 → 8 not taken.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 5 times.
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 5 times.
|
5 | if (!self || run != self->m_suggestionRun || !self->m_suggestions) |
| 116 | ✗ | return; | |
| 117 | 5 | self->m_suggestionDone = run; | |
| 118 | 5 | self->m_suggestionCounts.clear(); | |
| 119 |
2/2✓ Branch 24 → 17 taken 5 times.
✓ Branch 24 → 25 taken 5 times.
|
10 | for (const RelationSuggestions::Suggestion &s : found) |
| 120 |
1/1✓ Branch 21 → 22 taken 5 times.
|
5 | self->m_suggestionCounts.insert({s.first, s.second}, {s.mails, s.events}); |
| 121 | 5 | self->m_loaded = false; // the input carries them | |
| 122 | // S106-276: suggestionCount reads m_input, so the input is refreshed | ||
| 123 | // before the NOTIFY, not after it. | ||
| 124 |
2/6✗ Branch 28 → 29 not taken.
✓ Branch 28 → 31 taken 5 times.
✗ Branch 30 → 31 not taken.
✗ Branch 30 → 32 not taken.
✓ Branch 33 → 34 taken 5 times.
✗ Branch 33 → 36 not taken.
|
5 | if (!self->m_centerKey.isEmpty() || self->m_overview) |
| 125 | 5 | self->rebuild(); | |
| 126 | else | ||
| 127 | ✗ | self->ensureInput(); | |
| 128 | 5 | emit self->suggestionsChanged(); | |
| 129 | }, | ||
| 130 | Qt::QueuedConnection); | ||
| 131 | 5 | }); | |
| 132 | 5 | } | |
| 133 | |||
| 134 | 7 | QString QuickContactGraph::suggestionSentence(const RelationGraph::Node &a, const RelationGraph::Node &b) const { | |
| 135 | 7 | const QPair<qint64, qint64> pair{std::min(a.cardId, b.cardId), std::max(a.cardId, b.cardId)}; | |
| 136 | 7 | const QPair<int, int> counts = m_suggestionCounts.value(pair); | |
| 137 | 7 | QStringList reasons; | |
| 138 |
2/2✓ Branch 6 → 7 taken 2 times.
✓ Branch 6 → 13 taken 5 times.
|
7 | if (counts.first > 0) |
| 139 |
3/5✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 2 times.
✗ Branch 8 → 10 not taken.
✓ Branch 9 → 10 taken 2 times.
✓ Branch 10 → 11 taken 2 times.
|
2 | reasons << (counts.first == 1 ? tr("1 mail") : tr("%n mails", "", counts.first)); |
| 140 |
2/2✓ Branch 13 → 14 taken 5 times.
✓ Branch 13 → 20 taken 2 times.
|
7 | if (counts.second > 0) |
| 141 |
3/5✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 5 times.
✗ Branch 15 → 17 not taken.
✓ Branch 16 → 17 taken 5 times.
✓ Branch 17 → 18 taken 5 times.
|
5 | reasons << (counts.second == 1 ? tr("1 appointment") : tr("%n appointments", "", counts.second)); |
| 142 | //: Graph: a suggested relationship — "Ana Silva and Rui Costa: together in 7 mails and 3 appointments". | ||
| 143 |
4/4✓ Branch 20 → 21 taken 7 times.
✓ Branch 21 → 22 taken 7 times.
✓ Branch 22 → 23 taken 7 times.
✓ Branch 23 → 24 taken 7 times.
|
14 | return tr("%1 and %2: together in %3 — related?").arg(a.name, b.name, reasons.join(tr(" and "))); |
| 144 | 7 | } | |
| 145 | |||
| 146 | 2 | bool QuickContactGraph::dismissSuggestion(int from, int to) { | |
| 147 |
5/10✓ Branch 2 → 3 taken 2 times.
✗ Branch 2 → 8 not taken.
✓ Branch 3 → 4 taken 2 times.
✗ Branch 3 → 8 not taken.
✓ Branch 5 → 6 taken 2 times.
✗ Branch 5 → 8 not taken.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 2 times.
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 2 times.
|
2 | if (from < 0 || to < 0 || from >= m_graph.nodes.size() || to >= m_graph.nodes.size()) |
| 148 | ✗ | return false; | |
| 149 | // S106-277: a rebuild while the edge menu was open re-indexes the nodes; | ||
| 150 | // only a pair that is a suggestion of the current graph is dismissed. | ||
| 151 |
1/1✓ Branch 14 → 15 taken 2 times.
|
2 | const bool suggested = std::any_of(m_graph.edges.cbegin(), m_graph.edges.cend(), [from, to](const RelationGraph::Edge &edge) { |
| 152 |
2/2✓ Branch 4 → 5 taken 4 times.
✓ Branch 4 → 10 taken 2 times.
|
10 | return edge.kind == QLatin1String("suggestion") && |
| 153 |
4/8✓ Branch 5 → 6 taken 4 times.
✗ Branch 5 → 7 not taken.
✓ Branch 6 → 7 taken 3 times.
✓ Branch 6 → 9 taken 1 time.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 10 taken 3 times.
✗ Branch 8 → 9 not taken.
✗ Branch 8 → 10 not taken.
|
10 | ((edge.from == from && edge.to == to) || (edge.from == to && edge.to == from)); |
| 154 | }); | ||
| 155 |
2/2✓ Branch 15 → 16 taken 1 time.
✓ Branch 15 → 29 taken 1 time.
|
2 | if (!suggested) { |
| 156 |
7/7✓ Branch 16 → 17 taken 1 time.
✓ Branch 20 → 21 taken 1 time.
✓ Branch 21 → 22 taken 1 time.
✓ Branch 22 → 23 taken 1 time.
✓ Branch 23 → 24 taken 1 time.
✓ Branch 27 → 18 taken 1 time.
✓ Branch 27 → 28 taken 1 time.
|
2 | qCInfo(lcGraph) << "Suggestion dismiss refused: no suggestion between" << from << to; |
| 157 | 1 | return false; | |
| 158 | } | ||
| 159 | 1 | const RelationGraph::Node &a = m_graph.nodes.at(from); | |
| 160 | 1 | const RelationGraph::Node &b = m_graph.nodes.at(to); | |
| 161 |
1/1✓ Branch 31 → 32 taken 1 time.
|
1 | ContactStore store; |
| 162 |
2/3✓ Branch 32 → 33 taken 1 time.
✗ Branch 33 → 34 not taken.
✓ Branch 33 → 35 taken 1 time.
|
1 | if (!store.open(m_databasePath)) |
| 163 | ✗ | return false; | |
| 164 |
1/1✓ Branch 35 → 36 taken 1 time.
|
1 | QSqlDatabase db = store.database(); |
| 165 |
2/3✓ Branch 36 → 37 taken 1 time.
✗ Branch 37 → 38 not taken.
✓ Branch 37 → 39 taken 1 time.
|
1 | if (!RelationSuggestions::dismiss(db, a.key, b.key)) |
| 166 | ✗ | return false; | |
| 167 |
1/1✓ Branch 42 → 43 taken 1 time.
|
1 | m_suggestionCounts.remove({std::min(a.cardId, b.cardId), std::max(a.cardId, b.cardId)}); |
| 168 | 1 | m_loaded = false; | |
| 169 |
5/5✓ Branch 43 → 44 taken 1 time.
✓ Branch 47 → 48 taken 1 time.
✓ Branch 48 → 49 taken 1 time.
✓ Branch 52 → 45 taken 1 time.
✓ Branch 52 → 53 taken 1 time.
|
2 | qCInfo(lcGraph) << "Suggestion dismissed"; |
| 170 |
1/1✓ Branch 53 → 54 taken 1 time.
|
1 | rebuild(); |
| 171 |
1/1✓ Branch 54 → 55 taken 1 time.
|
1 | emit suggestionsChanged(); |
| 172 | 1 | return true; | |
| 173 | 1 | } | |
| 174 | |||
| 175 | 1 | void QuickContactGraph::forgetDismissedSuggestions() { | |
| 176 |
1/1✓ Branch 2 → 3 taken 1 time.
|
1 | ContactStore store; |
| 177 |
2/3✓ Branch 3 → 4 taken 1 time.
✓ Branch 4 → 5 taken 1 time.
✗ Branch 4 → 9 not taken.
|
1 | if (store.open(m_databasePath)) { |
| 178 |
1/1✓ Branch 5 → 6 taken 1 time.
|
1 | QSqlDatabase db = store.database(); |
| 179 |
1/1✓ Branch 6 → 7 taken 1 time.
|
1 | RelationSuggestions::forgetDismissed(db); |
| 180 | 1 | } | |
| 181 |
5/5✓ Branch 9 → 10 taken 1 time.
✓ Branch 13 → 14 taken 1 time.
✓ Branch 14 → 15 taken 1 time.
✓ Branch 18 → 11 taken 1 time.
✓ Branch 18 → 19 taken 1 time.
|
2 | qCInfo(lcGraph) << "Dismissed suggestions forgotten"; |
| 182 |
1/2✓ Branch 19 → 20 taken 1 time.
✗ Branch 19 → 21 not taken.
|
1 | if (m_suggestions) |
| 183 |
1/1✓ Branch 20 → 21 taken 1 time.
|
1 | computeSuggestions(); |
| 184 | 1 | } | |
| 185 |