Commit e702826d authored by mvogt's avatar mvogt

[libcontacts] Initial commit

Extract SeasideCache from nemo-qml-plugin-contacts and export from
this library.
parents
include(package.pri)
include(contacts-namespace.pri)
CONFIG += qt link_pkgconfig
QT -= gui
equals(QT_MAJOR_VERSION, 4) {
CONFIG += mobility
MOBILITY += contacts versit
PKGCONFIG += qtcontacts-sqlite-extensions
}
equals(QT_MAJOR_VERSION, 5) {
PKGCONFIG += Qt5Contacts Qt5Versit qtcontacts-sqlite-qt5-extensions
# Needed for qt4 moc, which can't handle numeric tests
DEFINES *= QT_VERSION_5
}
# We need different macros depending on which Contacts we're using
equals(QT_MAJOR_VERSION, 4) {
DEFINES *= BEGIN_CONTACTS_NAMESPACE=QTM_BEGIN_NAMESPACE
DEFINES *= END_CONTACTS_NAMESPACE=QTM_END_NAMESPACE
DEFINES *= USE_CONTACTS_NAMESPACE=QTM_USE_NAMESPACE
DEFINES *= BEGIN_VERSIT_NAMESPACE=
DEFINES *= END_VERSIT_NAMESPACE=
DEFINES *= USE_VERSIT_NAMESPACE=
}
equals(QT_MAJOR_VERSION, 5) {
DEFINES *= USING_QTPIM
DEFINES *= BEGIN_CONTACTS_NAMESPACE=QT_BEGIN_NAMESPACE_CONTACTS
DEFINES *= END_CONTACTS_NAMESPACE=QT_END_NAMESPACE_CONTACTS
DEFINES *= USE_CONTACTS_NAMESPACE=QTCONTACTS_USE_NAMESPACE
DEFINES *= BEGIN_VERSIT_NAMESPACE=QT_BEGIN_NAMESPACE_VERSIT
DEFINES *= END_VERSIT_NAMESPACE=QT_END_NAMESPACE_VERSIT
DEFINES *= USE_VERSIT_NAMESPACE=QTVERSIT_USE_NAMESPACE
}
TEMPLATE = subdirs
SUBDIRS = src tests
tests.depends = src
equals(QT_MAJOR_VERSION, 4): PACKAGENAME=contactcache
equals(QT_MAJOR_VERSION, 5): PACKAGENAME=contactcache-qt5
Name: libcontacts-qt5
Summary: Nemo contact cache library
Version: 0.0.0
Release: 1
Group: System/Libraries
License: BSD
URL: https://github.com/nemomobile/libcontacts
Source0: %{name}-%{version}.tar.bz2
Requires: qtcontacts-sqlite-qt5
BuildRequires: pkgconfig(Qt5Core)
BuildRequires: pkgconfig(Qt5Test)
BuildRequires: pkgconfig(Qt5Contacts)
BuildRequires: pkgconfig(Qt5Versit)
BuildRequires: pkgconfig(mlite5)
BuildRequires: pkgconfig(qtcontacts-sqlite-qt5-extensions)
%description
%{summary}.
%package tests
Summary: Nemo contact cache library tests
Group: System/Libraries
Requires: %{name} = %{version}-%{release}
%description tests
%{summary}.
%package devel
Summary: Nemo contact cache library headers
Group: System/Libraries
Requires: %{name} = %{version}-%{release}
%description devel
%{summary}.
%prep
%setup -q -n %{name}-%{version}
%build
%qmake5
make %{?jobs:-j%jobs}
%install
rm -rf %{buildroot}
%qmake_install
%post -p /sbin/ldconfig
%postun -p /sbin/ldconfig
%files
%defattr(-,root,root,-)
%{_libdir}/libcontactcache-qt5.so*
%files tests
%defattr(-,root,root,-)
/opt/tests/contactcache-qt5/*
%files devel
%defattr(-,root,root,-)
%{_includedir}/contactcache-qt5/*
%{_libdir}/pkgconfig/contactcache-qt5.pc
Name: libcontacts
Summary: Nemo contact cache library
Version: 0.0.0
Release: 1
Group: System/Libraries
License: BSD
URL: https://github.com/nemomobile/libcontacts
Source0: %{name}-%{version}.tar.bz2
Requires: qtcontacts-sqlite
BuildRequires: pkgconfig(QtCore)
BuildRequires: pkgconfig(QtContacts)
BuildRequires: pkgconfig(QtVersit)
BuildRequires: pkgconfig(mlite)
BuildRequires: pkgconfig(qtcontacts-sqlite-extensions)
%description
%{summary}.
%package tests
Summary: Nemo contact cache library tests
Group: System/Libraries
Requires: %{name} = %{version}-%{release}
%description tests
%{summary}.
%package devel
Summary: Nemo contact cache library headers
Group: System/Libraries
Requires: %{name} = %{version}-%{release}
%description devel
%{summary}.
%prep
%setup -q -n %{name}-%{version}
%build
%qmake
make %{?jobs:-j%jobs}
%install
rm -rf %{buildroot}
%qmake_install
%post -p /sbin/ldconfig
%postun -p /sbin/ldconfig
%files
%defattr(-,root,root,-)
%{_libdir}/libcontactcache.so*
%files tests
%defattr(-,root,root,-)
/opt/tests/contactcache/*
%files devel
%defattr(-,root,root,-)
%{_includedir}/contactcache/*
%{_libdir}/pkgconfig/contactcache.pc
/*
* Copyright (C) 2013 Jolla Mobile <matthew.vogt@jollamobile.com>
*
* You may use this file under the terms of the BSD license as follows:
*
* "Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Nemo Mobile nor the names of its contributors
* may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
*/
#ifndef CONTACTCACHEEXPORT_H
#define CONTACTCACHEEXPORT_H
#include <QtGlobal>
#ifdef CONTACTCACHE_BUILD
#define CONTACTCACHE_EXPORT Q_DECL_EXPORT
#else
#define CONTACTCACHE_EXPORT Q_DECL_IMPORT
#endif
#endif
/*
* libseaside - Library that provides an interface to the Contacts application
* Copyright (c) 2013, Matt Vogt
*
* This program is licensed under the terms and conditions of the
* Apache License, version 2.0. The full text of the Apache License is at
* http://www.apache.org/licenses/LICENSE-2.0
*
*/
#include "normalization_p.h"
namespace Normalization {
QString normalizePhoneNumber(const QString &input)
{
// Use the same algorithm as maemo localNumber
// Not actually the 'visual-separators' from RFC3966...
// This logic is derived from qtcontacts-tracker
static const QString separators(QString::fromLatin1(" .-()[]"));
static const QString dtmfChars(QString::fromLatin1("pPwWxX"));
// TODO: possibly make this tunable?
static const int maxCharacters = 7;
QString subset;
subset.reserve(input.length());
QString::const_iterator it = input.constBegin(), end = input.constEnd();
for ( ; it != end; ++it) {
if ((*it).isDigit()) {
// Convert to ASCII, capturing unicode digit values
subset.append(QChar::fromLatin1('0' + (*it).digitValue()));
} else if (!separators.contains(*it) &&
(*it).category() != QChar::Other_Format) {
// If this is a DTMF character, stop processing here
if (dtmfChars.contains(*it)) {
break;
} else {
subset.append(*it);
}
}
}
return subset.right(maxCharacters);
}
}
/*
* libseaside - Library that provides an interface to the Contacts application
* Copyright (c) 2013, Matt Vogt
*
* This program is licensed under the terms and conditions of the
* Apache License, version 2.0. The full text of the Apache License is at
* http://www.apache.org/licenses/LICENSE-2.0
*
*/
#ifndef __NORMALIZATION_P_H__
#define __NORMALIZATION_P_H__
#include <QString>
namespace Normalization {
QString normalizePhoneNumber(const QString &input);
}
#endif
/*
* Copyright (C) 2013 Jolla Mobile <andrew.den.exter@jollamobile.com>
*
* You may use this file under the terms of the BSD license as follows:
*
* "Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Nemo Mobile nor the names of its contributors
* may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
*/
#include "seasidecache.h"
#include "synchronizelists.h"
#include "normalization_p.h"
#include <QCoreApplication>
#ifdef USING_QTPIM
#include <QStandardPaths>
#else
#include <QDesktopServices>
#endif
#include <QDir>
#include <QEvent>
#include <QFile>
#include <QContactAvatar>
#include <QContactDetailFilter>
#include <QContactDisplayLabel>
#include <QContactEmailAddress>
#include <QContactFavorite>
#include <QContactName>
#include <QContactNickname>
#include <QContactOnlineAccount>
#include <QContactOrganization>
#include <QContactPhoneNumber>
#include <QContactGlobalPresence>
#include <QContactSyncTarget>
#include <QVersitContactExporter>
#include <QVersitContactImporter>
#include <QVersitReader>
#include <QVersitWriter>
#include <QtDebug>
USE_VERSIT_NAMESPACE
static QString aggregateRelationshipType =
#ifdef USING_QTPIM
QContactRelationship::Aggregates();
#else
QContactRelationship::Aggregates;
#endif
static const QString syncTargetLocal = QLatin1String("local");
static const QString syncTargetWasLocal = QLatin1String("was_local");
static QList<QChar> getAllContactNameGroups()
{
QList<QChar> groups;
groups << QLatin1Char('A')
<< QLatin1Char('B')
<< QLatin1Char('C')
<< QLatin1Char('D')
<< QLatin1Char('E')
<< QLatin1Char('F')
<< QLatin1Char('G')
<< QLatin1Char('H')
<< QLatin1Char('I')
<< QLatin1Char('J')
<< QLatin1Char('K')
<< QLatin1Char('L')
<< QLatin1Char('M')
<< QLatin1Char('N')
<< QLatin1Char('O')
<< QLatin1Char('P')
<< QLatin1Char('Q')
<< QLatin1Char('R')
<< QLatin1Char('S')
<< QLatin1Char('T')
<< QLatin1Char('U')
<< QLatin1Char('V')
<< QLatin1Char('W')
<< QLatin1Char('X')
<< QLatin1Char('Y')
<< QLatin1Char('Z')
<< QChar(0x00c5) // Å
<< QChar(0x00c4) // Ä
<< QChar(0x00d6) // Ö
<< QLatin1Char('#');
return groups;
}
SeasideCache *SeasideCache::instancePtr = 0;
QList<QChar> SeasideCache::allContactNameGroups = getAllContactNameGroups();
static QString managerName()
{
#ifdef USING_QTPIM
// Temporary override until qtpim supports QTCONTACTS_MANAGER_OVERRIDE
return QStringLiteral("org.nemomobile.contacts.sqlite");
#endif
QByteArray environmentManager = qgetenv("NEMO_CONTACT_MANAGER");
return !environmentManager.isEmpty()
? QString::fromLatin1(environmentManager, environmentManager.length())
: QString();
}
template<typename T, typename Filter, typename Field>
void setDetailType(Filter &filter, Field field)
{
#ifdef USING_QTPIM
filter.setDetailType(T::Type, field);
#else
filter.setDetailDefinitionName(T::DefinitionName, field);
#endif
}
SeasideCache* SeasideCache::instance()
{
return instancePtr;
}
SeasideCache::ContactIdType SeasideCache::apiId(const QContact &contact)
{
#ifdef USING_QTPIM
return contact.id();
#else
return contact.id().localId();
#endif
}
SeasideCache::ContactIdType SeasideCache::apiId(quint32 iid)
{
return QtContactsSqliteExtensions::apiContactId(iid);
}
bool SeasideCache::validId(const ContactIdType &id)
{
#ifdef USING_QTPIM
return !id.isNull();
#else
return (id != 0);
#endif
}
quint32 SeasideCache::internalId(const QContact &contact)
{
return internalId(contact.id());
}
quint32 SeasideCache::internalId(const QContactId &id)
{
return QtContactsSqliteExtensions::internalContactId(id);
}
#ifndef USING_QTPIM
quint32 SeasideCache::internalId(QContactLocalId id)
{
return QtContactsSqliteExtensions::internalContactId(id);
}
#endif
SeasideCache::SeasideCache()
: m_manager(managerName())
#ifdef HAS_MLITE
, m_displayLabelOrderConf(QLatin1String("/org/nemomobile/contacts/display_label_order"))
#endif
, m_resultsRead(0)
, m_populated(0)
, m_cacheIndex(0)
, m_queryIndex(0)
, m_appendIndex(0)
, m_fetchFilter(FilterNone)
, m_displayLabelOrder(FirstNameFirst)
, m_keepPopulated(false)
, m_updatesPending(false)
, m_refreshRequired(false)
, m_contactsUpdated(false)
{
Q_ASSERT(!instancePtr);
instancePtr = this;
m_timer.start();
#ifdef HAS_MLITE
connect(&m_displayLabelOrderConf, SIGNAL(valueChanged()), this, SLOT(displayLabelOrderChanged()));
QVariant displayLabelOrder = m_displayLabelOrderConf.value();
if (displayLabelOrder.isValid())
m_displayLabelOrder = static_cast<DisplayLabelOrder>(displayLabelOrder.toInt());
#endif
#ifdef USING_QTPIM
connect(&m_manager, SIGNAL(dataChanged()), this, SLOT(updateContacts()));
connect(&m_manager, SIGNAL(contactsAdded(QList<QContactId>)),
this, SLOT(contactsAdded(QList<QContactId>)));
connect(&m_manager, SIGNAL(contactsChanged(QList<QContactId>)),
this, SLOT(contactsChanged(QList<QContactId>)));
connect(&m_manager, SIGNAL(contactsRemoved(QList<QContactId>)),
this, SLOT(contactsRemoved(QList<QContactId>)));
#else
connect(&m_manager, SIGNAL(dataChanged()), this, SLOT(updateContacts()));
connect(&m_manager, SIGNAL(contactsAdded(QList<QContactLocalId>)),
this, SLOT(contactsAdded(QList<QContactLocalId>)));
connect(&m_manager, SIGNAL(contactsChanged(QList<QContactLocalId>)),
this, SLOT(contactsChanged(QList<QContactLocalId>)));
connect(&m_manager, SIGNAL(contactsRemoved(QList<QContactLocalId>)),
this, SLOT(contactsRemoved(QList<QContactLocalId>)));
#endif
connect(&m_fetchRequest, SIGNAL(resultsAvailable()), this, SLOT(contactsAvailable()));
connect(&m_fetchByIdRequest, SIGNAL(resultsAvailable()), this, SLOT(contactsAvailable()));
connect(&m_contactIdRequest, SIGNAL(resultsAvailable()), this, SLOT(contactIdsAvailable()));
connect(&m_relationshipsFetchRequest, SIGNAL(resultsAvailable()), this, SLOT(relationshipsAvailable()));
connect(&m_fetchRequest, SIGNAL(stateChanged(QContactAbstractRequest::State)),
this, SLOT(requestStateChanged(QContactAbstractRequest::State)));
connect(&m_fetchByIdRequest, SIGNAL(stateChanged(QContactAbstractRequest::State)),
this, SLOT(requestStateChanged(QContactAbstractRequest::State)));
connect(&m_contactIdRequest, SIGNAL(stateChanged(QContactAbstractRequest::State)),
this, SLOT(requestStateChanged(QContactAbstractRequest::State)));
connect(&m_relationshipsFetchRequest, SIGNAL(stateChanged(QContactAbstractRequest::State)),
this, SLOT(requestStateChanged(QContactAbstractRequest::State)));
connect(&m_removeRequest, SIGNAL(stateChanged(QContactAbstractRequest::State)),
this, SLOT(requestStateChanged(QContactAbstractRequest::State)));
connect(&m_saveRequest, SIGNAL(stateChanged(QContactAbstractRequest::State)),
this, SLOT(requestStateChanged(QContactAbstractRequest::State)));
connect(&m_relationshipSaveRequest, SIGNAL(stateChanged(QContactAbstractRequest::State)),
this, SLOT(requestStateChanged(QContactAbstractRequest::State)));
connect(&m_relationshipRemoveRequest, SIGNAL(stateChanged(QContactAbstractRequest::State)),
this, SLOT(requestStateChanged(QContactAbstractRequest::State)));
m_fetchRequest.setManager(&m_manager);
m_fetchByIdRequest.setManager(&m_manager);
m_contactIdRequest.setManager(&m_manager);
m_relationshipsFetchRequest.setManager(&m_manager);
m_removeRequest.setManager(&m_manager);
m_saveRequest.setManager(&m_manager);
m_relationshipSaveRequest.setManager(&m_manager);
m_relationshipRemoveRequest.setManager(&m_manager);
QContactFetchHint fetchHint;
fetchHint.setOptimizationHints(QContactFetchHint::NoRelationships
| QContactFetchHint::NoActionPreferences
| QContactFetchHint::NoBinaryBlobs);
// Note: no restriction on detail definitions - the cache should contain the entire contact
m_fetchRequest.setFetchHint(fetchHint);
setSortOrder(m_displayLabelOrder);
}
SeasideCache::~SeasideCache()
{
if (instancePtr == this)
instancePtr = 0;
}
void SeasideCache::checkForExpiry()
{
if (instancePtr->m_users.isEmpty()) {
bool unused = true;
for (int i = 0; i < FilterTypesCount; ++i) {
unused &= instancePtr->m_models[i].isEmpty();
}
if (unused) {
instancePtr->m_expiryTimer.start(30000, instancePtr);
}
}
}
void SeasideCache::registerModel(ListModel *model, FilterType type)
{
if (!instancePtr) {
new SeasideCache;
} else {
instancePtr->m_expiryTimer.stop();
for (int i = 0; i < FilterTypesCount; ++i)
instancePtr->m_models[i].removeAll(model);
}
instancePtr->m_models[type].append(model);
instancePtr->keepPopulated();
}
void SeasideCache::unregisterModel(ListModel *model)
{
for (int i = 0; i < FilterTypesCount; ++i)
instancePtr->m_models[i].removeAll(model);
checkForExpiry();
}
void SeasideCache::registerUser(QObject *user)
{
if (!instancePtr) {
new SeasideCache;
} else {
instancePtr->m_expiryTimer.stop();
}
instancePtr->m_users.insert(user);
}
void SeasideCache::unregisterUser(QObject *user)
{
instancePtr->m_users.remove(user);
checkForExpiry();
}
void SeasideCache::registerNameGroupChangeListener(SeasideNameGroupChangeListener *listener)
{
if (!instancePtr)
new SeasideCache;
instancePtr->m_nameGroupChangeListeners.append(listener);
}
void SeasideCache::unregisterNameGroupChangeListener(SeasideNameGroupChangeListener *listener)
{
if (!instancePtr)
return;
instancePtr->m_nameGroupChangeListeners.removeAll(listener);
}
QChar SeasideCache::nameGroupForCacheItem(CacheItem *cacheItem)
{
if (!cacheItem)
return QChar();
QChar group;
QString first;
QString last;
QContactName nameDetail = cacheItem->contact.detail<QContactName>();
if (SeasideCache::displayLabelOrder() == FirstNameFirst) {
first = nameDetail.firstName();
last = nameDetail.lastName();
} else {
first = nameDetail.lastName();
last = nameDetail.firstName();
}
if (!first.isEmpty()) {
group = first[0].toUpper();
} else if (!last.isEmpty()) {
group = last[0].toUpper();
} else {
QString displayLabel = (cacheItem->itemData)
? cacheItem->itemData->getDisplayLabel()
: generateDisplayLabel(cacheItem->contact);
if (!displayLabel.isEmpty())
group = displayLabel[0].toUpper();
}
// XXX temporary workaround for non-latin names: use non-name details to try to find a
// latin character group
if (!group.isNull() && group.toLatin1() != group) {
QString displayLabel = generateDisplayLabelFromNonNameDetails(cacheItem->contact);
if (!displayLabel.isEmpty())
group = displayLabel[0].toUpper();
}
if (group.isNull() || !allContactNameGroups.contains(group)) {
group = QLatin1Char('#'); // 'other' group
}
return group;
}
QList<QChar> SeasideCache::allNameGroups()
{
return allContactNameGroups;
}
QHash<QChar, int> SeasideCache::nameGroupCounts()
{
if (instancePtr)
return instancePtr->m_contactNameGroups;
return QHash<QChar, int>();
}
SeasideCache::DisplayLabelOrder SeasideCache::displayLabelOrder()
{
return instancePtr->m_displayLabelOrder;
}
int SeasideCache::contactId(const QContact &contact)
{
quint32 internal = internalId(contact);
return static_cast<int>(internal);
}
SeasideCache::CacheItem *SeasideCache::itemById(const ContactIdType &id)
{
if (!validId(id))
return 0;
quint32 iid = internalId(id);
CacheItem *item = 0;
QHash<quint32, CacheItem>::iterator it = instancePtr->m_people.find(iid);
if (it != instancePtr->m_people.end()) {
item = &(*it);
} else {
// Insert a new item into the cache if the one doesn't exist.
item = &(instancePtr->m_people[iid]);
#ifdef USING_QTPIM
item->contact.setId(id);
#else
QContactId contactId;
contactId.setLocalId(id);
item->contact.setId(contactId);
#endif
}
if (item->contactState == ContactAbsent) {
item->contactState = ContactRequested;
instancePtr->m_changedContacts.append(item->apiId());
instancePtr->fetchContacts();
}
return item;
}
#ifdef USING_QTPIM
SeasideCache::CacheItem *SeasideCache::itemById(int id)
{
if (id != 0) {
QContactId contactId(apiId(static_cast<quint32>(id)));
if (!contactId.isNull()) {
return itemById(contactId);
}
}