/****************************************************************************
**
** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies).
** All rights reserved.
** Contact: Nokia Corporation (qt-info@nokia.com)
**
** This file is part of the QtCore module of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** No Commercial Usage
** This file contains pre-release code and may not be distributed.
** You may use this file in accordance with the terms and conditions
** contained in the Technology Preview License Agreement accompanying
** this package.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 2.1 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 2.1 requirements
** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
**
** In addition, as a special exception, Nokia gives you certain additional
** rights. These rights are described in the Nokia Qt LGPL Exception
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
** If you have questions regarding the use of this file, please contact
** Nokia at qt-info@nokia.com.
**
**
**
**
**
**
**
**
** $QT_END_LICENSE$
**
****************************************************************************/
#ifndef SYMBIAN_BACKEND_USE_SQLITE
#include <qcontactdetail.h>
#include <QSet>
#include "cntsymbianfilterdbms.h"
#include "cnttransformcontact.h"
#include "cntsymbiantransformerror.h"
#include <cntdb.h>
#include <cntfield.h>
#include <centralrepository.h>
#include "qcontactname.h"
#include "qcontactdetailfilter.h"
#include "qcontactphonenumber.h"
#include "cntsymbiansorterdbms.h"
// Telephony Configuration API
// Keys under this category are used in defining telephony configuration.
const TUid KCRUidTelConfiguration = {0x102828B8};
// Amount of digits to be used in contact matching.
// This allows a customer to variate the amount of digits to be matched.
const TUint32 KTelMatchDigits = 0x00000001;
// Default match length
const TInt KDefaultMatchLength(7);
CntSymbianFilter::CntSymbianFilter(CContactDatabase& contactDatabase):
m_contactDatabase(contactDatabase),
m_contactSorter(0),
m_transformContact(0)
{
// TODO: take CntTransformContact ref as a parameter?
m_transformContact = new CntTransformContact;
m_contactSorter = new CntSymbianSorterDbms(m_contactDatabase, *m_transformContact);
}
CntSymbianFilter::~CntSymbianFilter()
{
delete m_contactSorter;
delete m_transformContact;
}
/*!
* The contact database version implementation for QContactManager::contacts
* function. See filterSupported for the list of supported filters.
* All the other filtering flags fallback to the generic filtering done
* in QContactManagerEngine (expected to be done by to the caller). Contacts
* are sorted only if the sort order is supported by contacts database. See
* CntSymbianSorterDbms::filterSupportLevel for the list of supported sort
* orders.
*
* Using detail filter with match flag MatchPhoneNumber is implemented by the
* contact model "phone number match" that filters by comparing the search
* string characters (digits) starting from the rightmost digit. The detail
* filter value must be at least 7 digits, otherwise an error code
* NotSupportedError is given. The actual digit count that is used is 7 to 15
* digits, depending on the configuration of the device.
*
* \a filter The QContactFilter to be used.
* \a sortOrders The sort orders to be used. If the sort orders are not
* supported by contacts database this parameter is ignored and sorting needs
* to be done by the caller.
* \a error On return, contains the possible error in filtering/sorting.
*/
QList<QContactLocalId> CntSymbianFilter::contacts(
const QContactFilter &filter,
const QList<QContactSortOrder> &sortOrders,
bool &filterSupportedFlag,
QContactManager::Error* error)
{
QList<QContactLocalId> result;
// No need to proceed if some of the filters in the chain is not supported
if(!filterSupportedFlag) return result;
// Intersection filter is handled by a recursive function call for each
// contained filter (unless at least one requires slow filtering)
if (filter.type() == QContactFilter::IntersectionFilter) {
QList<QContactFilter> filters = ((QContactIntersectionFilter) filter).filters();
for(int i(0); filterSupportedFlag && i < filters.count(); i++) {
if(result.isEmpty())
result = contacts(filters[i], sortOrders, filterSupportedFlag, error);
else
result = contacts(filters[i], sortOrders, filterSupportedFlag, error).toSet().intersect(result.toSet()).toList();
}
// Union filter is handled by a recursive function call for each
// contained filter (unless at least one requires slow filtering)
} else if (filter.type() == QContactFilter::UnionFilter) {
QList<QContactFilter> filters = ((QContactUnionFilter) filter).filters();
for(int i(0); filterSupportedFlag && i < filters.count(); i++) {
if(result.isEmpty())
result = contacts(filters[i], sortOrders, filterSupportedFlag, error);
else
result = (contacts(filters[i], sortOrders, filterSupportedFlag, error).toSet() + result.toSet()).toList();
}
// Detail filter with a string list is split and re-constructed into
// an intersection filter
} else if (filter.type() == QContactFilter::ContactDetailFilter
&& (static_cast<const QContactDetailFilter &>(filter)).value().type() == QVariant::StringList) {
QStringList values = (static_cast<const QContactDetailFilter &>(filter)).value().toStringList();
QContactIntersectionFilter intersectionFilter;
foreach(const QString& value, values) {
QContactDetailFilter detailFilter = filter;
detailFilter.setValue(value);
intersectionFilter.append(detailFilter);
}
// The resulting filter is handled with a recursive function call
result = contacts(intersectionFilter, sortOrders, filterSupportedFlag, error);
} else if (filter == QContactFilter()) {
// Empty filter -> don't do any filtering and tell the caller to not
// to do any filtering either
filterSupportedFlag = true;
result = filterContacts(QContactInvalidFilter(), error);
} else {
FilterSupport filterSupport = filterSupportLevel(filter);
if (filterSupport == Supported) {
filterSupportedFlag = true;
// Filter supported, use as the result directly
result = filterContacts(filter, error);
} else if (filterSupport == SupportedPreFilterOnly) {
// Filter only does pre-filtering, the caller is responsible of
// removing possible false positives after filtering
filterSupportedFlag = false;
result = filterContacts(filter, error);
} else if (filterSupport == IllegalFilter) {
// Don't do filtering; fail with an error
filterSupportedFlag = false;
*error = QContactManager::NotSupportedError;
} else {
// Don't do filtering here, return all contact ids and tell the
// caller to do slow filtering
filterSupportedFlag = false;
result = filterContacts(QContactInvalidFilter(), error);
}
}
return result;
}
/*!
* The contact database version implementation for
* QContactManager::filterSupport function.
*/
bool CntSymbianFilter::filterSupported(const QContactFilter& filter)
{
TBool result;
// Map filter support into a boolean value
FilterSupport support = filterSupportLevel(filter);
if (support == Supported || support == SupportedPreFilterOnly) {
result = true;
} else {
result = false;
}
return result;
}
/*!
* The possible return values are Supported, NotSupported and
* SupportedPreFilterOnly.
*
* Supported means that the filtering is implemented directly by the underlying
* database. NotSupported means that CntSymbianFilter::contacts will
* return an error. And SupportedPreFilterOnly means that the filter is not
* fully supported, but the CntSymbianFilter::contacts will act like the
* filter was supported for performance reasons, returning a result that may
* contain false positives. This means that the client must filter the
* pre-filtered set of contacts to see if there are false positives included.
* Note that the pre-filtering does not give any performance benefits if the
* result contains all contacts or almost all contacts.
*
* \a filter The QContactFilter to be checked.
* \a return Supported in case the filter is supported. NotSupported in case
* the filter is not supported. returns
*
* SupportedPreFilterOnly is returned in the following cases:
* 1. matchFlags is set to QContactFilter::MatchExactly (CntSymbianFilter::contacts
* will use QContactFilter::MatchContains)
* 2. matchFlags is set to QContactFilter::MatchStartsWith (CntSymbianFilter::contacts
* will use QContactFilter::MatchContains)
* 3. matchFlags is set to QContactFilter::MatchEndsWith (CntSymbianFilter::contacts
* will use QContactFilter::MatchContains)
* 4. matchFlags is set to QContactFilter::MatchCaseSensitive (CntSymbianFilter::contacts
* will use QContactFilter::MatchContains)
*/
CntAbstractContactFilter::FilterSupport CntSymbianFilter::filterSupportLevel(const QContactFilter& filter)
{
if (filter.type() == QContactFilter::ContactDetailFilter) {
const QContactDetailFilter &detailFilter = static_cast<const QContactDetailFilter &>(filter);
QContactFilter::MatchFlags matchFlags = detailFilter.matchFlags();
const QString defName = detailFilter.detailDefinitionName();
const QString fieldName = detailFilter.detailFieldName();
// Filter must target a certain field
if (fieldName.isEmpty()) {
return NotSupported;
}
// Phone numbers
if (defName == QContactPhoneNumber::DefinitionName) {
if (matchFlags == QContactFilter::MatchPhoneNumber) {
if (detailFilter.value().canConvert(QVariant::String)) {
if (detailFilter.value().toString().length() >= 7) {
return Supported;
} else {
// It is a feature of Symbian contact model that phone
// number match requires at least 7 digits. In case of
// phone number match it is best to give an error as a
// result because the phone number match logic would
// not be much of use with less than 7 digit matching.
// It would give false positives too often.
return IllegalFilter;
}
}
} else if (matchFlags == QContactFilter::MatchExactly
|| matchFlags == QContactFilter::MatchEndsWith
|| matchFlags == QContactFilter::MatchFixedString) {
if (detailFilter.value().canConvert(QVariant::String)) {
// It is a feature of Symbian contact model that phone
// number match requires at least 7 digits
if (detailFilter.value().toString().length() >= 7) {
return SupportedPreFilterOnly;
}
}
}
// Names
} else if (defName == QContactName::DefinitionName
|| defName == QContactNickname::DefinitionName
|| defName == QContactEmailAddress::DefinitionName) {
if (matchFlags == QContactFilter::MatchContains) {
return Supported;
}
// Don't care about case sensitivity flag because:
// 1) We do not support it. 2) We are doing prefiltering only.
matchFlags &= ~QContactFilter::MatchFlags(QContactFilter::MatchCaseSensitive);
if (matchFlags == QContactFilter::MatchExactly ||
matchFlags == QContactFilter::MatchContains ||
matchFlags == QContactFilter::MatchStartsWith ||
matchFlags == QContactFilter::MatchEndsWith ||
matchFlags == QContactFilter::MatchFixedString) {
return SupportedPreFilterOnly;
}
// display label, this is a special case that contains several name
// fields and company name
} else if (defName == QContactDisplayLabel::DefinitionName) {
if (matchFlags == QContactFilter::MatchStartsWith) {
return Supported;
}
// Don't care about case sensitivity flag because:
// 1) We do not support it. 2) We are doing prefiltering only.
matchFlags &= ~QContactFilter::MatchFlags(QContactFilter::MatchCaseSensitive);
if (matchFlags == QContactFilter::MatchStartsWith) {
return SupportedPreFilterOnly;
}
}
}
return NotSupported;
}
QList<QContactLocalId> CntSymbianFilter::filterContacts(
const QContactFilter& filter,
QContactManager::Error* error)
{
QList<QContactLocalId> matches;
CContactIdArray* idArray(0);
if (filter.type() == QContactFilter::InvalidFilter) {
TTime epoch(0);
idArray = m_contactDatabase.ContactsChangedSinceL(epoch); // return all contacts
} else if(filterSupportLevel(filter) == NotSupported) {
*error = QContactManager::NotSupportedError;
} else if (filter.type() == QContactFilter::ContactDetailFilter) {
const QContactDetailFilter &detailFilter = static_cast<const QContactDetailFilter &>(filter);
// Phone numbers
if (detailFilter.detailDefinitionName() == QContactPhoneNumber::DefinitionName) {
QString number((detailFilter.value()).toString());
TPtrC commPtr(reinterpret_cast<const TUint16*>(number.utf16()));
TInt matchLength(KDefaultMatchLength);
// no need to propagate error, we can use the default match length
TRAP_IGNORE(getMatchLengthL(matchLength));
TInt err = matchContacts(idArray, commPtr, matchLength);
if (err == KErrNone) {
// Phone number matching sometimes includes nonexisting contacts to
// the result for some reason. Remove them.
for (TInt i(0); i < idArray->Count(); ) {
if(!contactExists((*idArray)[i])) {
idArray->Remove(i);
} else {
i++;
}
}
} else {
CntSymbianTransformError::transformError(err, error);
}
// Names, e-mail, display label (other flags)
} else {
QString name((detailFilter.value()).toString());
TPtrC namePtr(reinterpret_cast<const TUint16*>(name.utf16()));
CContactItemFieldDef *fieldDef(0);
TRAPD(err, transformDetailFilterL(detailFilter, fieldDef));
if(err != KErrNone){
CntSymbianTransformError::transformError(err, error);
} else {
Q_ASSERT_X(fieldDef->Count() > 0, "CntSymbianFilter", "Illegal field def");
TInt err = findContacts(idArray, *fieldDef, namePtr);
if(err != KErrNone) {
CntSymbianTransformError::transformError(err, error);
}
// Display label special case with "starts with" flag;
// False positives are removed here for performance reasons
// (this is a very common use case in a name list view)
//
// Another option might be to use CContactDatabase::FindInTextDefLC
// for filtering with display label
if (detailFilter.detailDefinitionName() == QContactDisplayLabel::DefinitionName
&& detailFilter.matchFlags() == QContactFilter::MatchStartsWith) {
// Remove false positives
for(TInt i(0); i < idArray->Count(); ++i) {
CContactItem* contactItem = m_contactDatabase.ReadContactLC((*idArray)[i]);
const CContactItemFieldSet& fieldSet(contactItem->CardFields());
if(isFalsePositive(fieldSet, KUidContactFieldGivenName, namePtr)
&& isFalsePositive(fieldSet, KUidContactFieldFamilyName, namePtr)
&& isFalsePositive(fieldSet, KUidContactFieldCompanyName, namePtr)
&& isFalsePositive(fieldSet, KUidContactFieldSecondName, namePtr)){
idArray->Remove(i);
i--;
}
CleanupStack::PopAndDestroy(contactItem);
}
}
}
delete fieldDef;
}
}
if(idArray && (*error == QContactManager::NoError)) {
// copy the matching contact ids
for(int i(0); i < idArray->Count(); i++) {
matches.append(QContactLocalId((*idArray)[i]));
}
}
delete idArray;
return matches;
}
/*!
* Checks if the contact's field set includes field \a fieldTypeUid and if the
* field contents matches the search string. The features currently:
* 1. only checks the first field instance
* 2. supports only "starts with"
* 3. searches every word inside the field
* 4. supports only "not case-sensitive" matching
*/
bool CntSymbianFilter::isFalsePositive(const CContactItemFieldSet& fieldSet, const TUid& fieldTypeUid, const TDesC& searchString)
{
bool value(true);
TInt index = fieldSet.Find(fieldTypeUid);
if(index >= 0) {
const CContactItemField& field(fieldSet[index]);
CContactTextField* storage = field.TextStorage();
TPtrC text = storage->Text();
index = text.FindC(searchString);
// Check if this is the first word beginning with search string
if(index == 0)
value = false;
// Check if this is in the beginning of a word (the preceding
// character is a space)
else if(index > 0 && TChar(text[index-1]) == TChar(0x20))
value = false;
}
return value;
}
bool CntSymbianFilter::contactExists(const TContactItemId &contactId)
{
TRAPD(err, m_contactDatabase.ReadMinimalContactL(contactId));
return err == KErrNone;
}
/*!
* Transform detail filter into a contact item field definition that can be
* used with CContactDatabase finds.
*/
void CntSymbianFilter::transformDetailFilterL(
const QContactDetailFilter &detailFilter,
CContactItemFieldDef *&fieldDef)
{
const TInt defaultReserve(1);
const TInt nameFieldsCount(5);
CContactItemFieldDef* tempFieldDef = new (ELeave) CContactItemFieldDef();
CleanupStack::PushL(tempFieldDef);
tempFieldDef->SetReserveL(defaultReserve);
// TODO: Refactor to use the transform classes
// Names
if(detailFilter.detailDefinitionName() == QContactName::DefinitionName) {
if(detailFilter.detailFieldName() == QContactName::FieldPrefix) {
tempFieldDef->AppendL(KUidContactFieldPrefixName);
} else if(detailFilter.detailFieldName() == QContactName::FieldFirstName) {
tempFieldDef->AppendL(KUidContactFieldGivenName);
} else if(detailFilter.detailFieldName() == QContactName::FieldMiddleName) {
tempFieldDef->AppendL(KUidContactFieldAdditionalName);
} else if(detailFilter.detailFieldName() == QContactName::FieldLastName) {
tempFieldDef->AppendL(KUidContactFieldFamilyName);
} else if(detailFilter.detailFieldName() == QContactName::FieldSuffix) {
tempFieldDef->AppendL(KUidContactFieldSuffixName);
} else {
// default to all name fields
tempFieldDef->SetReserveL(nameFieldsCount);
tempFieldDef->AppendL(KUidContactFieldPrefixName);
tempFieldDef->AppendL(KUidContactFieldGivenName);
tempFieldDef->AppendL(KUidContactFieldAdditionalName);
tempFieldDef->AppendL(KUidContactFieldFamilyName);
tempFieldDef->AppendL(KUidContactFieldSuffixName);
}
}
// Nick name
else if(detailFilter.detailDefinitionName() == QContactNickname::DefinitionName) {
tempFieldDef->AppendL(KUidContactFieldSecondName);
}
// Email
else if(detailFilter.detailDefinitionName() == QContactEmailAddress::DefinitionName) {
tempFieldDef->AppendL(KUidContactFieldEMail);
}
// Display label
else if(detailFilter.detailDefinitionName() == QContactDisplayLabel::DefinitionName) {
// in S60 display label is constructed with "nick name", "first name",
// "last name" and/or "company name"
tempFieldDef->SetReserveL(nameFieldsCount);
tempFieldDef->AppendL(KUidContactFieldSecondName);
tempFieldDef->AppendL(KUidContactFieldGivenName);
tempFieldDef->AppendL(KUidContactFieldFamilyName);
tempFieldDef->AppendL(KUidContactFieldCompanyName);
}
CleanupStack::Pop(tempFieldDef);
fieldDef = tempFieldDef;
}
/*!
* Find contacts based on a contact field contents.
* \a idArray On return contains the ids of the contacts that have the field
* defined that contains the find text.
* \a fieldUid The UID of the contact database field to be searched.
* \a text The text to be searched for.
* \return Symbian error code.
*/
TInt CntSymbianFilter::findContacts(
CContactIdArray*& idArray,
const CContactItemFieldDef& fieldDef,
const TDesC& text) const
{
CContactIdArray* idArrayTmp(0);
TRAPD( err, idArrayTmp = findContactsL(fieldDef, text));
if(err == KErrNone)
{
idArray = idArrayTmp;
}
return err;
}
/*!
* Leaving implementation called by findContacts.
*/
CContactIdArray* CntSymbianFilter::findContactsL(
const CContactItemFieldDef& fieldDef,
const TDesC& text) const
{
CContactIdArray* idsArray = m_contactDatabase.FindLC(text, &fieldDef);
CleanupStack::Pop(idsArray); // Ownership transferred
return idsArray;
}
/*
* Find contacts based on a phone number.
* \a idArray On return contains the ids of the contacts that match the filter.
* \a phoneNumber The phone number to match
* \a matchLength Match length; digits from right.
*/
TInt CntSymbianFilter::matchContacts(
CContactIdArray*& idArray,
const TDesC& phoneNumber,
const TInt matchLength)
{
CContactIdArray* idArrayTmp(0);
TRAPD( err, idArrayTmp = m_contactDatabase.MatchPhoneNumberL(phoneNumber, matchLength));
if(err == KErrNone)
{
idArray = idArrayTmp;
}
return err;
}
/*
* Get the match length setting used in MatchPhoneNumber type filtering.
* \a matchLength Phone number digits to be used in matching (counted from
* right).
*/
void CntSymbianFilter::getMatchLengthL(TInt& matchLength)
{
//Get number of digits used to match
CRepository* repository = CRepository::NewL(KCRUidTelConfiguration);
CleanupStack::PushL(repository);
User::LeaveIfError(repository->Get(KTelMatchDigits, matchLength));
CleanupStack::PopAndDestroy(repository);
}
#endif