diff --git a/smsapp/CMakeLists.txt b/smsapp/CMakeLists.txt index 5f7607de..6a603d38 100644 --- a/smsapp/CMakeLists.txt +++ b/smsapp/CMakeLists.txt @@ -1,24 +1,52 @@ qt5_add_resources(KCSMS_SRCS resources.qrc) find_package(KF5People) -add_executable(kdeconnect-sms - main.cpp +add_library(kdeconnectsms conversationmodel.cpp conversationlistmodel.cpp +) + +set_target_properties(kdeconnectsms PROPERTIES + VERSION ${KDECONNECT_VERSION} + SOVERSION ${KDECONNECT_VERSION_MAJOR} +) + +generate_export_header(kdeconnectsms EXPORT_FILE_NAME ${CMAKE_CURRENT_BINARY_DIR}/kdeconnectsms_export.h BASE_NAME KDEConnectSmsAppLib) +target_include_directories(kdeconnectsms PUBLIC ${CMAKE_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR}) + +# If ever this library is actually used by someone else, we should export these headers +target_link_libraries(kdeconnectsms +LINK_PRIVATE + Qt5::Quick + Qt5::Widgets + KF5::People + kdeconnectinterfaces +) + +set(libkdeconnectsms_HEADERS + conversationmodel.h + conversationlistmodel.h + ${CMAKE_CURRENT_BINARY_DIR}/kdeconnectsms_export.h +) + +add_executable(kdeconnect-sms + main.cpp ${KCSMS_SRCS}) target_include_directories(kdeconnect-sms PUBLIC ${CMAKE_BINARY_DIR}) target_link_libraries(kdeconnect-sms + kdeconnectsms kdeconnectinterfaces Qt5::Quick Qt5::Widgets KF5::CoreAddons KF5::DBusAddons KF5::I18n KF5::People ) install(TARGETS kdeconnect-sms ${INSTALL_TARGETS_DEFAULT_ARGS}) +install(TARGETS kdeconnectsms ${INSTALL_TARGETS_DEFAULT_ARGS} LIBRARY NAMELINK_SKIP) install(PROGRAMS org.kde.kdeconnect.sms.desktop DESTINATION ${KDE_INSTALL_APPDIR}) diff --git a/smsapp/conversationlistmodel.cpp b/smsapp/conversationlistmodel.cpp index b6a1a299..bf1a6017 100644 --- a/smsapp/conversationlistmodel.cpp +++ b/smsapp/conversationlistmodel.cpp @@ -1,215 +1,289 @@ /** * Copyright (C) 2018 Aleix Pol Gonzalez * Copyright (C) 2018 Simon Redman * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "conversationlistmodel.h" +#include #include #include "interfaces/conversationmessage.h" #include "interfaces/dbusinterfaces.h" Q_LOGGING_CATEGORY(KDECONNECT_SMS_CONVERSATIONS_LIST_MODEL, "kdeconnect.sms.conversations_list") +OurSortFilterProxyModel::OurSortFilterProxyModel(){} +OurSortFilterProxyModel::~OurSortFilterProxyModel(){} + ConversationListModel::ConversationListModel(QObject* parent) : QStandardItemModel(parent) , m_conversationsInterface(nullptr) { //qCDebug(KDECONNECT_SMS_CONVERSATIONS_LIST_MODEL) << "Constructing" << this; auto roles = roleNames(); roles.insert(FromMeRole, "fromMe"); roles.insert(AddressRole, "address"); roles.insert(PersonUriRole, "personUri"); roles.insert(ConversationIdRole, "conversationId"); roles.insert(DateRole, "date"); setItemRoleNames(roles); ConversationMessage::registerDbusType(); } ConversationListModel::~ConversationListModel() { } void ConversationListModel::setDeviceId(const QString& deviceId) { if (deviceId == m_deviceId) { return; } if (deviceId.isEmpty()) { return; } qCDebug(KDECONNECT_SMS_CONVERSATIONS_LIST_MODEL) << "setDeviceId" << deviceId << "of" << this; if (m_conversationsInterface) { disconnect(m_conversationsInterface, SIGNAL(conversationCreated(QVariantMap)), this, SLOT(handleCreatedConversation(QVariantMap))); disconnect(m_conversationsInterface, SIGNAL(conversationUpdated(QVariantMap)), this, SLOT(handleConversationUpdated(QVariantMap))); delete m_conversationsInterface; m_conversationsInterface = nullptr; } m_deviceId = deviceId; Q_EMIT deviceIdChanged(); // This method still gets called *with a valid deviceID* when the device is not connected while the component is setting up // Detect that case and don't do anything. DeviceDbusInterface device(deviceId); if (!(device.isValid() && device.isReachable())) { return; } m_conversationsInterface = new DeviceConversationsDbusInterface(deviceId, this); connect(m_conversationsInterface, SIGNAL(conversationCreated(QVariantMap)), this, SLOT(handleCreatedConversation(QVariantMap))); connect(m_conversationsInterface, SIGNAL(conversationUpdated(QVariantMap)), this, SLOT(handleConversationUpdated(QVariantMap))); prepareConversationsList(); m_conversationsInterface->requestAllConversationThreads(); } void ConversationListModel::prepareConversationsList() { if (!m_conversationsInterface->isValid()) { qCWarning(KDECONNECT_SMS_CONVERSATIONS_LIST_MODEL) << "Tried to prepareConversationsList with an invalid interface!"; return; } QDBusPendingReply validThreadIDsReply = m_conversationsInterface->activeConversations(); setWhenAvailable(validThreadIDsReply, [this](const QVariantList& convs) { clear(); // If we clear before we receive the reply, there might be a (several second) visual gap! for (const QVariant& headMessage : convs) { QDBusArgument data = headMessage.value(); QVariantMap message; data >> message; handleCreatedConversation(message); } }, this); } void ConversationListModel::handleCreatedConversation(const QVariantMap& msg) { createRowFromMessage(msg); } void ConversationListModel::handleConversationUpdated(const QVariantMap& msg) { createRowFromMessage(msg); } void ConversationListModel::printDBusError(const QDBusError& error) { qCWarning(KDECONNECT_SMS_CONVERSATIONS_LIST_MODEL) << error; } QStandardItem * ConversationListModel::conversationForThreadId(qint32 threadId) { for(int i=0, c=rowCount(); idata(ConversationIdRole) == threadId) return it; } return nullptr; } void ConversationListModel::createRowFromMessage(const QVariantMap& msg) { const ConversationMessage message(msg); if (message.type() == -1) { // The Android side currently hacks in -1 if something weird comes up // TODO: Remove this hack when MMS support is implemented return; } bool toadd = false; QStandardItem* item = conversationForThreadId(message.threadID()); if (!item) { toadd = true; item = new QStandardItem(); QScopedPointer personData(lookupPersonByAddress(message.address())); if (personData) { item->setText(personData->name()); item->setIcon(QIcon(personData->photo())); item->setData(personData->personUri(), PersonUriRole); } else { item->setData(QString(), PersonUriRole); item->setText(message.address()); } item->setData(message.threadID(), ConversationIdRole); } // Update the message if the data is newer // This will be true if a conversation receives a new message, but false when the user // does something to trigger past conversation history loading bool oldDateExists; qint64 oldDate = item->data(DateRole).toLongLong(&oldDateExists); if (!oldDateExists || message.date() >= oldDate) { // If there was no old data or incoming data is newer, update the record item->setData(message.address(), AddressRole); item->setData(message.type() == ConversationMessage::MessageTypeSent, FromMeRole); item->setData(message.body(), Qt::ToolTipRole); item->setData(message.date(), DateRole); } if (toadd) appendRow(item); } KPeople::PersonData* ConversationListModel::lookupPersonByAddress(const QString& address) { const QString& canonicalAddress = canonicalizePhoneNumber(address); int rowIndex = 0; for (rowIndex = 0; rowIndex < m_people.rowCount(); rowIndex++) { const QString& uri = m_people.get(rowIndex, KPeople::PersonsModel::PersonUriRole).toString(); KPeople::PersonData* person = new KPeople::PersonData(uri); - const QString& email = person->email(); - const QString& phoneNumber = canonicalizePhoneNumber(person->contactCustomProperty("phoneNumber").toString()); - - // To decide if a phone number matches: - // 1. Are they similar lengths? If two numbers are very different, probably one is junk data and should be ignored - // 2. Is one a superset of the other? Phone number digits get more specific the further towards the end of the string, - // so if one phone number ends with the other, it is probably just a more-complete version of the same thing - const QString& longerNumber = canonicalAddress.length() >= phoneNumber.length() ? canonicalAddress : phoneNumber; - const QString& shorterNumber = canonicalAddress.length() < phoneNumber.length() ? canonicalAddress : phoneNumber; + const QStringList& allEmails = person->allEmails(); + for (const QString& email : allEmails) { + // Although we are nominally an SMS messaging app, it is possible to send messages to phone numbers using email -> sms bridges + if (address == email) { + return person; + } + } - bool matchingPhoneNumber = longerNumber.endsWith(shorterNumber) && shorterNumber.length() * 2 >= longerNumber.length(); + // TODO: Either upgrade KPeople with an allPhoneNumbers method + const QVariantList allPhoneNumbers = person->contactCustomProperty(QStringLiteral("all-phoneNumber")).toList(); + for (const QVariant& rawPhoneNumber : allPhoneNumbers) { + const QString& phoneNumber = canonicalizePhoneNumber(rawPhoneNumber.toString()); + bool matchingPhoneNumber = isPhoneNumberMatchCanonicalized(canonicalAddress, phoneNumber); - if (address == email || matchingPhoneNumber) { - //qCDebug(KDECONNECT_SMS_CONVERSATIONS_LIST_MODEL) << "Matched" << address << "to" << person->name(); - return person; + if (matchingPhoneNumber) { + //qCDebug(KDECONNECT_SMS_CONVERSATIONS_LIST_MODEL) << "Matched" << address << "to" << person->name(); + return person; + } } delete person; } return nullptr; } +bool ConversationListModel::isPhoneNumberMatchCanonicalized(const QString& canonicalPhone1, const QString& canonicalPhone2) +{ + // To decide if a phone number matches: + // 1. Are they similar lengths? If two numbers are very different, probably one is junk data and should be ignored + // 2. Is one a superset of the other? Phone number digits get more specific the further towards the end of the string, + // so if one phone number ends with the other, it is probably just a more-complete version of the same thing + const QString& longerNumber = canonicalPhone1.length() >= canonicalPhone2.length() ? canonicalPhone1 : canonicalPhone2; + const QString& shorterNumber = canonicalPhone1.length() < canonicalPhone2.length() ? canonicalPhone1 : canonicalPhone2; + + const CountryCode& country = determineCountryCode(longerNumber); + + const bool shorterNumberIsShortCode = isShortCode(shorterNumber, country); + const bool longerNumberIsShortCode = isShortCode(longerNumber, country); + + if ((shorterNumberIsShortCode && !longerNumberIsShortCode) || (!shorterNumberIsShortCode && longerNumberIsShortCode)) { + // If only one of the numbers is a short code, they clearly do not match + return false; + } + + bool matchingPhoneNumber = longerNumber.endsWith(shorterNumber); + + return matchingPhoneNumber; +} + +bool ConversationListModel::isPhoneNumberMatch(const QString& phone1, const QString& phone2) +{ + const QString& canonicalPhone1 = canonicalizePhoneNumber(phone1); + const QString& canonicalPhone2 = canonicalizePhoneNumber(phone2); + + return ConversationListModel::isPhoneNumberMatchCanonicalized(canonicalPhone1, canonicalPhone2); +} + +bool ConversationListModel::isShortCode(const QString& phoneNumber, const ConversationListModel::CountryCode& country) +{ + // Regardless of which country this number belongs to, a number of length less than 6 is a "short code" + if (phoneNumber.length() <= 6) { + return true; + } + if (country == CountryCode::Australia && phoneNumber.length() == 8 && phoneNumber.startsWith("19")) { + return true; + } + if (country == CountryCode::CzechRepublic && phoneNumber.length() <= 9) { + // This entry of the Wikipedia article is fairly poorly written, so it is not clear whether a + // short code with length 7 should start with a 9. Leave it like this for now, upgrade as + // we get more information + return true; + } + return false; +} + +ConversationListModel::CountryCode ConversationListModel::determineCountryCode(const QString& canonicalNumber) +{ + // This is going to fall apart if someone has not entered a country code into their contact book + // or if Android decides it can't be bothered to report the country code, but probably we will + // be fine anyway + if (canonicalNumber.startsWith("41")) { + return CountryCode::Australia; + } + if (canonicalNumber.startsWith("420")) { + return CountryCode::CzechRepublic; + } + + // The only countries I care about for the current implementation are Australia and CzechRepublic + // If we need to deal with further countries, we should probably find a library + return CountryCode::Other; +} + QString ConversationListModel::canonicalizePhoneNumber(const QString& phoneNumber) { QString toReturn(phoneNumber); toReturn = toReturn.remove(' '); toReturn = toReturn.remove('-'); toReturn = toReturn.remove('('); toReturn = toReturn.remove(')'); toReturn = toReturn.remove('+'); toReturn = toReturn.remove(QRegularExpression("^0*")); // Strip leading zeroes return toReturn; } diff --git a/smsapp/conversationlistmodel.h b/smsapp/conversationlistmodel.h index 238fbf6c..283cfba4 100644 --- a/smsapp/conversationlistmodel.h +++ b/smsapp/conversationlistmodel.h @@ -1,121 +1,154 @@ /** * Copyright (C) 2018 Aleix Pol Gonzalez * Copyright (C) 2018 Simon Redman * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef CONVERSATIONLISTMODEL_H #define CONVERSATIONLISTMODEL_H #include #include #include #include #include #include "interfaces/conversationmessage.h" #include "interfaces/dbusinterfaces.h" +#include "kdeconnectsms_export.h" + Q_DECLARE_LOGGING_CATEGORY(KDECONNECT_SMS_CONVERSATIONS_LIST_MODEL) -class OurSortFilterProxyModel : public QSortFilterProxyModel, public QQmlParserStatus +class KDECONNECTSMSAPPLIB_EXPORT OurSortFilterProxyModel : public QSortFilterProxyModel, public QQmlParserStatus { Q_OBJECT Q_INTERFACES(QQmlParserStatus) Q_PROPERTY(Qt::SortOrder sortOrder READ sortOrder WRITE setSortOrder) public: Qt::SortOrder sortOrder() const { return m_sortOrder; } void setSortOrder(Qt::SortOrder sortOrder) { if (m_sortOrder != sortOrder) { m_sortOrder = sortOrder; sortNow(); } } void classBegin() override {} void componentComplete() override { m_completed = true; sortNow(); } + OurSortFilterProxyModel(); + ~OurSortFilterProxyModel(); + private: void sortNow() { if (m_completed && dynamicSortFilter()) sort(0, m_sortOrder); } bool m_completed = false; Qt::SortOrder m_sortOrder = Qt::AscendingOrder; }; -class ConversationListModel +class KDECONNECTSMSAPPLIB_EXPORT ConversationListModel : public QStandardItemModel { Q_OBJECT Q_PROPERTY(QString deviceId READ deviceId WRITE setDeviceId NOTIFY deviceIdChanged) public: ConversationListModel(QObject* parent = nullptr); ~ConversationListModel(); enum Roles { FromMeRole = Qt::UserRole, PersonUriRole, AddressRole, ConversationIdRole, DateRole, }; Q_ENUM(Roles) + enum CountryCode { + Australia, + CzechRepublic, + Other, // I only care about a few country codes + }; + QString deviceId() const { return m_deviceId; } void setDeviceId(const QString &/*deviceId*/); + /** + * Return true to indicate the two phone numbers should be considered the same, false otherwise + */ + static bool isPhoneNumberMatch(const QString& phone1, const QString& phone2); + + /** + * Return true to indicate the two phone numbers should be considered the same, false otherwise + * Requires canonicalized phone numbers as inputs + */ + static bool isPhoneNumberMatchCanonicalized(const QString& canonicalPhone1, const QString& canonicalPhone2); + + /** + * See inline comments for how short codes are determined + * All information from https://en.wikipedia.org/wiki/Short_code + */ + static bool isShortCode(const QString& canonicalNumber, const CountryCode& country); + + /** + * Try to guess the country code from the passed number + */ + static CountryCode determineCountryCode(const QString& canonicalNumber); + + /** + * Simplify a phone number to a known form + */ + static QString canonicalizePhoneNumber(const QString& phoneNumber); + public Q_SLOTS: void handleCreatedConversation(const QVariantMap& msg); void handleConversationUpdated(const QVariantMap& msg); void createRowFromMessage(const QVariantMap& message); void printDBusError(const QDBusError& error); Q_SIGNALS: void deviceIdChanged(); private: /** * Get all conversations currently known by the conversationsInterface, if any */ void prepareConversationsList(); /** * Get the data for a particular person given their contact address */ KPeople::PersonData* lookupPersonByAddress(const QString& address); - /** - * Simplify a phone number to a known form - */ - QString canonicalizePhoneNumber(const QString& phoneNumber); - QStandardItem* conversationForThreadId(qint32 threadId); DeviceConversationsDbusInterface* m_conversationsInterface; QString m_deviceId; KPeople::PersonsModel m_people; }; #endif // CONVERSATIONLISTMODEL_H diff --git a/smsapp/conversationmodel.h b/smsapp/conversationmodel.h index ddab1c69..f8cbb72d 100644 --- a/smsapp/conversationmodel.h +++ b/smsapp/conversationmodel.h @@ -1,73 +1,75 @@ /** * Copyright (C) 2018 Aleix Pol Gonzalez * Copyright (C) 2018 Simon Redman * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef CONVERSATIONMODEL_H #define CONVERSATIONMODEL_H #include #include #include #include "interfaces/dbusinterfaces.h" +#include "kdeconnectsms_export.h" + Q_DECLARE_LOGGING_CATEGORY(KDECONNECT_SMS_CONVERSATION_MODEL) #define INVALID_THREAD_ID -1 -class ConversationModel +class KDECONNECTSMSAPPLIB_EXPORT ConversationModel : public QStandardItemModel { Q_OBJECT Q_PROPERTY(qint64 threadId READ threadId WRITE setThreadId) Q_PROPERTY(QString deviceId READ deviceId WRITE setDeviceId) public: ConversationModel(QObject* parent = nullptr); ~ConversationModel(); enum Roles { FromMeRole = Qt::UserRole, DateRole, }; Q_ENUM(Roles) qint64 threadId() const; void setThreadId(const qint64& threadId); QString deviceId() const { return m_deviceId; } void setDeviceId(const QString &/*deviceId*/); Q_INVOKABLE void sendReplyToConversation(const QString& message); Q_INVOKABLE void requestMoreMessages(const quint32& howMany = 10); private Q_SLOTS: void createRowFromMessage(const QVariantMap &msg, int pos); void handleConversationUpdate(const QVariantMap &msg); private: DeviceConversationsDbusInterface* m_conversationsInterface; QString m_deviceId; qint64 m_threadId = INVALID_THREAD_ID; QSet knownMessageIDs; // Set of known Message uIDs }; #endif // CONVERSATIONMODEL_H diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index f81dace9..b9cc8dfd 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,33 +1,43 @@ find_package(Qt5 ${QT_MIN_VERSION} REQUIRED COMPONENTS Test) include_directories( ${KDEConnectCore_BINARY_DIR} ${CMAKE_SOURCE_DIR} ${CMAKE_BINARY_DIR} ${CMAKE_BINARY_DIR}/plugins/sendnotifications/ + ${CMAKE_BINARY_DIR}/smsapp/ ) set(kdeconnect_libraries kdeconnectcore KF5::I18n KF5::KIOWidgets Qt5::DBus Qt5::Network Qt5::Test qca-qt5 ) +set(kdeconnect_sms_libraries + ${kdeconnect_libraries} + kdeconnectinterfaces + kdeconnectsms + Qt5::Quick + KF5::People +) + ecm_add_test(pluginloadtest.cpp LINK_LIBRARIES ${kdeconnect_libraries}) ecm_add_test(sendfiletest.cpp LINK_LIBRARIES ${kdeconnect_libraries}) ecm_add_test(networkpackettests.cpp LINK_LIBRARIES ${kdeconnect_libraries}) ecm_add_test(testsocketlinereader.cpp TEST_NAME testsocketlinereader LINK_LIBRARIES ${kdeconnect_libraries}) ecm_add_test(testsslsocketlinereader.cpp TEST_NAME testsslsocketlinereader LINK_LIBRARIES ${kdeconnect_libraries}) ecm_add_test(kdeconnectconfigtest.cpp TEST_NAME kdeconnectconfigtest LINK_LIBRARIES ${kdeconnect_libraries}) ecm_add_test(lanlinkprovidertest.cpp TEST_NAME lanlinkprovidertest LINK_LIBRARIES ${kdeconnect_libraries}) ecm_add_test(devicetest.cpp TEST_NAME devicetest LINK_LIBRARIES ${kdeconnect_libraries}) ecm_add_test(testnotificationlistener.cpp ../plugins/sendnotifications/sendnotificationsplugin.cpp ../plugins/sendnotifications/notificationslistener.cpp ../plugins/sendnotifications/notifyingapplication.cpp TEST_NAME testnotificationlistener LINK_LIBRARIES ${kdeconnect_libraries} Qt5::DBus KF5::Notifications KF5::IconThemes) + ecm_add_test(testconversationlistmodel.cpp LINK_LIBRARIES ${kdeconnect_sms_libraries}) diff --git a/tests/testconversationlistmodel.cpp b/tests/testconversationlistmodel.cpp new file mode 100644 index 00000000..ce0f712d --- /dev/null +++ b/tests/testconversationlistmodel.cpp @@ -0,0 +1,246 @@ +/** + * Copyright 2019 Simon Redman + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "conversationlistmodel.h" + +#include + +/** + * This class tests the working of device class + */ +class ConversationListModelTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + + void testSimplePhoneMatch(); + void testMessyPhoneMatch(); + void testMissingCountryCode(); + void testLeadingZeros(); + void testLongDistancePhoneNumber(); + void testShortCodePhoneNumberNonMatch(); + void testShortCodeMatch(); + void testShortCodeNonMatch(); + void testAustralianShortCodeNumberNonMatch(); + void testAustralianShortCodeMatch(); + void testAustralianShortCodeNonMatch(); + void testCzechRepublicShortCodeNumberNonMatch(); + void testCzechRepublicShortCodeMatch(); + void testCzechRepublicShortCodeNonMatch(); + void testDifferentPhoneNumbers1(); + void testDifferentPhoneNumbers2(); +}; + +/** + * Two phone numbers which are exactly the same should match + */ +void ConversationListModelTest::testSimplePhoneMatch() +{ + const QString& phoneNumber = QLatin1String("+1 (222) 333-4444"); + + QVERIFY2(ConversationListModel::isPhoneNumberMatch(phoneNumber, phoneNumber), + "Failed to match a phone number with itself"); +} + +/** + * Two phone numbers which have been entered with different formatting should match + */ +void ConversationListModelTest::testMessyPhoneMatch() +{ + const QString& phoneNumber = QLatin1String("12223334444"); + const QString& messyPhoneNumber = QLatin1String("+1 (222) 333-4444"); + + QVERIFY2(ConversationListModel::isPhoneNumberMatch(phoneNumber, messyPhoneNumber), + "Failed to match same number with different formatting characters"); +} + +/** + * I don't think most people in the US even know what a country code *is*, and I know lots of people + * who don't enter it into their contacts book here. Make sure that kind of match works. + */ +void ConversationListModelTest::testMissingCountryCode() +{ + const QString& shortForm = QLatin1String("(222) 333-4444"); + const QString& longForm = QLatin1String("+1 (222) 333-4444"); + + QVERIFY2(ConversationListModel::isPhoneNumberMatch(shortForm, longForm), + "Failed to match two same numbers with missing country code"); +} + +/** + * I don't quite understand which cases this applies, but sometimes you see a message where the + * country code has been prefixed with a few zeros. Make sure that works too. + */ +void ConversationListModelTest::testLeadingZeros() +{ + const QString& shortForm = QLatin1String("+1 (222) 333-4444"); + const QString& zeroForm = QLatin1String("001 (222) 333-4444"); + + QVERIFY2(ConversationListModel::isPhoneNumberMatch(shortForm, zeroForm), + "Failed to match two same numbers with padded zeros"); +} + +/** + * At least on my phone, it is possible to leave the area code and country code off but still have + * the phone call or text message go through. Some people's contacts books might be this way, so make + * sure we support matching that too + */ +void ConversationListModelTest::testLongDistancePhoneNumber() +{ + const QString& shortForm = QLatin1String("333-4444"); + const QString& longForm = QLatin1String("+1 (222) 333-4444"); + + QVERIFY2(ConversationListModel::isPhoneNumberMatch(shortForm, longForm), + "Failed to suffix match two phone numbers"); +} + +/** + * Phone operators define short codes for a variety of reasons. Even if they have the same suffix, + * they should not match a regular phone number + */ +void ConversationListModelTest::testShortCodePhoneNumberNonMatch() +{ + const QString& shortCode = QLatin1String("44455"); + const QString& normalNumber = QLatin1String("222-334-4455"); + + QVERIFY2(!ConversationListModel::isPhoneNumberMatch(shortCode, normalNumber), + "Short code matched with normal number"); +} + +/** + * Two of the same short code should be able to match + */ +void ConversationListModelTest::testShortCodeMatch() +{ + const QString& shortCode = QLatin1String("44455"); + QVERIFY2(ConversationListModel::isPhoneNumberMatch(shortCode, shortCode), + "Did not match two of the same short code"); +} + +/** + * Two different short codes should not match + */ +void ConversationListModelTest::testShortCodeNonMatch() +{ + const QString& shortCode1 = QLatin1String("44455"); + const QString& shortCode2 = QLatin1String("66677"); + QVERIFY2(!ConversationListModel::isPhoneNumberMatch(shortCode1, shortCode2), + "Incorrectly matched two different short codes"); +} + +/** + * Even in the land down under, a short code phone number should not match a regular phone number + */ +void ConversationListModelTest::testAustralianShortCodeNumberNonMatch() +{ + const QString& australianShortCode = QLatin1String("19333444"); + // I'm not sure if this is even a valid Australian phone number, but whatever... + const QString& australianPhoneNumber = QLatin1String("+41 02 1233 3444"); + + QVERIFY2(!ConversationListModel::isPhoneNumberMatch(australianShortCode, australianPhoneNumber), + "Matched Australian short code with regular phone number"); +} + +/** + * Two of the same Australian short code numbers should be able to match + */ +void ConversationListModelTest::testAustralianShortCodeMatch() +{ + const QString& australianShortCode = QLatin1String("12333444"); + + QVERIFY2(ConversationListModel::isPhoneNumberMatch(australianShortCode, australianShortCode), + "Failed to match Australian short code number"); +} + +/** + * Two different Australian short code numbers should be able to match + */ +void ConversationListModelTest::testAustralianShortCodeNonMatch() +{ + const QString& australianShortCode1 = QLatin1String("12333444"); + const QString& australianShortCode2 = QLatin1String("12555666"); + + QVERIFY2(!ConversationListModel::isPhoneNumberMatch(australianShortCode1, australianShortCode2), + "Matched two different Australian short code numbers"); +} + +/** + * A Czech Republic short code phone number should not match a regular phone number + */ +void ConversationListModelTest::testCzechRepublicShortCodeNumberNonMatch() +{ + const QString& czechRepShortCode = QLatin1String("9090930"); + // I'm not sure if this is even a valid Czech Republic phone number, but whatever... + const QString& czechRepPhoneNumber = QLatin1String("+420 809 090 930"); + + QVERIFY2(!ConversationListModel::isPhoneNumberMatch(czechRepShortCode, czechRepPhoneNumber), + "Matched Czech Republic short code with regular phone number"); +} + +/** + * Two of the same Czech Republic short code numbers should be able to match + */ +void ConversationListModelTest::testCzechRepublicShortCodeMatch() +{ + const QString& czechRepShortCode = QLatin1String("9090930"); + + QVERIFY2(ConversationListModel::isPhoneNumberMatch(czechRepShortCode, czechRepShortCode), + "Failed to match Czech Republic short code number"); +} + +/** + * Two different Czech Republic short code numbers should be able to match + */ +void ConversationListModelTest::testCzechRepublicShortCodeNonMatch() +{ + const QString& czechRepShortCode1 = QLatin1String("9090930"); + const QString& czechRepShortCode2 = QLatin1String("9080990"); + + QVERIFY2(!ConversationListModel::isPhoneNumberMatch(czechRepShortCode1, czechRepShortCode2), + "Matched two different Czech Republic short code numbers"); +} + +/** + * Two phone numbers which are different should not be reported as the same + */ +void ConversationListModelTest::testDifferentPhoneNumbers1() +{ + const QString& phone1 = QLatin1String("+1 (222) 333-4444"); + const QString& phone2 = QLatin1String("+1 (333) 222-4444"); + + QVERIFY2(!ConversationListModel::isPhoneNumberMatch(phone1, phone2), + "Incorrectly marked two different numbers as same"); +} + +/** + * Two phone numbers which are different should not be reported as the same + */ +void ConversationListModelTest::testDifferentPhoneNumbers2() +{ + const QString& phone1 = QLatin1String("+1 (222) 333-4444"); + const QString& phone2 = QLatin1String("122-2333"); + + QVERIFY2(!ConversationListModel::isPhoneNumberMatch(phone1, phone2), + "Incorrectly *prefix* matched two phone numbers"); +} + +QTEST_MAIN(ConversationListModelTest); +#include "testconversationlistmodel.moc"