File indexing completed on 2024-05-19 05:14:36

0001 /*
0002   This file is part of KAddressBook.
0003 
0004   SPDX-FileCopyrightText: 2020 Konrad Czapla <kondzio89dev@gmail.com>
0005 
0006   SPDX-License-Identifier: GPL-2.0-or-later
0007 */
0008 
0009 #include "contactinfoproxymodel.h"
0010 
0011 #include "kaddressbook_debug.h"
0012 
0013 #include <Akonadi/Item>
0014 #include <Akonadi/ItemFetchJob>
0015 #include <Akonadi/ItemFetchScope>
0016 #include <Akonadi/Monitor>
0017 #include <KContacts/Addressee>
0018 #include <KLocalizedString>
0019 
0020 #include <KJob>
0021 
0022 ContactInfoProxyModel::ContactInfoProxyModel(QObject *parent)
0023     : QIdentityProxyModel(parent)
0024     , mMonitor(new Akonadi::Monitor(this))
0025 {
0026     mMonitor->setTypeMonitored(Akonadi::Monitor::Items);
0027     mMonitor->itemFetchScope().fetchFullPayload(true);
0028     connect(mMonitor, &Akonadi::Monitor::itemChanged, this, &ContactInfoProxyModel::slotItemChanged);
0029     connect(this, &ContactInfoProxyModel::rowsAboutToBeRemoved, this, &ContactInfoProxyModel::slotRowsAboutToBeRemoved);
0030 }
0031 
0032 QVariant ContactInfoProxyModel::data(const QModelIndex &index, int role) const
0033 {
0034     if (!index.isValid()) {
0035         return {};
0036         qCWarning(KADDRESSBOOK_LOG) << "invalid index!";
0037     }
0038     if (role >= Roles::PictureRole && role <= Roles::DescriptionRole) {
0039         const auto item = index.data(Akonadi::EntityTreeModel::ItemRole).value<Akonadi::Item>();
0040         Q_ASSERT(item.isValid());
0041         if (item.hasPayload<KContacts::Addressee>()) {
0042             const auto contact = item.payload<KContacts::Addressee>();
0043             switch (role) {
0044             case Roles::PictureRole:
0045                 return contact.photo().data();
0046             case Roles::InitialsRole:
0047                 return getInitials(contact);
0048             case Roles::DescriptionRole:
0049                 return getDescription(contact);
0050             }
0051         } else if (item.hasPayload<KContacts::ContactGroup>()) {
0052             const auto groupContacts = item.payload<KContacts::ContactGroup>();
0053             if (!mPendingGroupItems.contains(item.id())) {
0054                 if (!mGroupsCache.contains(item.id())) {
0055                     mMonitor->setItemMonitored(item);
0056                     mGroupsCache[item.id()] = ContactCacheData::List();
0057                 }
0058 
0059                 if (groupContacts.contactReferenceCount() > 0 && isCacheItemToFetch(item.id(), groupContacts)) {
0060                     resolveGroup(item.id(), groupContacts);
0061                 }
0062             }
0063             switch (role) {
0064             case Roles::PictureRole:
0065                 return {};
0066             case Roles::InitialsRole:
0067                 return mPendingGroupItems.contains(item.id()) ? i18n("...") : getInitials(item.id(), groupContacts);
0068             case Roles::DescriptionRole:
0069                 return mPendingGroupItems.contains(item.id()) ? i18n("Loading contacts details ...") : getDescription(item.id(), groupContacts);
0070             }
0071         }
0072     }
0073     return QIdentityProxyModel::data(index, role);
0074 }
0075 
0076 QString ContactInfoProxyModel::getInitials(const KContacts::Addressee &contact) const
0077 {
0078     QString initials;
0079     if (!contact.givenName().isEmpty()) {
0080         initials.append(contact.givenName().front());
0081     }
0082     if (!contact.familyName().isEmpty()) {
0083         initials.append(contact.familyName().front());
0084     }
0085 
0086     if (initials.isEmpty() && !contact.realName().isEmpty()) {
0087         initials.append(contact.realName().front());
0088     }
0089 
0090     return initials.toUpper();
0091 }
0092 
0093 QString ContactInfoProxyModel::getInitials(const Akonadi::Item::Id groupItemId, const KContacts::ContactGroup &groupContacts) const
0094 {
0095     QString initials;
0096 
0097     for (int idx = 0; idx < groupContacts.dataCount(); idx++) {
0098         const QString name = groupContacts.data(idx).name().trimmed();
0099         if (!name.isEmpty()) {
0100             initials.append(name.front());
0101         }
0102     }
0103 
0104     const auto groupCacheId = mGroupsCache[groupItemId];
0105     for (const ContactCacheData &cacheContact : groupCacheId) {
0106         const QString name = cacheContact.name().trimmed();
0107         if (!name.isEmpty()) {
0108             initials.append(name.front());
0109         }
0110     }
0111 
0112     return initials.toUpper();
0113 }
0114 
0115 QString ContactInfoProxyModel::getDescription(const KContacts::Addressee &contact) const
0116 {
0117     QString dataSeparator;
0118     QString emailAddress;
0119     QString phone;
0120 
0121     if (!contact.preferredEmail().isEmpty()) {
0122         emailAddress = i18n("Email: %1", contact.preferredEmail());
0123     }
0124     const QList<KContacts::PhoneNumber> phoneList = contact.phoneNumbers().toList();
0125     QList<KContacts::PhoneNumber>::const_reverse_iterator itPhone =
0126         std::find_if(phoneList.rbegin(), phoneList.rend(), [&phoneList](const KContacts::PhoneNumber &phone) {
0127             return phone.isPreferred() || phoneList.at(0) == phone;
0128         });
0129     if (itPhone != phoneList.rend()) {
0130         phone = i18n("Phone: %1", (*itPhone).number());
0131     }
0132     if (!emailAddress.isEmpty() && !phone.isEmpty()) {
0133         dataSeparator = QStringLiteral(",");
0134     }
0135 
0136     return i18n("%1%2 %3", emailAddress, dataSeparator, phone).trimmed();
0137 }
0138 
0139 QString ContactInfoProxyModel::getDescription(const Akonadi::Item::Id groupItemId, const KContacts::ContactGroup &groupContacts) const
0140 {
0141     QStringList groupDescription;
0142     QString contactDescription;
0143 
0144     for (int idx = 0; idx < groupContacts.dataCount(); idx++) {
0145         QString dataSeparator;
0146         if (!groupContacts.data(idx).name().isEmpty() && !groupContacts.data(idx).email().isEmpty()) {
0147             dataSeparator = QStringLiteral("-");
0148         }
0149         contactDescription = i18n("%1 %2 %3", groupContacts.data(idx).name(), dataSeparator, groupContacts.data(idx).email());
0150         groupDescription << contactDescription.trimmed();
0151         contactDescription.clear();
0152     }
0153     for (int idx = 0; idx < groupContacts.contactReferenceCount(); idx++) {
0154         const KContacts::ContactGroup::ContactReference contactRef = groupContacts.contactReference(idx);
0155 
0156         ContactCacheData::ConstListIterator it = findCacheItem(groupItemId, contactRef);
0157         if (it != mGroupsCache[groupItemId].end()) {
0158             QString cacheSeparator;
0159             QString email;
0160             email = contactRef.preferredEmail().isEmpty() ? it->email() : contactRef.preferredEmail();
0161             if (it->name().isEmpty() && email.isEmpty()) {
0162                 continue;
0163             } else if (!it->name().isEmpty() && !email.isEmpty()) {
0164                 cacheSeparator = QStringLiteral("-");
0165             }
0166             contactDescription = i18n("%1 %2 %3", it->name(), cacheSeparator, email);
0167             groupDescription << contactDescription.trimmed();
0168             contactDescription.clear();
0169         }
0170     }
0171     return groupDescription.join(QStringLiteral(", "));
0172 }
0173 
0174 QStringList ContactInfoProxyModel::getIdsContactGroup(const KContacts::ContactGroup &group) const
0175 {
0176     QStringList groupRefIds;
0177     groupRefIds.reserve(group.contactReferenceCount());
0178     for (int idx = 0; idx < group.contactReferenceCount(); idx++) {
0179         const KContacts::ContactGroup::ContactReference &reference = group.contactReference(idx);
0180 
0181         groupRefIds += reference.gid().isEmpty() ? reference.uid() : reference.gid();
0182     }
0183     return groupRefIds;
0184 }
0185 
0186 QStringList ContactInfoProxyModel::getIdsCacheContactGroup(const Akonadi::Item::Id groupItemId) const
0187 {
0188     QStringList groupCacheRefIds;
0189     groupCacheRefIds.reserve(mGroupsCache[groupItemId].size());
0190     for (const auto &cacheContact : mGroupsCache[groupItemId]) {
0191         groupCacheRefIds += cacheContact.gid().isEmpty() ? cacheContact.uid() : cacheContact.gid();
0192     }
0193     return groupCacheRefIds;
0194 }
0195 
0196 bool ContactInfoProxyModel::isCacheItemToFetch(const Akonadi::Item::Id groupItemId, const KContacts::ContactGroup &group) const
0197 {
0198     QStringList groupRefIds = getIdsContactGroup(group);
0199     QStringList groupCacheRefIds = getIdsCacheContactGroup(groupItemId);
0200 
0201     auto sortFunc = [](const QString &lhs, const QString &rhs) -> bool {
0202         return lhs.toLongLong() < rhs.toLongLong();
0203     };
0204 
0205     std::sort(groupRefIds.begin(), groupRefIds.end(), sortFunc);
0206     groupRefIds.erase(std::unique(groupRefIds.begin(), groupRefIds.end()), groupRefIds.end());
0207 
0208     std::sort(groupCacheRefIds.begin(), groupCacheRefIds.end(), sortFunc);
0209 
0210     return !std::equal(groupRefIds.begin(), groupRefIds.end(), groupCacheRefIds.begin(), groupCacheRefIds.end());
0211 }
0212 
0213 ContactInfoProxyModel::ContactCacheData::ListIterator ContactInfoProxyModel::findCacheItem(const Akonadi::Item::Id groupItemId,
0214                                                                                            const ContactInfoProxyModel::ContactCacheData &cacheContact)
0215 {
0216     ContactCacheData::ListIterator it =
0217         std::find_if(mGroupsCache[groupItemId].begin(), mGroupsCache[groupItemId].end(), [&cacheContact](const ContactCacheData &contact) -> bool {
0218             return contact == cacheContact;
0219         });
0220     return it;
0221 }
0222 
0223 ContactInfoProxyModel::ContactCacheData::ConstListIterator
0224 ContactInfoProxyModel::findCacheItem(const Akonadi::Item::Id groupItemId, const ContactInfoProxyModel::ContactCacheData &cacheContact) const
0225 {
0226     ContactCacheData::ConstListIterator it =
0227         std::find_if(mGroupsCache[groupItemId].cbegin(), mGroupsCache[groupItemId].cend(), [&cacheContact](const ContactCacheData &contact) -> bool {
0228             return contact == cacheContact;
0229         });
0230     return it;
0231 }
0232 
0233 QMap<const char *, QVariant> ContactInfoProxyModel::buildFetchProperties(const Akonadi::Item::Id groupItemId) const
0234 {
0235     return QMap<const char *, QVariant>{
0236         {"groupItemId", QVariant::fromValue((groupItemId))},
0237     };
0238 }
0239 
0240 void ContactInfoProxyModel::resolveGroup(const Akonadi::Item::Id groupItemId, const KContacts::ContactGroup &groupContacts) const
0241 {
0242     Akonadi::Item::List groupItemsList;
0243 
0244     for (int idx = 0; idx < groupContacts.contactReferenceCount(); idx++) {
0245         const KContacts::ContactGroup::ContactReference contactRef = groupContacts.contactReference(idx);
0246 
0247         if (findCacheItem(groupItemId, contactRef) == mGroupsCache[groupItemId].cend()) {
0248             mGroupsCache[groupItemId].push_back(contactRef);
0249             Akonadi::Item newItem;
0250 
0251             if (contactRef.gid().isEmpty()) {
0252                 newItem.setId(contactRef.uid().toLongLong());
0253             } else {
0254                 newItem.setGid(contactRef.gid());
0255             }
0256             groupItemsList << newItem;
0257         }
0258     }
0259     if (!groupItemsList.isEmpty()) {
0260         mPendingGroupItems << groupItemId;
0261         fetchItems(groupItemsList, buildFetchProperties(groupItemId));
0262     }
0263 }
0264 
0265 void ContactInfoProxyModel::fetchItems(const Akonadi::Item::List &items, const QMap<const char *, QVariant> &properties) const
0266 {
0267     auto job = new Akonadi::ItemFetchJob(items);
0268     job->fetchScope().fetchFullPayload();
0269     job->fetchScope().setIgnoreRetrievalErrors(true);
0270 
0271     for (const auto &property : properties.toStdMap()) {
0272         job->setProperty(property.first, property.second);
0273     }
0274 
0275     connect(job, &Akonadi::ItemFetchJob::result, this, &ContactInfoProxyModel::slotFetchJobFinished);
0276 }
0277 
0278 void ContactInfoProxyModel::slotFetchJobFinished(KJob *job)
0279 {
0280     if (job->error()) {
0281         qCWarning(KADDRESSBOOK_LOG) << " error during fetching items" << job->errorString();
0282         return;
0283     }
0284     auto fetchJob = qobject_cast<Akonadi::ItemFetchJob *>(job);
0285 
0286     const auto groupItemId = job->property("groupItemId").value<Akonadi::Item::Id>();
0287 
0288     const auto items = fetchJob->items();
0289     for (const Akonadi::Item &item : items) {
0290         ContactCacheData::List::iterator it_contact = findCacheItem(groupItemId, item);
0291         if (it_contact != mGroupsCache[groupItemId].end()) {
0292             if (it_contact->setData(item)) {
0293                 mMonitor->setItemMonitored(item);
0294             } else {
0295                 qCWarning(KADDRESSBOOK_LOG) << QStringLiteral("item with id %1 cannot be saved into cache").arg(item.id());
0296             }
0297         }
0298     }
0299 
0300     if (mPendingGroupItems.contains(groupItemId)) {
0301         mPendingGroupItems.removeOne(groupItemId);
0302     }
0303     const QModelIndex index = Akonadi::EntityTreeModel::modelIndexesForItem(this, Akonadi::Item(groupItemId)).constFirst();
0304     Q_EMIT dataChanged(index, index, mKrole);
0305 }
0306 
0307 void ContactInfoProxyModel::slotItemChanged(const Akonadi::Item &item, const QSet<QByteArray> &partIdentifiers)
0308 {
0309     Q_UNUSED(partIdentifiers)
0310     Q_ASSERT(item.isValid());
0311 
0312     if (item.hasPayload<KContacts::Addressee>()) {
0313         QMapIterator<Akonadi::Item::Id, ContactCacheData::List> it_group(mGroupsCache);
0314         while (it_group.hasNext()) {
0315             it_group.next();
0316             ContactCacheData::ListIterator it_contact = findCacheItem(it_group.key(), item);
0317             if (it_contact != mGroupsCache[it_group.key()].end()) {
0318                 if (it_contact->setData(item)) {
0319                     const QModelIndex index = Akonadi::EntityTreeModel::modelIndexesForItem(this, Akonadi::Item(it_group.key())).constFirst();
0320                     Q_EMIT dataChanged(index, index, mKrole);
0321                 } else {
0322                     qCWarning(KADDRESSBOOK_LOG) << QStringLiteral("changed item with id %1 cannot be saved into cache").arg(item.id());
0323                 }
0324             }
0325         }
0326     } else if (item.hasPayload<KContacts::ContactGroup>()) {
0327         if (mGroupsCache.contains(item.id())) {
0328             const auto groupContacts = item.payload<KContacts::ContactGroup>();
0329             mGroupsCache[item.id()].clear();
0330             if (groupContacts.contactReferenceCount() > 0 && isCacheItemToFetch(item.id(), groupContacts)) {
0331                 resolveGroup(item.id(), groupContacts);
0332             }
0333             const QModelIndex index = Akonadi::EntityTreeModel::modelIndexesForItem(this, Akonadi::Item(item.id())).constFirst();
0334             Q_EMIT dataChanged(index, index, mKrole);
0335         }
0336     }
0337 }
0338 
0339 void ContactInfoProxyModel::slotRowsAboutToBeRemoved(const QModelIndex &parent, int first, int last)
0340 {
0341     for (int idx = first; idx <= last; idx++) {
0342         const auto item = this->index(idx, 0, parent).data(Akonadi::EntityTreeModel::ItemRole).value<Akonadi::Item>();
0343         Q_ASSERT(item.isValid());
0344         if (item.hasPayload<KContacts::Addressee>()) {
0345             QMapIterator<Akonadi::Item::Id, ContactCacheData::List> it_group(mGroupsCache);
0346             while (it_group.hasNext()) {
0347                 it_group.next();
0348                 ContactCacheData::List::iterator it_contact = findCacheItem(it_group.key(), item);
0349                 if (it_contact != mGroupsCache[it_group.key()].end()) {
0350                     mGroupsCache[it_group.key()].erase(it_contact);
0351                 }
0352             }
0353         } else if (item.hasPayload<KContacts::ContactGroup>()) {
0354             if (mGroupsCache.contains(item.id())) {
0355                 mGroupsCache.remove(item.id());
0356             }
0357         }
0358     }
0359 }
0360 
0361 bool ContactInfoProxyModel::ContactCacheData::setData(const Akonadi::Item &item)
0362 {
0363     bool result(false);
0364     if (validateItem(item)) {
0365         const auto contact = item.payload<KContacts::Addressee>();
0366         mName = contact.realName();
0367         mEmail = contact.preferredEmail();
0368         result = true;
0369     }
0370     return result;
0371 }
0372 
0373 bool ContactInfoProxyModel::ContactCacheData::validateItem(const Akonadi::Item &item) const
0374 {
0375     return item.isValid() && mUid == QString::number(item.id()) && mGid == item.gid() && item.hasPayload<KContacts::Addressee>();
0376 }
0377 
0378 bool operator==(const ContactInfoProxyModel::ContactCacheData &lhs, const ContactInfoProxyModel::ContactCacheData &rhs)
0379 {
0380     return !lhs.gid().isEmpty() ? lhs.gid() == rhs.gid() : !lhs.uid().isEmpty() ? lhs.uid() == rhs.uid() : false;
0381 }
0382 
0383 #include "moc_contactinfoproxymodel.cpp"