pax_global_header00006660000000000000000000000064145545024670014526gustar00rootroot0000000000000052 comment=5afbb36b5040699d3a8f4faf0167fb272353a402 history-service-0.5/000077500000000000000000000000001455450246700145315ustar00rootroot00000000000000history-service-0.5/.gitignore000066400000000000000000000017501455450246700165240ustar00rootroot00000000000000*/*_automoc* */*/*_automoc* *.cbp CMakeCache.txt CMakeFiles */CMakeFiles */cmake_install.cmake cmake_install.cmake config.h coverage* */CTestTestfile.cmake CTestTestfile.cmake daemon/history-daemon */*.desktop Makefile */Makefile */*/*.moc */*/*/moc_ */*/moc_* */moc_* plugins/sqlite/*.depends plugins/sqlite/qrc_* plugins/sqlite/schema/schema.sql */*.service daemon/historyserviceadaptor.* src/*.pc src/libhistoryservice.so* src/pluginthreadviewadaptor.* src/plugineventviewadaptor.* Testing */tests/*Test test_*.xml tools/*/history-* *.user install_manifest.txt plugins/sqlite/*qrc plugins/sqlite/schema/version.info daemon/tests/mock/telepathy-mock* daemon/tests/mock/mockconnectionadaptor* daemon/tests/dbus-test-wrapper.sh tests/*/*/*Test tests/*/*Test tests/*/moc_* tests/*/*/moc_* tests/*/*.moc* tests/*/*/*.moc* tests/*/*automoc* tests/*/*/*automoc* tests/common/dbus-services/*service tests/common/dbus-session.conf tests/common/mock/telepathy-mock tests/common/mock/mockconnectionadaptor.* history-service-0.5/AUTHORS000066400000000000000000000005101455450246700155750ustar00rootroot00000000000000Alberto Mardegan Alfred Neumayer Dalton Durst Dan Chapman Florian Boucault Florian Leeber Guido Berhoerster Gustavo Pichorim Boiko Jezek JEzEk Ken VanDine Lionel Duboeuf Marius Marius Gripsgard Mike Gabriel Ratchanan Srirattanamet Renato Araujo Oliveira Filho Roberto Mier Escandon Robert Tari Tiago Salem Herrmann Timo Jyrinki history-service-0.5/CMakeLists.txt000066400000000000000000000105201455450246700172670ustar00rootroot00000000000000cmake_minimum_required(VERSION 3.5) project(history-service VERSION 0.5 LANGUAGES CXX) set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules) # Standard install paths include(GNUInstallDirs) # Check for include files include(CheckIncludeFileCXX) include(CheckIncludeFile) # Instruct CMake to run moc automatically when needed. set(CMAKE_AUTOMOC ON) find_package(Qt5Contacts) find_package(Qt5DBus) find_package(Qt5Qml) find_package(Qt5Quick) find_package(Qt5Test) find_package(Qt5Network) find_package(LibPhoneNumber REQUIRED) include(qt5) if(NOT CMAKE_CROSSCOMPILING) find_program(QMAKE_EXECUTABLE qmake) if(QMAKE_EXECUTABLE STREQUAL "QMAKE_EXECUTABLE-NOTFOUND") message(FATAL_ERROR "qmake not found") endif() execute_process( COMMAND ${QMAKE_EXECUTABLE} -query QT_INSTALL_QML RESULT_VARIABLE RESULT OUTPUT_VARIABLE QT_INSTALL_QML OUTPUT_STRIP_TRAILING_WHITESPACE ) if(NOT RESULT EQUAL 0) message(FATAL_ERROR "Failed to determine QT_INSTALL_QML from qmake") endif() else() # qmake isn't multi-arch aware as it installs arch-specific mkspec files # in to /usr/share, so we can't use it here (we'd need a qmake binary # for the host arch using data for the target arch) set(QT_INSTALL_QML "/usr/lib/${CMAKE_LIBRARY_ARCHITECTURE}/qt5/qml") endif() find_package(PkgConfig REQUIRED) pkg_check_modules(TP_QT5 REQUIRED TelepathyQt5) pkg_check_modules(SQLITE3 REQUIRED sqlite3) pkg_check_modules(QTGLIB QtGLib-2.0) pkg_check_modules(SYSTEMD REQUIRED systemd) find_program(DBUS_RUNNER dbus-test-runner) option(TRACE_SQLITE "Print Sqlite commants to the log." off) if (${TRACE_SQLITE}) add_definitions(-DTRACE_SQLITE) endif() add_definitions(-DQT_NO_KEYWORDS) include_directories( ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR} ) set(HISTORY_PLUGIN_PATH ${CMAKE_INSTALL_LIBDIR}/${PROJECT_NAME}/plugins) set(TEST_DATA_DIR "${CMAKE_SOURCE_DIR}/tests/data") configure_file(config.h.in ${CMAKE_CURRENT_BINARY_DIR}/config.h @ONLY) # generate a macro to make it easier to enable coverage support on targets function(ENABLE_COVERAGE) get_directory_property(COVERAGE_TARGETS DIRECTORY ${CMAKE_SOURCE_DIR} COVERAGE_TARGETS) list(APPEND COVERAGE_TARGETS ${ARGN}) MESSAGE(STATUS "Enabling coverage report for target(s): ${ARGN}") set_property(DIRECTORY ${CMAKE_SOURCE_DIR} PROPERTY COVERAGE_TARGETS ${COVERAGE_TARGETS}) endfunction() enable_testing() add_definitions(-std=c++11) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=gnu99 -Wall") # TODO remove the need for -Wno-unused-variable # unused-variable currently fails because of public headers includes static char's # there is also a gcc bug that makes it not possable to rely on diagnostic pragmas # https://gcc.gnu.org/bugzilla/show_bug.cgi?id=69967 set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wno-unused-variable") option(ENABLE_WERROR "Treat all build warnings as errors" ON) if(ENABLE_WERROR) add_compile_options(-Werror) endif() # Define the version to be used in the library set(HISTORY_VERSION_MAJOR 0) set(HISTORY_VERSION_MINOR 0) set(HISTORY_VERSION_PATCH 0) set(PACKAGE_VERSION ${HISTORY_VERSION_MAJOR}.${HISTORY_VERSION_MINOR}.${HISTORY_VERSION_PATCH}) add_subdirectory(src) add_subdirectory(daemon) add_subdirectory(plugins) add_subdirectory(tools) add_subdirectory(Lomiri) add_subdirectory(tests) find_package(CoverageReport) # Coverage ##################################################################### # Enable code coverage calculation with gcov/gcovr/lcov # Usage: # * Switch build type to coverage (use ccmake or cmake-gui) # * Invoke make, make test, make coverage # * Find html report in subdir coveragereport # * Find xml report feasible for jenkins in coverage.xml ##################################################################### IF(CMAKE_BUILD_TYPE MATCHES [cC][oO][vV][eE][rR][aA][gG][eE]) SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -ftest-coverage -fprofile-arcs" ) SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -ftest-coverage -fprofile-arcs" ) SET(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -coverage" ) SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -coverage" ) GET_DIRECTORY_PROPERTY(COVERAGE_TARGETS DIRECTORY ${CMAKE_SOURCE_DIR} COVERAGE_TARGETS) ENABLE_COVERAGE_REPORT(TARGETS ${COVERAGE_TARGETS}) ENDIF(CMAKE_BUILD_TYPE MATCHES [cC][oO][vV][eE][rR][aA][gG][eE]) history-service-0.5/COPYING000066400000000000000000001045131455450246700155700ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) 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 3 of the License, or (at your option) any later version. 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 . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . history-service-0.5/ChangeLog000066400000000000000000004157661455450246700163260ustar00rootroot000000000000002024-01-25 Mike Gabriel * Release 0.5 (HEAD -> main, tag: 0.5) 2023-11-10 Marius Gripsgard * Merge branch 'personal/lduboeuf/fix_deadlock' into 'main' (d31262e) 2023-10-18 Lionel Duboeuf * clickable it (d328120) 2023-10-17 Lionel Duboeuf * Avoid deadlock while ContactManager being destroyed (e978395) 2023-04-06 Marius Gripsgard * Merge branch 'personal/lduboeuf/fix_emblemcount' into 'main' (8dd212b) 2023-04-03 Lionel Duboeuf * emblem counter: prevent un-pinned app to be shown after boot fixes: https://gitlab.com/ubports/development/core/messaging-app/-/issues/330 (1dd41db) 2023-03-03 Mike Gabriel * Merge branch 'personal/lduboeuf/debug_logs' into 'main' (1fb4d61) 2023-03-03 Lionel Duboeuf * add logs (7bf1a0d) * create a fallback fileName in case of empty attachment identifier fixes: https://gitlab.com/ubports/development/core/history-service/-/issues/12 (4def364) 2023-01-18 Mike Gabriel * Merge branch 'personal/lduboeuf/fix_zerocounter' into 'main' (bd0a14d) 2023-01-17 Lionel Duboeuf * emblemcounter: set the emblem visible before setting count property when messaging-app/dialer app is not pinned, launcher service only creates the emblem on countVisible property, in that case the count is initialized to zero. (c6c9061) 2023-01-13 Guido Berhoerster * Merge branch 'personal/sunweaver/enable-Werror-CMake-option' into 'main' (77c1b2a) 2023-01-13 Mike Gabriel * CMakeLists.txt: Add ENABLE_WERROR CMake option, defaulting to ON. (1370777) * Release 0.4 (385e179) (tag: 0.4) 2023-01-13 Guido Berhoerster * Merge branch 'personal/sunweaver/project-name-and-version' into 'main' (c7163fa) 2023-01-13 Mike Gabriel * CMake: Specify VERSION in project() function, use PROJECT_NAME variable instead of hard-coded 'history-service', bump to CMake 3.5 (from 3.4). (9c77b77) * d/changelog: undo release-like thing in d/changelog (introduced via 2a0f65c1) (178da48) * .bzr-builddeb/default.conf: Drop file. End-of-bzr. (53b4b3e) 2023-01-12 Mike Gabriel * Merge branch 'main_-_removeAttachments' into 'main' (3bfd269) 2022-03-31 Lionel Duboeuf * Remove attachments management Store attachments with their file name Add MMS tests (2a0f65c) 2023-01-06 Mike Gabriel * Merge branch 'main_-_testmigration' into 'main' (0c1a702) 2022-11-29 Lionel Duboeuf * Add ofono accounts migration management The ofono accounts ids may differ between devices. If a user import his old history database into a new device, we can have accountIds mismatch, making messaging-app impracticable. This check is done when Telepathy registers a new account ( at startup or when adding a SIM card ) allow us to migrate old account Ids ( e.g ofono/ofono/account0 to ofono/ofono/ril_0) in database table (47d75dc) * Allow to disable triggers when needed (63eb790) 2022-11-26 Mike Gabriel * Merge branch 'changelog-xenial-to-focal' into 'main' (cfc8286) 2022-01-10 jEzEk * Update debian/changelog (06ee7ba) 2022-11-26 Mike Gabriel * Merge branch 'main_-_emblemcounter' into 'main' (ed9eb9a) 2022-04-26 Lionel Duboeuf * add messaging-app / dialer-app emblem counter management (842fff3) 2022-08-01 Ratchanan Srirattanamet * Merge branch 'fix-dbus-apparmor-check' into 'main' (cd4d85f) 2022-08-01 Guido Berhoerster * Allow DBus activation by AppArmor-confined apps (46855a9) 2022-04-25 Ratchanan Srirattanamet * Merge branch 'personal/peat-psuwit/merge-xenial-2022Apr' into 'main' (a99df26) 2022-04-21 Ratchanan Srirattanamet * Merge remote-tracking branch 'origin/ubports/xenial' into personal/peat-psuwit/merge-xenial-2022Apr (e104db5) 2022-02-28 Guido Berhoerster * Merge branch 'ubports/focal_-_lomiri-telephony-apps' into 'main' (c5fd484) 2022-02-21 Guido Berhoerster * Remove unused and hidden desktop file for history-daemon (51f4156) 2022-02-15 Guido Berhoerster * Convert upstart jobs to DBus activated systemd units (f3a6e76) 2022-01-28 Guido Berhoerster * Rename SQLite plugin (e1ef85e) * Rename DBus namespace from com.canonical to com.lomiri (87f1432) * Rename qtdeclarative5-ubuntu-history0.1 to qml-module-lomiri-history0.1 (2533159) 2021-12-16 Florian Leeber * Merge branch 'xenial_-_removeevents' into 'ubports/xenial' (88201fb) 2021-12-16 Lionel Duboeuf * Add remove event method by Filter and type (3c57393) 2021-12-15 Dalton Durst * Merge branch 'personal/fredldotme/mms-to-xenial' into 'ubports/xenial' (ffe0e85) 2021-12-15 jezek * Failed MMS redownload (f7630d6) 2021-12-15 Alfred Neumayer * Merge branch 'xenial_-_failed-MMS-redownload' into 'main' (0683655) 2021-12-15 jezek * Failed MMS redownload (8faf763) 2021-12-06 Guido Berhoerster * Merge branch 'ubports/focal_-_history-service_armhf_fix' into 'main' (7914ff7) * Increase test default timeout to 120 s in order to fix armhf build failures (61570a6) * Merge branch 'main' into 'main' (68cb187) * Port to focal (74a5109) 2021-12-04 Marius Gripsgard * Merge branch 'ubports/xenial' into 'main' (d41485d) * Merge branch 'ubports/focal' into 'main' (39c579c) * Merge branch 'main' into 'ubports/focal' (9071f32) * Merge branch 'personal/usb/move-jenkinsfile' into 'ubports/xenial' (60e0f00) 2021-12-02 Dalton Durst * Delete snapcraft.yaml (cc790cd) * Move bzrignore to gitignore (1422039) * Move Jenkinsfile to debian/Jenkinsfile (23ba146) 2021-11-24 Marius Gripsgard * Merge branch 'mr/rename-com-ubuntu-touch-accountsservice-phone' into 'main' (e3717c4) * Merge branch 'mr/drop-wextra' into 'main' (c47a493) 2021-11-23 Robert Tari * CMakeLists.txt: Remove the -Wextra flag (523d770) 2021-11-17 Guido Berhoerster * Merge branch 'ubports/focal' into 'ubports/focal' (389fc6c) 2021-11-16 Guido Berhoerster * Do not hardcode dconf-service path (dfbd706) 2021-11-11 Robert Tari * Rename com.ubuntu.touch.AccountsService.Phone (6aa1706) 2021-05-26 Dalton Durst * Merge pull request #39 from lduboeuf/xenial_-_upgrade_issue (17c341e) 2021-05-26 Lionel Duboeuf * prevent V19.sql from being executed when column already exist (e3c26ed) * make sure not to downgrade the db version number (722ca69) 2021-05-25 Lionel Duboeuf * protect sql update from failing if triggers already exists (#38) (d9009dc) 2021-05-21 Lionel Duboeuf * don't update sentTime column on db upgrade. For big history message, that would lead to query execution timeout and hsitory daemon not activated. Instead ensure that sentTime is always here when fetching/writing (34133bd) 2021-05-14 Lionel Duboeuf * Manage the sent date for sms/mms (#32) (2a94dd4) 2021-01-26 Florian Leeber * Merge pull request #36 from lduboeuf/xenial_-_updateJenkinsFile (94cdf95) 2021-01-26 Lionel Duboeuf * update Jenkinsfile (04ef122) 2020-10-08 Marius Gripsgard * Use cmake-extras (#33) (23c69ec) * Clean up code to build with -Wall -Werror -Wextra (#30) (8285a4a) 2020-10-07 Marius Gripsgard * Replace deprecated QString::null with QString() (#31) (93d628b) 2020-09-30 Marius Gripsgard * Merge remote-tracking branch 'origin/xenial_-_deprecated-qstringnull' into main (a6da6a9) * Merge remote-tracking branch 'origin/xenial_-_cmake-extras' into main (d5fd3d6) * Use make -j1 to do testing (bc65158) * Use cmake-extras (52caf49) * [plugin/sqlite] Add assert if argc is less than requied (7a45f07) 2020-09-12 Marius Gripsgard * Fix implicit-fallthrough warning (7d7a07f) * Fix maybe-uninitialized warning (bb6754b) * Clean up code to build with -Wall -Werror -Wextra (5051fad) 2020-09-10 Marius Gripsgard * Make sure to return value (8be02ee) 2020-09-11 Marius Gripsgard * Replace deprecated QString::null with QString() (d74a07e) 2020-07-11 Florian Leeber * Merge pull request #27 from lduboeuf/xenial_-_fixsentdate (1aa75ce) 2020-07-11 Lionel Duboeuf * tab instead of space for rule file (ae5599c) * test CI (ebaef1f) * update changelog (3986ce7) * Revert "use the message sent date instead of the received one" (40390c4) 2020-07-11 Florian Leeber * Merge pull request #26 from lduboeuf/xenial_-_updatechangelog (b3fafeb) 2020-07-09 Lionel Duboeuf * update changelog (d6f9e44) 2020-03-10 Lionel Duboeuf * add exclusion filter. This allow to query history db with "Not Equals" statement (0c7aecb) 2020-07-08 Florian Leeber * Merge pull request #23 from mardy/tests-debug (f5df67a) 2020-07-06 Alberto Mardegan * Build: declare history-qml a module (a2bdb93) 2020-07-05 Alberto Mardegan * Build: do not link to QML plugin, directly use its sources (a938bf9) 2020-07-06 Alberto Mardegan * Build: fix duplicate file in Ubuntu.History (0a65e1b) 2020-07-03 Alberto Mardegan * Revert disabling of tests (cbb2f9a) 2020-06-26 Florian Leeber * Merge pull request #22 from ubports/xenial_-_disable-tests (d26cb06) * Unfortunately test disabling does not work, remove them entirely (8abbac2) 2020-06-25 Florian Leeber * disable for real (3913871) * Disable instable tests (f1cfc1b) 2020-06-18 Florian Leeber * Merge pull request #13 from lduboeuf/xenial_-_storemessagesentdate (ed91470) 2020-04-03 Florian Leeber * Merge pull request #19 from lduboeuf/xenial_-_readme (629165d) 2020-04-03 Lionel Duboeuf * add readme (8e22380) 2020-04-02 Florian Leeber * Merge pull request #17 from lduboeuf/xenial (fc2613a) 2020-04-01 Lionel Duboeuf * update changelog: fix wrong version number (1520757) * update changelog (18ce6e2) 2020-03-31 Florian Leeber * Merge pull request #11 from lduboeuf/xenial_-_draftstatus (ef4e513) 2020-03-25 Lionel Duboeuf * fix tests (4146238) * use the message sent date instead of the received one (f87f4e5) 2020-02-03 Lionel Duboeuf * add MessageStatus Draft (9dc221b) 2018-09-18 Alberto Mardegan * Update changelog (1eeb840) * ContactMatcher: Use C++11 signal connections (f5933b2) * Build: allow building with crossbuilder (3a69c76) 2018-09-01 Marius Gripsgard * Merge pull request #5 from mardy/xenial_-_latestqtpim (f4569c5) 2018-07-25 Alberto Mardegan * Rebuild against latest QtPim (f0eef3b) 2018-03-06 Marius Gripsgard * Fix fix changes that got lost in bzr to git merge (6b20f49) * Remove duplicated function (651a0cd) * New release (ed9f191) * Merge remote-tracking branch 'lp-s/master' into xenial (1ca1ee5) 2018-03-06 Marius * Merge staging (4cc0bcc) 2018-01-11 Dan Chapman * Imported to UBports (8d31f29) 2017-03-22 Gustavo Pichorim Boiko * Fix return value of a function that is now asynchronous (77ebca5) * Fix return value of a function that is now asynchronous (20ce0c6) * Make it possible to debug sqlite commands. (08fe84a) * Do not load the participants from threads automatically. If the client really needs it, it can use the newly added API to fetch the participants. (df2a42f) * Use a QLockFile to ensure there will be only one instance of the daemon per user. As we now delay the registration on dbus, sometimes we ended up having two instances of the daeon running (because of dbus activation). This change makes sure that won't happen. (d78e3ba) * Reduce the dbus traffic when marking messages and threads as read. (75df779) * Allow pass multiple fields on sort clause. (3dda074) * Mark entire conversations as read. (f812ee2) * Improve Roles management performance by caching the retrieved data. (0c91e3d) * Only start saving information events about contacts joining and leaving after the self contact is in the local list of participants. (f4dd83a) * Improve the notifications of participants changing (38b09ac) * Adapt to support VOIP accounts. (1380851) * Remove debugs (db783dc) 2017-03-22 Renato Araujo Oliveira Filho * Code optimize. (eca54b8) 2017-03-21 Gustavo Pichorim Boiko * Rebase this branch using the VOIP and IRC changes. (5fc1804) 2017-03-20 Gustavo Pichorim Boiko * Fix thread ack query (a531e49) 2017-03-15 Gustavo Pichorim Boiko * Only mark threads as read if it has unread events. (60c6ed5) 2017-03-15 Tiago Salem Herrmann * Avoid duplicate join and leave events (20edd59) 2017-03-14 Gustavo Pichorim Boiko * Fix filtering of events emitted in signals. (3cb2802) 2017-03-10 Gustavo Pichorim Boiko * When extracting properties from text channels, avoid parsing participants when there is no need for it. (f95720d) * Merge parent (5396dfb) 2017-03-09 Gustavo Pichorim Boiko * History-daemon is not a system service, so store a lock somewhere in the user's home directory to allow running one instance per user. (6df7f69) * Cache the requests that didn't match any contact, and fix a wrong condition. (b459d2e) * Deliver the threads with the participants filled for the threads we know will need it. (42a745a) 2017-03-09 Tiago Salem Herrmann * merge parent branch (3f16f75) * merge parent branch (f9d59fb) * revert wrong commit (bb09d82) 2017-03-08 Tiago Salem Herrmann * merge parent branch (7f12a93) * merge parent branch (660910a) * merge parent branch (0466b0c) 2017-03-08 Gustavo Pichorim Boiko * Do not load the participants from threads automatically. If the client really needs it, it can use the newly added API to fetch the participants. (47943dd) * Make sure tests use different lock files so that they can run simultaneously (0c95795) 2017-03-03 Gustavo Pichorim Boiko * Use a QLockFile to ensure there will be only one instance of the daemon per user. As we now delay the registration on dbus, sometimes we ended up having two instances of the daemon running (because of dbus activation). This change makes sure that won't happen. (d16db0c) * When threads are removed, make sure the events models get refreshed accordingly. (74b8851) 2017-03-02 Gustavo Pichorim Boiko * Update tests (0e9c848) * Optimize thread removal by deleting the events using a sqlite trigger instead of manually removing and notifying them. Also make the thread and event removal asynchronous calls on dbus level as the result will be notified back to us as signals. (b0c8a00) * Stop using the telepathy pending message status to mark messages as read as that causes performance problems when dealing with a large number of messages. (b3a7504) 2017-02-23 Tiago Salem Herrmann * fix build (ca553ea) 2017-02-21 Tiago Salem Herrmann * merge parent branch (c95f6aa) * merge parent branch (0105cc9) * merge parent branch (d4f6674) * merge parent branch (85b8712) * merge parent branch (0ffb331) * only remove participants from irc rooms (8f30e94) 2017-02-17 Gustavo Pichorim Boiko * Only update the new events (eff554e) 2017-02-16 Gustavo Pichorim Boiko * Buffer marking threads as read and get rid of duplicate calls. Also, change the markEventAsRead to not fetch the event from the daemon. (45fc630) * Move the mark{Event,Threads}AsRead to HistoryModel so that all models can use it. (ce708d7) 2017-02-15 Gustavo Pichorim Boiko * merge parent (8a3a0ea) * Buffer the emission of Threads{Added,Removed,Modified} and Events{Added,Removed,Modified} signals on DBus to avoid unecessary round trips (5180b6f) 2017-02-15 Tiago Salem Herrmann * merge parent branch (9b6c794) * merge parent branch (2b41158) * merge parent branch (38651e4) * merge parent branch (01a154b) * merge parent branch (ec27365) 2017-02-14 Gustavo Pichorim Boiko * Move the status mapping to a separate function (f1ff477) 2017-02-07 Renato Araujo Oliveira Filho * Keep history model sorted if multiple fields are specified. (1f19eff) * Update comments. (7b063e4) * Create unit test for multiple fields sort query. (d5eab29) * Allow pass multiple fields on sort clause. (78999a0) 2017-02-07 Tiago Salem Herrmann * merge parent branch (14b37d1) * Mark entire conversations as read (befaf5e) 2017-02-06 Tiago Salem Herrmann * merge trunk (01bd55f) 2017-02-03 Tiago Salem Herrmann * Improve roles management (1a28cf9) * merge parent (2ca46d7) 2017-02-03 Gustavo Pichorim Boiko * Make sure IDs passed to the models are normalized. (baa7e86) 2017-02-02 Gustavo Pichorim Boiko * Handle missed VOIP calls. (ac1c859) 2017-02-01 Tiago Salem Herrmann * merge parent (b2982c7) * remove participants from events (005e83a) 2017-01-30 Gustavo Pichorim Boiko * Fix method return value. (f5f6284) 2017-01-27 Gustavo Pichorim Boiko * Avoid using participants in text events, and also avoid fetching threads when not needed. (48ef078) 2017-01-26 Gustavo Pichorim Boiko * Only start saving information events about contacts joining and leaving after the self contact is in the local list of participants. (88c23fd) 2017-01-25 Tiago Salem Herrmann * Improve performance (4d47d0f) 2017-01-06 Gustavo Pichorim Boiko * Handle contact match for SIP accounts (cb54981) 2017-01-04 Gustavo Pichorim Boiko * Merge trunk. (84bda40) 2016-11-30 Bileto Bot * Releasing 0.1+17.04.20161130-0ubuntu1 (cfbae89) * Releasing 0.1+17.04.20161130-0ubuntu1 (012ce33) 2016-11-30 Gustavo Pichorim Boiko * Improve group chat support. (2f3272c) * Improve group chat support. (bd71ad5) 2016-11-24 Bileto Bot * Releasing 0.1+17.04.20161124.1-0ubuntu1 (455eeb3) * Releasing 0.1+17.04.20161124.1-0ubuntu1 (6e1eac9) 2016-11-24 Renato Araujo Oliveira Filho * Append "SNAP" prefix into the path to the plugins dir if none is set. (80d4f68) * Append "SNAP" prefix into the path to the plugins dir if none is set. (a00eee8) 2016-11-24 Gustavo Pichorim Boiko * Simplify the filtering of participants. (c278f65) * Simplify the filtering of participants. (c275d17) * Remove obsolete comment (53c2b98) * Remove obsolete comment (fb12c4d) * Use a more complete time format to generate the hash. (a143e41) * Use a more complete time format to generate the hash. (e091846) * Use compareIds() (179686f) * Use compareIds() (a2657c9) 2016-11-23 Gustavo Pichorim Boiko * Remove debug prints. (f8d7206) * Remove debug prints. (cc7a003) * And one more loop. (b6b2522) * And one more loop. (cc53746) * Simplify one more iteration (357ca44) * Simplify one more iteration (1fa7137) * Simplify the filtering of participants. (72ef8e4) * Simplify the filtering of participants. (9159b54) 2016-11-21 Renato Araujo Oliveira Filho * Does not use "CLICK_MODE" build flag. (c7b63e4) * Does not use "CLICK_MODE" build flag. (5a4f27e) * Created snap package. (af24479) * Created snap package. (f60fb9f) * Append "SNAP" prefix into the path to the plugins dir if none is set. (f5f1caa) * Append "SNAP" prefix into the path to the plugins dir if none is set. (ec9500e) 2016-11-17 Gustavo Pichorim Boiko * - Fix broken tests with latest changes. - Set ChatType correctly when using createThreadForParticipants() (73ff5e3) * - Fix broken tests with latest changes. - Set ChatType correctly when using createThreadForParticipants() (4be7024) 2016-11-17 Tiago Salem Herrmann * Set ChatType correctly when using createThreadForParticipants() (5ff316a) * Set ChatType correctly when using createThreadForParticipants() (c486b07) 2016-11-17 Gustavo Pichorim Boiko * - Create hash for threadId of broadcast messages - Avoid grouping chats with different chatType's (bf6a784) * - Create hash for threadId of broadcast messages - Avoid grouping chats with different chatType's (b6b8c41) 2016-11-16 Tiago Salem Herrmann * - Create hash for threadId of broadcast messages - Avoid grouping chats with different chatType's (8eec940) * - Create hash for threadId of broadcast messages - Avoid grouping chats with different chatType's (fd7c37a) 2016-11-09 Gustavo Pichorim Boiko * Update existing chats to Room or None based on the MMS option in Accounts Service. (8aecf37) * Update existing chats to Room or None based on the MMS option in Accounts Service. (6e7a97d) * Allow applications to insert different kind of information events. (65fa732) * Allow applications to insert different kind of information events. (9fb930a) 2016-11-09 Tiago Salem Herrmann * Allow applications to insert different kind of information events (c25f3e7) * Allow applications to insert different kind of information events (5c33e85) 2016-11-08 Tiago Salem Herrmann * Address comments (11587b0) * Address comments (20b0ad2) * Update existing chats to Room or None based on the mms option in Accounts Service (f7a5c46) * Update existing chats to Room or None based on the mms option in Accounts Service (79df8ad) 2016-11-03 Gustavo Pichorim Boiko * Leave the mock call channel opened for a bit longer to make sure the last state change propagates correctly. (2c12f2c) * Leave the mock call channel opened for a bit longer to make sure the last state change propagates correctly. (98de322) * Leave the mock call channel opened for a bit longer to make sure the last state change propagates correctly. (0ebab07) * Leave the mock call channel opened for a bit longer to make sure the last state change propagates correctly. (c244d0a) 2016-11-02 Gustavo Pichorim Boiko * Fix tests on latest tp-qt. (c715962) * Fix tests on latest tp-qt. (3a283fa) * Fix building with latest tp-qt. (177da86) * Fix building with latest tp-qt. (14d3957) 2016-10-28 Gustavo Pichorim Boiko * Skip self join notification in conversation when account is a ofono one (84aa6c7) * Skip self join notification in conversation when account is a ofono one (8e8b2f1) 2016-10-28 Roberto Mier Escandon * avoid showing self join message when account is a ofono one (4d315fc) * avoid showing self join message when account is a ofono one (3a58f11) 2016-10-27 Gustavo Pichorim Boiko * Adapt the registration of objects and services to the way QtDBus works from 5.6.x on. (d221109) * Adapt the registration of objects and services to the way QtDBus works from 5.6.x on. (781fcb9) * Re-enable DaemonTests. (bc5ce1c) * Re-enable DaemonTests. (59f646a) 2016-10-20 Gustavo Pichorim Boiko * Fix destroying threadviews by not leaving queries opened. (fb3b3b9) * Fix destroying threadviews by not leaving queries opened. (332ea69) * RolesClientInterface implementation to access roles interface features Received events when roles changes write roles event information only when received updates for roles (isolating roles updates from members updates) added db plugin method to update roles for members (08b08e8) * RolesClientInterface implementation to access roles interface features Received events when roles changes write roles event information only when received updates for roles (isolating roles updates from members updates) added db plugin method to update roles for members (8cd9aeb) * Added event info for group dissolved (7c85263) * Added event info for group dissolved (78f9196) 2016-10-20 Roberto Mier Escandon * resolved conflicts (d184032) * resolved conflicts (cea906f) * removed not needed local rules configuration (97e69b1) * removed not needed local rules configuration (2e17c34) 2016-10-20 Gustavo Pichorim Boiko * - added sender as parameter for info events just in case event needs two actors (source and destination of the action) - added subjectAsAlias event model role to translate contact id<->alias before showing, to avoid having id some milliseconds before showing the alias (358d60f) * - added sender as parameter for info events just in case event needs two actors (source and destination of the action) - added subjectAsAlias event model role to translate contact id<->alias before showing, to avoid having id some milliseconds before showing the alias (ac5f4e5) * Adds information events in history service for invited people (remote pending) Second and next times same contact is invited any new info event is added, since it was already previously invited (a9adc13) * Adds information events in history service for invited people (remote pending) Second and next times same contact is invited any new info event is added, since it was already previously invited (8d3bc1f) * Use subject2 telepathy interface instead of roomConfig in order to have actor when updated the title (6f4d99d) * Use subject2 telepathy interface instead of roomConfig in order to have actor when updated the title (7de8e1a) * Improve information events handling. Now all strings have to be translated on the app side. (ada712b) * Improve information events handling. Now all strings have to be translated on the app side. (cae49fa) * Show events in conversation when group modifications happen (members updates, title, etc..) (49d19a7) * Show events in conversation when group modifications happen (members updates, title, etc..) (d1d31e4) 2016-10-20 Roberto Mier Escandon * change 'You' by 'self' when referring to myself (2b45e87) * change 'You' by 'self' when referring to myself (06f869b) * removed not needed comments (bca1893) * removed not needed comments (7a84b23) * addressed comments (dfc767f) * addressed comments (900b172) 2016-10-20 Gustavo Pichorim Boiko * Remove obsolete comments (2e64af6) * Remove obsolete comments (fe0e0b4) 2016-10-19 Gustavo Pichorim Boiko * Trying to get in history daemon the event of group cancelled to insert a new event in related thread, to be seen in UI. (0db49f1) * Trying to get in history daemon the event of group cancelled to insert a new event in related thread, to be seen in UI. (134bb07) * Initial group chat support. (4a019a6) * Initial group chat support. (67a7dd1) 2016-10-19 Tiago Salem Herrmann * we have to also group broadcast messages (334160a) * we have to also group broadcast messages (a0c009a) 2016-10-19 Gustavo Pichorim Boiko * Add comments on things that need fixing (6102632) * Add comments on things that need fixing (473f571) 2016-10-18 Gustavo Pichorim Boiko * Fix destroying threadviews by not leaving queries opened. (301f85f) * Fix destroying threadviews by not leaving queries opened. (a651c98) 2016-10-06 Tiago Salem Herrmann * merge parent branch (7f515f9) * merge parent branch (8520097) * merge parent branch (393cb9a) * merge parent branch (dda8340) * merge parent branch (86ddf10) * merge parent branch (44285f6) * merge parent branch (d8636b4) * merge parent branch (09862b9) * merge parent branch (2168484) * merge parent branch (0248f24) * merge parent branch (5e03161) * merge parent branch (418a4b5) * merge parent branch (869218b) * merge parent branch (c12b99a) * merge parent branch (010d0b5) * merge parent branch (08a5f3c) * merge parent branch (e20d668) * merge parent branch (7cbbc4d) 2016-09-28 Gustavo Pichorim Boiko * Fix the crash in one more place. (c170873) * Fix the crash in one more place. (de6b80b) * Do not try to access the roles interface when not available (df9f925) * Do not try to access the roles interface when not available (fa6c392) 2016-09-26 Roberto Mier Escandon * removed traces (29af378) * removed traces (6f4290c) 2016-09-22 Roberto Mier Escandon * added copyright for new files (34a97e3) * added copyright for new files (2d06b84) 2016-09-21 Roberto Mier Escandon * updated plugin to have independant roles management (3fdcf0e) * updated plugin to have independant roles management (1dc69fb) * updated interface to get properties syncronously (298bc56) * updated interface to get properties syncronously (dfde7b9) * first success (fc824e2) * first success (e6211d2) 2016-09-20 Roberto Mier Escandon * tracing properties and event (6d9c254) * tracing properties and event (8870925) * merged prereq lp:~phablet-team/history-service/end-group-info-event (8fb7a94) * merged prereq lp:~phablet-team/history-service/end-group-info-event (31cf656) 2016-09-19 Roberto Mier Escandon * implemented roles interface client (59df643) * implemented roles interface client (f25858e) 2016-09-19 Gustavo Pichorim Boiko * Adapt the registration of objects and services to the way QtDBus works from 5.6.x on. (b8f7b0e) * Adapt the registration of objects and services to the way QtDBus works from 5.6.x on. (a23a6e7) * Merge parent branch (e4b2658) * Merge parent branch (31dbc7f) 2016-09-19 Roberto Mier Escandon * fixed (c79e6d5) * fixed (6516b35) 2016-09-16 Roberto Mier Escandon * move LastEventRole to be the last one in the enum (16ac747) * move LastEventRole to be the last one in the enum (7822e98) * added subjectAsAlias event model role to translate contact id<->alias before showing, to avoid having id some milliseconds before showing the alias added sender as parameter for info events just in case event needs two actors (source and destination of the action) (1374ecb) * added subjectAsAlias event model role to translate contact id<->alias before showing, to avoid having id some milliseconds before showing the alias added sender as parameter for info events just in case event needs two actors (source and destination of the action) (5cbf7ca) 2016-09-15 Roberto Mier Escandon * implemented event information for invited people (9d71238) * implemented event information for invited people (58d903d) 2016-09-14 Roberto Mier Escandon * merged parent branch (6d14842) * merged parent branch (5b97f8b) * differenciate self leaving from self kicked (28c448c) * differenciate self leaving from self kicked (b66eb99) * merged prereq branch lp:~phablet-team/history-service/improve_information_events (f4d6b1e) * merged prereq branch lp:~phablet-team/history-service/improve_information_events (d79021c) 2016-09-09 Tiago Salem Herrmann * export flags and fix missing properties (393763b) * export flags and fix missing properties (c0c648a) * Change information event types (5b75966) * Change information event types (b938c50) 2016-09-09 Gustavo Pichorim Boiko * Re-enable DaemonTests (82b597b) * Re-enable DaemonTests (39e1dee) 2016-09-09 Bileto Bot * Releasing 0.1+16.10.20160909.1-0ubuntu1 (15044fd) * Releasing 0.1+16.10.20160909.1-0ubuntu1 (a499313) 2016-09-09 Gustavo Pichorim Boiko * Temporarily disable DaemonTest until we get QtDBus fixed. (919fba0) * Temporarily disable DaemonTest until we get QtDBus fixed. (350a94a) * Temporarily disable DaemonTest until we get QtDBus fixed. (c70091f) * Temporarily disable DaemonTest until we get QtDBus fixed. (d581f4e) 2016-09-06 Tiago Salem Herrmann * merge parent branch (418669f) * merge parent branch (912bfeb) 2016-09-06 Roberto Mier Escandon * use subject interface instead of roomconfig one to manage the title (61f2f0d) * use subject interface instead of roomconfig one to manage the title (1ac7d65) * fix update schema sentences (1fbc93b) * fix update schema sentences (0b2df63) * added schema changes (6709c8d) * added schema changes (dd8eb48) 2016-09-01 Roberto Mier Escandon * managed roles to write admin ownership event information (037ad9d) * managed roles to write admin ownership event information (268dd11) * added self roles field to chatRoomInfo (bc34ad0) * added self roles field to chatRoomInfo (c4afa1e) * added information event for group change (273a48a) * added information event for group change (7aa4455) * finished imple and tested members fluctuation (59881d2) * finished imple and tested members fluctuation (4117e90) * added some comments (d3dd899) * added some comments (a2046bb) * show join event notification only when really joining, not when creating the group or the thread (1260aa8) * show join event notification only when really joining, not when creating the group or the thread (ecaa8fc) 2016-08-31 Roberto Mier Escandon * initial code for members fluctuation messages (13f53cd) * initial code for members fluctuation messages (d37aa3b) 2016-08-29 Tiago Salem Herrmann * merge parent branch (8f45aed) * merge parent branch (95e735b) * skip update if no properties are found (d6ab1b0) * skip update if no properties are found (0970652) 2016-08-26 Tiago Salem Herrmann * merge parent branch (5d46ce9) * merge parent branch (fb3148a) * remove pending members from participants (27458e3) * remove pending members from participants (9a96822) 2016-08-26 Roberto Mier Escandon * changed 'not' to ! (b9cf2b3) * changed 'not' to ! (757c469) * ?? (bded82e) * ?? (b495b7e) * check that thread is not empty (6b2a105) * check that thread is not empty (526fc6d) * check that thread is not empty (b169c6d) * check that thread is not empty (bd5518d) * rolledback not needed changes (a5a7736) * rolledback not needed changes (3f5f31c) * simplified solution (9ce1e94) * simplified solution (eae9d0c) 2016-08-25 Tiago Salem Herrmann * set state as well (425074d) * set state as well (2df190a) 2016-08-26 Roberto Mier Escandon * logical try to get the close event in history daemon in order to write related event (da86164) * logical try to get the close event in history daemon in order to write related event (5acfa3b) 2016-08-25 Tiago Salem Herrmann * Export Roles to qml (ef2d915) * Export Roles to qml (c0d1037) * add support for contact roles (66f854c) * add support for contact roles (1e202f8) 2016-08-23 Tiago Salem Herrmann * add state to participants (4b8a7da) * add state to participants (7254ec2) 2016-08-18 Tiago Salem Herrmann * avoid adding invalid values into the map (4bcfcac) * avoid adding invalid values into the map (4eaf15a) 2016-08-17 Tiago Salem Herrmann * set ChatRoomInfo to new threads (3da6699) * set ChatRoomInfo to new threads (841e7a8) 2016-08-16 Tiago Salem Herrmann * set Requested = true so other clients know if the channel is new and requested or not (c41a889) * set Requested = true so other clients know if the channel is new and requested or not (598c640) * compile agains new qt (5d43531) * compile agains new qt (9b831e3) 2016-07-11 Gustavo Pichorim Boiko * Merge trunk (f490612) * Merge trunk (fe68c01) * Remove unwanted debug (ddf500c) * Remove unwanted debug (8f5234f) * Make sure empty threads have a reasonable timestamp and show in the right position on the list. (5b0de86) * Make sure empty threads have a reasonable timestamp and show in the right position on the list. (fc7737f) * Fix packaging for crossbuilding. (e88488a) * Fix packaging for crossbuilding. (2b6d8d6) * Make sure new threads get a good timestamp. (9fdb0e7) * Make sure new threads get a good timestamp. (b11c869) * Make sure we only insert the information event once in the thread when joining a group chat. (6d13d51) * Make sure we only insert the information event once in the thread when joining a group chat. (ab4a44c) 2016-07-09 Tiago Salem Herrmann * Avoid reverting status of messages (7b55229) * Avoid reverting status of messages (db5deea) 2016-07-08 Gustavo Pichorim Boiko * Update the room properties when we get the channel for it. (ebae119) * Update the room properties when we get the channel for it. (a74cfab) * As a temporary fix, insert an event telling the user he joined a group. (f9d7518) * As a temporary fix, insert an event telling the user he joined a group. (a7f2c76) * Do not wait for the DBus async calls to finish (doing processEvents() in between). It causes inconsistencies and crashes in history-daemon. Use QDBusAbstractInterface::call instead. (3e2422a) * Do not wait for the DBus async calls to finish (doing processEvents() in between). It causes inconsistencies and crashes in history-daemon. Use QDBusAbstractInterface::call instead. (9ba9441) 2016-07-06 Gustavo Pichorim Boiko * Fix returning threads with no participants. (ff11889) * Fix returning threads with no participants. (3a0f4f0) 2016-07-05 Tiago Salem Herrmann * use macros and fill pending participants list correctly (f9c069e) * use macros and fill pending participants list correctly (9bf27ce) 2016-07-05 Florian Boucault * Fix packaging for crossbuilding. (1f7609d) * Fix packaging for crossbuilding. (30c6bf0) 2016-07-04 Tiago Salem Herrmann * Rename types (03c92d9) * Rename types (6bab4a1) * add support for local and remote pending member lists (e3e64f2) * add support for local and remote pending member lists (b4f31ec) 2016-06-28 Tiago Salem Herrmann * merge trunk (3d9f2da) * merge trunk (940e333) 2016-06-27 Gustavo Pichorim Boiko * Fix tests. (884fbb8) * Fix tests. (452f50d) 2016-06-17 Gustavo Pichorim Boiko * Add option sqlite command tracing (13f1cb7) * Handle waiting of asyncronous interface properties correctly to avoid crashes. (ec3ee93) * Handle waiting of asyncronous interface properties correctly to avoid crashes. (99ee3e4) * Add missing specs. (6319544) * Add missing specs. (78e5649) * Add one missing rollback. (8ed8580) * Add one missing rollback. (0987316) * Make sure related queries are run in a transaction. (6ed1e39) * Make sure related queries are run in a transaction. (b4fb90c) * Update copyright headers (ce629a2) * Update copyright headers (90488cc) 2016-06-15 Gustavo Pichorim Boiko * Get the interfaces' properties using the class methods instead of calling dbus directly. (7ada560) * Get the interfaces' properties using the class methods instead of calling dbus directly. (c6eb7aa) 2016-05-30 Tiago Salem Herrmann * fix member change signaling (fb90c31) * fix member change signaling (610b3ad) * Force chatType for legacy method calls (9eeddd3) * Force chatType for legacy method calls (9800ffa) 2016-05-30 Gustavo Pichorim Boiko * Fix build. (06962a4) * Fix build. (8c88c8c) * When checking which threads to group, discard the ones we know should not be grouped. (494ec5b) * When checking which threads to group, discard the ones we know should not be grouped. (e587cdd) * Update copyright header (e940acf) * Update copyright header (3f4cb80) * Fix the condition for grouping multimedia accounts. (b82d76c) * Fix the condition for grouping multimedia accounts. (4dae67f) 2016-05-30 Tiago Salem Herrmann * set chatType to 1 (Contact) for existing ofono 1-1 chats. Leave mms group chats as 0 (None). (0157bee) * set chatType to 1 (Contact) for existing ofono 1-1 chats. Leave mms group chats as 0 (None). (ce62634) 2016-05-27 Gustavo Pichorim Boiko * Use another condition to determine when to group multimedia threads. (ceda08a) * Use another condition to determine when to group multimedia threads. (56f2338) * Make sure we don't group threads of chat rooms with one member (4db77b5) * Make sure we don't group threads of chat rooms with one member (9ddd682) * Fix selecting participants (5146ad8) * Fix selecting participants (00d86d7) 2016-05-26 Gustavo Pichorim Boiko * Make sure we don't return chat rooms in threadForParticipants() (86c571f) * Make sure we don't return chat rooms in threadForParticipants() (b1be399) 2016-05-23 Gustavo Pichorim Boiko * Remove messages that were superseded. (1975bef) * Remove messages that were superseded. (d007e74) 2016-05-23 Tiago Salem Herrmann * update thread participants (455c6dd) * update thread participants (d7b2b84) 2016-05-19 Tiago Salem Herrmann * do not allow inserting into chat_room_info if threadId or accountId are empty (a55a1f1) * do not allow inserting into chat_room_info if threadId or accountId are empty (43a562e) 2016-05-18 Tiago Salem Herrmann * remove chat_room_info when thread is removed (848c51e) * remove chat_room_info when thread is removed (aa838ae) * do not update room info if no accountId or threadId is provided (9872bf4) * do not update room info if no accountId or threadId is provided (d58c4b6) 2016-05-13 Gustavo Pichorim Boiko * Expose the chat type to QML. (2af7700) * Expose the chat type to QML. (28724fd) 2016-05-06 Tiago Salem Herrmann * do not group multimedia group chats (177250f) * do not group multimedia group chats (275e855) 2016-04-27 Tiago Salem Herrmann * use participantIds instead of participants (0f65c38) * use participantIds instead of participants (211d5a5) * add FieldParticipantIds (9febf1c) * add FieldParticipantIds (76f969f) 2016-04-25 Tiago Salem Herrmann * convert qvariant to qstringlist (cb66ca4) * convert qvariant to qstringlist (eb05845) 2016-04-25 CI Train Bot * Releasing 0.1+16.04.20160425-0ubuntu1 (d7fdf20) * Releasing 0.1+16.04.20160425-0ubuntu1 (c351757) 2016-04-25 Gustavo Pichorim Boiko * Make sure Telepathy types are always initialized. Fixes: #1563050 Approved by: PS Jenkins bot (07dc46b) * Make sure Telepathy types are always initialized. Fixes: #1563050 Approved by: PS Jenkins bot (3a3ad17) 2016-04-20 Tiago Salem Herrmann * Expose alias to qml (d72bbf0) * Expose alias to qml (acf6b42) 2016-04-18 Tiago Salem Herrmann * remove debug (c9849d5) * remove debug (4397a6f) * check if interfaces are available before using them (5592d55) * check if interfaces are available before using them (9c9d533) * fix expose chatRoomInfo to qml (11dc206) * fix expose chatRoomInfo to qml (0f3160e) * Add chatType to model (942c35d) * Add chatType to model (8747f7f) 2016-04-15 Tiago Salem Herrmann * Initial chat room support (ce99296) * Initial chat room support (284a42a) 2016-03-30 Gustavo Pichorim Boiko * Add unit test. (7ccc1ca) * Add unit test. (cea96c5) * Make sure Telepathy types are always initialized. (ddcdf72) * Make sure Telepathy types are always initialized. (87909ba) 2016-01-04 CI Train Bot * Releasing 0.1+16.04.20160104-0ubuntu1 (65669e8) * Releasing 0.1+16.04.20160104-0ubuntu1 (c3f2340) 2016-01-04 Tiago Salem Herrmann * Do proper dbus demarshalling of detailProperties. Fixes: #1510655 (abe2e90) * Do proper dbus demarshalling of detailProperties. Fixes: #1510655 (049827d) * do proper dbus demarshalling of detailProperties (48c654a) * do proper dbus demarshalling of detailProperties (c64598e) 2015-12-07 CI Train Bot * Releasing 0.1+16.04.20151207-0ubuntu1 (71dd011) * Releasing 0.1+16.04.20151207-0ubuntu1 (35fbc65) 2015-12-07 Tiago Salem Herrmann * Fix build with new qt on xenial-proposed. (7f3f7d5) * Fix build with new qt on xenial-proposed. (28e0d75) * Mark messages as read on onMessageRead(). Fixes: #1373059 Approved by: Gustavo Pichorim Boiko (a882aa0) * Mark messages as read on onMessageRead(). Fixes: #1373059 Approved by: Gustavo Pichorim Boiko (d44acb6) * Allow removing empty threads Fixes: #1517602 Approved by: PS Jenkins bot, Gustavo Pichorim Boiko (963c8c6) * Allow removing empty threads Fixes: #1517602 Approved by: PS Jenkins bot, Gustavo Pichorim Boiko (491e242) 2015-12-03 Tiago Salem Herrmann * fix variable name (cf421b0) * fix variable name (887901c) 2015-11-20 Tiago Salem Herrmann * merge parent branch (5e575c3) * merge parent branch (fe2ca24) * fix broken test (89aad9a) * fix broken test (86dadfa) * merge fix-1517602 branch (b962be8) * merge fix-1517602 branch (7ad47bb) * use the right token (87d4617) * use the right token (5986a32) * Mark messages as read on onMessageRead() (867423f) * Mark messages as read on onMessageRead() (56e7693) * make sure FieldNewEvent is always bool (f775e98) * make sure FieldNewEvent is always bool (6a6b7f5) 2015-11-19 Tiago Salem Herrmann * Allow removing empty threads (2ec4acd) * Allow removing empty threads (b2e6f51) 2015-11-03 CI Train Bot * Releasing 0.1+16.04.20151103-0ubuntu1 (3f3309f) * Releasing 0.1+16.04.20151103-0ubuntu1 (09c6bc6) 2015-11-03 Tiago Salem Herrmann * Enable cross compiling. Approved by: PS Jenkins bot (ea3d677) * Enable cross compiling. Approved by: PS Jenkins bot (2e83c96) * merge trunk (f4a4900) * merge trunk (18bff2d) 2015-10-20 CI Train Bot * Releasing 0.1+15.10.20151020.1-0ubuntu1 (d656105) * Releasing 0.1+15.10.20151020.1-0ubuntu1 (bcc80ab) 2015-10-20 Gustavo Pichorim Boiko * Store threads' timestamps in UTC in the cache, and convert only when returning to the client, so that it has the most up-to-date local time. Approved by: PS Jenkins bot (65e2678) * Store threads' timestamps in UTC in the cache, and convert only when returning to the client, so that it has the most up-to-date local time. Approved by: PS Jenkins bot (e4689d0) * Move the classes from historyprivate static library to libhistoryservice itself and just keep their header files private. Approved by: Tiago Salem Herrmann, PS Jenkins bot (4eaacc3) * Move the classes from historyprivate static library to libhistoryservice itself and just keep their header files private. Approved by: Tiago Salem Herrmann, PS Jenkins bot (306a0e1) * Request contact information for all known participants on history-daemon initialization, and use this cached information on the models. Approved by: Tiago Salem Herrmann, PS Jenkins bot (abc6d56) * Request contact information for all known participants on history-daemon initialization, and use this cached information on the models. Approved by: Tiago Salem Herrmann, PS Jenkins bot (77e6598) 2015-10-20 Tiago Salem Herrmann * Initial cache implementation for grouped conversations. Approved by: PS Jenkins bot, Gustavo Pichorim Boiko (d4ebdde) * Initial cache implementation for grouped conversations. Approved by: PS Jenkins bot, Gustavo Pichorim Boiko (2897a92) * - Set max count hint - Don't use fallback to tel to avoid queries to the address-book-service with null filters Approved by: PS Jenkins bot, Gustavo Pichorim Boiko (ad59781) * - Set max count hint - Don't use fallback to tel to avoid queries to the address-book-service with null filters Approved by: PS Jenkins bot, Gustavo Pichorim Boiko (ee6580c) 2015-10-20 Gustavo Pichorim Boiko * Optimize the thread matching by avoiding some phone number formatting and validating. Fixes: #1488988 Approved by: Tiago Salem Herrmann, PS Jenkins bot (5f5b407) * Optimize the thread matching by avoiding some phone number formatting and validating. Fixes: #1488988 Approved by: Tiago Salem Herrmann, PS Jenkins bot (78e8e05) * Make it possible to use the custom functions defined by the sqlite plugin in the schema files themselves. Approved by: Tiago Salem Herrmann, PS Jenkins bot (074dc63) * Make it possible to use the custom functions defined by the sqlite plugin in the schema files themselves. Approved by: Tiago Salem Herrmann, PS Jenkins bot (45d9cf7) * Store threads' timestamps in UTC in the cache, and convert only when returning to the client, so that it has the most up-to-date local time. (053db50) * Store threads' timestamps in UTC in the cache, and convert only when returning to the client, so that it has the most up-to-date local time. (36b3bf7) 2015-10-13 Gustavo Pichorim Boiko * Remove unused include statement. (57a02d5) * Remove unused include statement. (857cddb) * Merge parent. (cb0f0fa) * Merge parent. (bae4b6c) * Fix grouping events. (66c1e6f) * Fix grouping events. (b46875b) 2015-10-13 Tiago Salem Herrmann * file not needed (042f81e) * file not needed (6c49b7c) * Enable cross compiling (2b68014) * Enable cross compiling (7a1ad65) 2015-10-09 Gustavo Pichorim Boiko * Merge parent branch (c0d418e) * Merge parent branch (1b90ebe) * Add a comment explaining why the upstart configuration is activating history-daemon via dbus (de5813b) * Add a comment explaining why the upstart configuration is activating history-daemon via dbus (f2c12e0) 2015-10-08 Gustavo Pichorim Boiko * Merge parent changes (caaaf03) * Merge parent changes (06d1573) * Use QVariantMap directly. (fda004c) * Use QVariantMap directly. (6baa324) * Merge latest changes from parent. (d3667c2) * Merge latest changes from parent. (90f2215) * Merge parent branch (e88930b) * Merge parent branch (2527475) * Merge latest changes from parent branch. (bc8fda6) * Merge latest changes from parent branch. (484fe38) * Make it possible to fetch threads from QML. (98d9679) * Make it possible to fetch threads from QML. (0553c55) 2015-10-08 Tiago Salem Herrmann * replace QList by History::Threads (f5e157e) * replace QList by History::Threads (6b321f8) * change "lastEventTimestamp" by History::FieldLastEventTimestamp (1130afb) * change "lastEventTimestamp" by History::FieldLastEventTimestamp (6e61bdb) * - overload generateThreadMapKey(QString,QString) - remove unused Q_DECLARE_METATYPE (d297b9f) * - overload generateThreadMapKey(QString,QString) - remove unused Q_DECLARE_METATYPE (87c0c6b) * add fixme (a4ecf44) * add fixme (f414a36) * create History::FieldGroupingProperty and use as applicable (1adf934) * create History::FieldGroupingProperty and use as applicable (abed7f3) 2015-10-07 Gustavo Pichorim Boiko * Merge changes from parent branch. (cb39a11) * Merge changes from parent branch. (c60b864) * Merge changes from parent branch. (08e473c) * Merge changes from parent branch. (13f7419) 2015-10-07 Tiago Salem Herrmann * Use QVariantList instead of QList (144542b) * Use QVariantList instead of QList (1f1c344) * check if FieldGroupedThreads is present before iterating threads (95f0189) * check if FieldGroupedThreads is present before iterating threads (9db91fd) 2015-10-07 Gustavo Pichorim Boiko * Merge latest changes from parent branch. (bc87518) * Merge latest changes from parent branch. (2e97ef1) * Check if the map entries have a contactId before checking the value to avoid inserting null QVariant values in it. (1a12030) * Check if the map entries have a contactId before checking the value to avoid inserting null QVariant values in it. (d282e96) 2015-10-06 Gustavo Pichorim Boiko * Move ChannelObserver to History namespace. (a80d853) * Move ChannelObserver to History namespace. (be90395) * Move ContactMatcher to History namespace. (16f6ce1) * Move ContactMatcher to History namespace. (b37427a) * Move PhoneUtils to History namespace. (1fea5fa) * Move PhoneUtils to History namespace. (9236c21) * Move TelepathyHelper to the History namespace. (3028b4f) * Move TelepathyHelper to the History namespace. (0547ea0) 2015-10-05 Gustavo Pichorim Boiko * Merge changes from parent. (5b29d3f) * Merge changes from parent. (929fe41) * Merge latest changes from parent. (fb351b1) * Merge latest changes from parent. (0400b9c) 2015-10-05 Tiago Salem Herrmann * fix comment (60c1f46) * fix comment (1cbf550) * Fix empty thread removal and improve tests (9f10267) * Fix empty thread removal and improve tests (c5c3274) 2015-10-05 Gustavo Pichorim Boiko * Merge parent branch. (c56bf29) * Merge parent branch. (84c9248) * Merge changes from parent branch. (9c96f31) * Merge changes from parent branch. (196eb8b) * Add tests for participants. (cad7c7f) * Add tests for participants. (f31279b) 2015-10-05 Tiago Salem Herrmann * Add tests (2b7706c) * Add tests (a92cb74) * improve performance a bit (1c2c576) * improve performance a bit (45beb22) 2015-10-02 Gustavo Pichorim Boiko * Merge parent branch. (7c2b128) * Merge parent branch. (1d4fe57) * Test the new code added to contactmatcher. (9f35f79) * Test the new code added to contactmatcher. (0454bb5) * Merge parent branch (7dc71f1) * Merge parent branch (8eb56bc) * Re-enable tests. (a8f8705) * Re-enable tests. (86e7c1a) * Merge parent branch. (dd356b7) * Merge parent branch. (316ee68) * Remove some code that would cause a wrong contact match on new contacts added. (b585b92) * Remove some code that would cause a wrong contact match on new contacts added. (e911e07) * Merge latest changes from parent. (f829106) * Merge latest changes from parent. (5535595) * Merge latest changes from parent. (56dee99) * Merge latest changes from parent. (89f222b) 2015-10-02 Tiago Salem Herrmann * merge parent branch (a812255) * merge parent branch (29e8cdc) * merge parent branch (7ee6bee) * merge parent branch (751e72e) 2015-10-02 Gustavo Pichorim Boiko * Change the way we store the normalized ID when creating new threads. (f0342db) * Change the way we store the normalized ID when creating new threads. (a23c6b2) * Merge latest changes from parent. (31a547b) * Merge latest changes from parent. (2ee1037) * Ignore events that don't have a threadId or an eventId. (fe6c0ef) * Ignore events that don't have a threadId or an eventId. (45c0fa1) * Merge latest changes from parent. (c3c6103) * Merge latest changes from parent. (c8c9ce6) * Fix some tests. (4f21357) * Fix some tests. (311e129) 2015-10-02 Tiago Salem Herrmann * Skip empty threadIds Make a copy of conversationKey to avoid crashes (f8c3de9) * Skip empty threadIds Make a copy of conversationKey to avoid crashes (1fa9008) 2015-10-01 Gustavo Pichorim Boiko * Merge latest changes from parent branch. (05aa896) * Merge latest changes from parent branch. (dfaa1d7) * Make sure the views don't get invalid content when the service gets restarted. (a640f36) * Make sure the views don't get invalid content when the service gets restarted. (b180d08) * Merge latest changes from parent branch. (3a34e3c) * Merge latest changes from parent branch. (23c564f) * Revert the previous change, keeping the senderId role pointing to just the ID, and add one extra "sender" role to have the full contact info. (479941f) * Revert the previous change, keeping the senderId role pointing to just the ID, and add one extra "sender" role to have the full contact info. (dcba497) * Send the contact info for the sender too. (52fd319) * Send the contact info for the sender too. (e57fb59) * Merge latest changes from parent branch. (a085cb2) * Merge latest changes from parent branch. (b0c37bb) * There is no need to wait for the backend to be initialised anymore, the DBus interface is only exposed after the cache is ready. (4816c54) * There is no need to wait for the backend to be initialised anymore, the DBus interface is only exposed after the cache is ready. (39b932a) 2015-09-30 Gustavo Pichorim Boiko * Register the DBus interface only after the cache is already generated. (c6285d6) * Register the DBus interface only after the cache is already generated. (ea00747) * Return just the ID for the remote participant. We don't need the full contact info there. (5793202) * Return just the ID for the remote participant. We don't need the full contact info there. (1a9c4be) * Merge latest changes from parent branch. (6f7a05e) * Merge latest changes from parent branch. (53c9b1e) * Merge latest changes from parent branch. (39511bc) * Merge latest changes from parent branch. (c6b0d75) 2015-09-30 Tiago Salem Herrmann * update cache when events are removed as well (54ee165) * update cache when events are removed as well (7f21afb) 2015-09-30 Gustavo Pichorim Boiko * Add some debug to check cache generation time. (5de95b8) * Add some debug to check cache generation time. (771c9a2) * Fix detecting cache entries that have contact matches already. (3881fa7) * Fix detecting cache entries that have contact matches already. (dfe38c4) * Merge parent branch. (1362b0c) * Merge parent branch. (d49974c) * Merge parent branch. (ed96108) * Merge parent branch. (4f185fd) 2015-09-30 Tiago Salem Herrmann * Test all threads, as one participant might be in another as a group chat participant (1447242) * Test all threads, as one participant might be in another as a group chat participant (f35d8fc) 2015-09-30 Gustavo Pichorim Boiko * Merge latest changes from parent branch. (6dcbf39) * Merge latest changes from parent branch. (6e2111b) * Merge latest changes from parent branch. (bb226db) * Merge latest changes from parent branch. (f115cfe) 2015-09-29 Tiago Salem Herrmann * use own thread to compare when there are no grouped threads (b0d84ca) * use own thread to compare when there are no grouped threads (5e94dfa) 2015-09-29 Gustavo Pichorim Boiko * Make sure history-service gets autostarted after address-book-service. As there is the risk of it getting dbus-activated before the address-book-service is started (in case an SMS comes in when you boot the phone), make sure that the contact info cache gets regenerated when address-book-service starts/stops (6ad9d59) * Make sure history-service gets autostarted after address-book-service. As there is the risk of it getting dbus-activated before the address-book-service is started (in case an SMS comes in when you boot the phone), make sure that the contact info cache gets regenerated when address-book-service starts/stops (631cf7e) * Return the participants list correctly when creating new threads. (d7c3e75) * Return the participants list correctly when creating new threads. (4623443) * Merge latest changes from parent branch. (8feb154) * Merge latest changes from parent branch. (758fb12) * Merge more changes from parent branch. (67b33fe) * Merge more changes from parent branch. (a599ad6) * Merge latest changes from parent branch. (91a76c4) * Merge latest changes from parent branch. (209b840) 2015-09-29 Tiago Salem Herrmann * compare using normalized ids (ac1b37f) * compare using normalized ids (9a1d72b) * update cache properly when threads are removed (4c51036) * update cache properly when threads are removed (5f0582c) * remove unused method (4d1e44e) * remove unused method (fd024b4) * keep accountId and threadId in memory. (0640de4) * keep accountId and threadId in memory. (3766e81) * remove old code (8b78e0d) * remove old code (8975198) 2015-09-28 Gustavo Pichorim Boiko * Merge latest changes from parent branch. (7e9c45b) * Merge latest changes from parent branch. (4a96deb) * Change Event and derived classes to use History::Participants instead of QStringList. (0b90ccb) * Change Event and derived classes to use History::Participants instead of QStringList. (6cebe57) * Wait for the plugin to report itself as initialised before processing queries. (6b00130) * Wait for the plugin to report itself as initialised before processing queries. (2b24b51) * Re-enable the code that watches for contact changes in the models. (def1a07) * Re-enable the code that watches for contact changes in the models. (41312c7) * Make it possible to store existent contact info when asking ContactMatcher to watch for an identifier. (cc80019) * Make it possible to store existent contact info when asking ContactMatcher to watch for an identifier. (2d571b3) * Merge latest changes from parent branch. (cf50561) * Merge latest changes from parent branch. (8035cfd) * Merge latest changes from parent branch. (192afa5) * Merge latest changes from parent branch. (729bc6c) 2015-09-28 Tiago Salem Herrmann * - use cache for getSingleThread when grouped - keep reverse map to avoid comparing the entire list (3625899) * - use cache for getSingleThread when grouped - keep reverse map to avoid comparing the entire list (9ebb56e) 2015-09-28 Gustavo Pichorim Boiko * Merge latest changes from parent branch. (30e7b38) * Merge latest changes from parent branch. (619edcc) * Store all identifiers, even if they belong to the same phone number to speed up cache lookups. (4b1abf3) * Store all identifiers, even if they belong to the same phone number to speed up cache lookups. (e6c72c4) * Expose the contact detail properties. (e995aef) * Expose the contact detail properties. (fffe5ad) * Do not do phone comparisons when we know there was a match. (9cd236f) * Do not do phone comparisons when we know there was a match. (039e566) * Move the classes from historyprivate static library to libhistoryservice itself and just keep their header files private. (40847f8) * Move the classes from historyprivate static library to libhistoryservice itself and just keep their header files private. (8f0ecbe) 2015-09-25 Gustavo Pichorim Boiko * Fix building. (a0da0eb) * Fix building. (75dfe0d) * Merge latest changes from parent branch (bda2813) * Merge latest changes from parent branch (4a86a19) 2015-09-25 Tiago Salem Herrmann * Remove debugs (a464e6d) * Remove debugs (391fa0d) * remove debugs update cache properly (d93ff94) * remove debugs update cache properly (ec9561f) * use the right property (4e6b15a) * use the right property (efc990d) 2015-09-25 Gustavo Pichorim Boiko * Add missing copyright header. (6c96aab) * Add missing copyright header. (835be75) 2015-09-25 Tiago Salem Herrmann * use the same schema as participants (d8a9b80) * use the same schema as participants (11615b2) 2015-09-25 Gustavo Pichorim Boiko * Disable tests for now. (ccee1b4) * Disable tests for now. (25dd5a1) * Fix the demarshalling from QVariantList (the internal element is a variant, not the map itself). (8d85129) * Fix the demarshalling from QVariantList (the internal element is a variant, not the map itself). (77b7426) * De-marshall QDBusArguments to retrieve the participants list. (40bc1e9) * De-marshall QDBusArguments to retrieve the participants list. (181e373) * Only generate the thread cache after matching the contacts. Also remove duplicate participants from the result set before asking for contact information. (d6b5d44) * Only generate the thread cache after matching the contacts. Also remove duplicate participants from the result set before asking for contact information. (bf03ea8) 2015-09-24 Gustavo Pichorim Boiko * Merge latest changes from parent branch (c564be4) * Merge latest changes from parent branch (fb547e5) * Add a message saying the failure output is expected during schema generation. (bd058ef) * Add a message saying the failure output is expected during schema generation. (e81ab90) * If the database is up-to-date, don't try to run an empty list of statements. (bfd85b6) * If the database is up-to-date, don't try to run an empty list of statements. (ae0d782) * Merge latest changes from parent branch (e0f67e9) * Merge latest changes from parent branch (9f78876) * Generate the cache only after telepathy is ready. (2a50b69) * Generate the cache only after telepathy is ready. (adbeb0d) 2015-09-24 Tiago Salem Herrmann * try dbus cast and then normal demarshling (92ac0e5) * try dbus cast and then normal demarshling (07db693) 2015-09-23 Gustavo Pichorim Boiko * Fix some more participants usage. (a65d4b0) * Fix some more participants usage. (23c4aa4) * Merge latest changes from parent branch. (c87c4d0) * Merge latest changes from parent branch. (140c687) * Quickly hack the history model so that it always use contact data from the server for testing. (930aa0c) * Quickly hack the history model so that it always use contact data from the server for testing. (99ab7fd) 2015-09-23 Tiago Salem Herrmann * Update cache and displayed thread (d955ffd) * Update cache and displayed thread (467e111) * also fill map with last event properties (a38f189) * also fill map with last event properties (c755438) 2015-09-23 Gustavo Pichorim Boiko * Start implementing contact matching/caching on the daemon side. (7a2762e) * Start implementing contact matching/caching on the daemon side. (0af8b66) 2015-09-23 Tiago Salem Herrmann * initial refactor of the model to use cache from service side (95c579b) * initial refactor of the model to use cache from service side (a96ea4d) * convert to properties before sending to dbus (5e3f08f) * convert to properties before sending to dbus (1763d26) * update historythreadmodel to the new groupedThreads property (0f78b31) * update historythreadmodel to the new groupedThreads property (236ad95) * Make getSingleThread return grouped threads as well (7deae81) * Make getSingleThread return grouped threads as well (c3457ca) * Return Thread object instead of QVariantMap (dfaf8fe) * Return Thread object instead of QVariantMap (58d79a7) 2015-09-21 Tiago Salem Herrmann * reenable tests (a2b0b9f) * reenable tests (ae7ebce) * move code that fills cache to a separate method (ff79a50) * move code that fills cache to a separate method (cb9b52b) * Use matchFlags to decide if phone comparison is needed (0526f6c) * Use matchFlags to decide if phone comparison is needed (9a251c2) * Use matchFlags to determine grouping (24be8aa) * Use matchFlags to determine grouping (f1e7ecb) * Pass a qvariantmap instead of a bool to be more flexible (7b59c72) * Pass a qvariantmap instead of a bool to be more flexible (4794b3d) * Avoid property name collision (38c4718) * Avoid property name collision (00243a5) * add shouldGroupAccount() (f42e996) * add shouldGroupAccount() (2fa38b6) 2015-09-18 Tiago Salem Herrmann * Add groupedThreads property to model/views (df07082) * Add groupedThreads property to model/views (6612d02) 2015-09-17 Tiago Salem Herrmann * Use descending order to create the cache (a132f9c) * Use descending order to create the cache (a5ab742) * initial cache implementation for optimization (2a91fce) * initial cache implementation for optimization (a357544) 2015-09-14 Tiago Salem Herrmann * merge parent branch (8d81464) * merge parent branch (3678a05) 2015-09-10 Tiago Salem Herrmann * Set max count hint and don't use fallback to tel to avoid queries to the address-book-service with null filters (ad54198) * Set max count hint and don't use fallback to tel to avoid queries to the address-book-service with null filters (e5db4ed) 2015-09-03 Gustavo Pichorim Boiko * Try a phone number that matches brazilian format. (d64f897) * Try a phone number that matches brazilian format. (ad71b43) * Add missing copyright header. (1bbdb6d) * Add missing copyright header. (459bb71) 2015-09-02 Gustavo Pichorim Boiko * Verify that the thread returned is not null on tests. (d425832) * Verify that the thread returned is not null on tests. (5f0c9f8) * Add missing copyright. (9c0b63a) * Add missing copyright. (81d0b8c) * Adjust a test to the new behavior. (6f2e976) * Adjust a test to the new behavior. (cb2d0b5) * Fix the normalization function. (66d75c1) * Fix the normalization function. (81b36b4) * Merge schema generation fixes. (87cff0d) * Merge schema generation fixes. (ee85cd0) * Make it possible to use the custom functions defined by the sqlite plugin in the schema files themselves. (256abe0) * Make it possible to use the custom functions defined by the sqlite plugin in the schema files themselves. (a7c4bcb) 2015-08-28 Gustavo Pichorim Boiko * Avoid useless copying of stringlists. (81147e9) * Avoid useless copying of stringlists. (ebbab40) * Clear the normalizedParticipants list before repopulating it. (3179e4c) * Clear the normalizedParticipants list before repopulating it. (4cb6e7b) * If the two numbers are the same, do not forward the query to libphonenumber, just return true. (84bbd7f) * If the two numbers are the same, do not forward the query to libphonenumber, just return true. (3caebe1) 2015-08-28 CI Train Bot * Releasing 0.1+15.10.20150828.1-0ubuntu1 (d49c9de) * Releasing 0.1+15.10.20150828.1-0ubuntu1 (93d9dee) 2015-08-28 Gustavo Pichorim Boiko * Do not override the build type. Also remove a block of unused code from debian/rules. (f2a4b5e) * Do not override the build type. Also remove a block of unused code from debian/rules. (64a5b46) * Prevent the history-daemon from crashing in the event of an ended call not having any remote member. Fixes: #1458990 Approved by: PS Jenkins bot, Tiago Salem Herrmann (ac56333) * Prevent the history-daemon from crashing in the event of an ended call not having any remote member. Fixes: #1458990 Approved by: PS Jenkins bot, Tiago Salem Herrmann (406cb50) * Fix flags. Approved by: PS Jenkins bot, Tiago Salem Herrmann (fc83b4b) * Fix flags. Approved by: PS Jenkins bot, Tiago Salem Herrmann (33b94fa) 2015-08-28 Tiago Salem Herrmann * Correctly set mCanFetchMore in the HistoryGroupedThreadsModel. Approved by: PS Jenkins bot, Gustavo Pichorim Boiko (638c480) * Correctly set mCanFetchMore in the HistoryGroupedThreadsModel. Approved by: PS Jenkins bot, Gustavo Pichorim Boiko (de73a9a) * Manually add self id to participants list if needed. Fixes: #1486187 Approved by: PS Jenkins bot, Gustavo Pichorim Boiko (4317a3b) * Manually add self id to participants list if needed. Fixes: #1486187 Approved by: PS Jenkins bot, Gustavo Pichorim Boiko (b8ad820) * - Generate eventId when there is none - Set status to "Accepted" if the channel does not support delivery reports Approved by: PS Jenkins bot, Gustavo Pichorim Boiko (0a00d18) * - Generate eventId when there is none - Set status to "Accepted" if the channel does not support delivery reports Approved by: PS Jenkins bot, Gustavo Pichorim Boiko (66a8988) 2015-08-28 Gustavo Pichorim Boiko * Merge trunk. (ea697a1) * Merge trunk. (59b9e0f) * Merge trunk. (71bce3f) * Merge trunk. (c961168) 2015-08-27 Gustavo Pichorim Boiko * Store the normalized ID when creating new threads. (8076404) * Store the normalized ID when creating new threads. (deb68e3) * Update copyright headers. (a2e6d13) * Update copyright headers. (35334ac) * Optimize the thread matching by avoiding some phone number formatting and validating. (67eb40f) * Optimize the thread matching by avoiding some phone number formatting and validating. (0a2924c) * Do not override the build type. Also remove a block of unused code from debian/rules. (1c20c7e) * Do not override the build type. Also remove a block of unused code from debian/rules. (cd9ef68) 2015-08-19 CI Train Bot * Releasing 0.1+15.10.20150819.2-0ubuntu1 (fbe3156) * Releasing 0.1+15.10.20150819.2-0ubuntu1 (149db80) 2015-08-19 Gustavo Pichorim Boiko * Fix tests on vivid. (2c99308) * Fix tests on vivid. (269c2f2) 2015-08-19 Tiago Salem Herrmann * Wait telepathy to be ready before querying address-book-service. Fixes: #1485005 (ff441df) * Wait telepathy to be ready before querying address-book-service. Fixes: #1485005 (75b2cdb) 2015-08-19 Gustavo Pichorim Boiko * Tweak the phone numbers used in tests in an attempt to make the tests pass on vivid. (d9e51cc) * Tweak the phone numbers used in tests in an attempt to make the tests pass on vivid. (2882042) * Attempt to fix tests on vivid. (42f9639) * Attempt to fix tests on vivid. (2544302) 2015-08-18 Tiago Salem Herrmann * wait telepathy to be ready before querying address-book-service (7726a4b) * wait telepathy to be ready before querying address-book-service (47cb7fd) 2015-08-18 Gustavo Pichorim Boiko * Merge trunk. (3cbe907) * Merge trunk. (8c388d0) 2015-08-10 CI Train Bot * Releasing 0.1+15.10.20150810-0ubuntu1 (946413b) * Releasing 0.1+15.10.20150810-0ubuntu1 (b6b770d) 2015-08-10 Tiago Salem Herrmann * Use libphonenumber for phone number validation, normalization and comparison. Fixes: #1471545, #1473028 Approved by: PS Jenkins bot (b083390) * Use libphonenumber for phone number validation, normalization and comparison. Fixes: #1471545, #1473028 Approved by: PS Jenkins bot (dfe0cf0) 2015-07-31 Gustavo Pichorim Boiko * Add missing gcc5 rebuild changelog. (f751874) * Add missing gcc5 rebuild changelog. (295f1a9) 2015-07-31 Tiago Salem Herrmann * Use libphonenumber for comparison only for numbers with more than 6 digits (78ca050) * Use libphonenumber for comparison only for numbers with more than 6 digits (1766d28) 2015-07-17 Tiago Salem Herrmann * Provide a fallback to country code if none is provided (314af6a) * Provide a fallback to country code if none is provided (37b2f23) 2015-07-16 Tiago Salem Herrmann * merge generate-eventId branch (24f30c8) * merge generate-eventId branch (7398946) 2015-07-15 Tiago Salem Herrmann * add libphonenumber as a dependency (c3c907c) * add libphonenumber as a dependency (4030ec8) * Use libphonenumber for phone comparison and normalization (368a1c4) * Use libphonenumber for phone comparison and normalization (2c1cb93) 2015-07-13 Tiago Salem Herrmann * make method static (a0863bf) * make method static (b2d6618) * move code to a separate method (127a491) * move code to a separate method (cc2b511) * Manually add self id to participants list if needed. (253a733) * Manually add self id to participants list if needed. (6b69ccc) 2015-07-01 CI Train Bot * Releasing 0.1+15.10.20150701-0ubuntu1 (8bb8e5a) * Releasing 0.1+15.10.20150701-0ubuntu1 (658fec3) 2015-07-01 Tiago Salem Herrmann * Update to telepathy-qt 0.9.6.1. (c709189) * Update to telepathy-qt 0.9.6.1. (3408f8c) 2015-06-29 Tiago Salem Herrmann * check if the received message belongs to the self contact (83e7573) * check if the received message belongs to the self contact (000c22d) 2015-06-16 Tiago Salem Herrmann * update to telepathy-qt 0.9.6.1 (dcea3b0) * update to telepathy-qt 0.9.6.1 (9527573) 2015-05-27 Gustavo Pichorim Boiko * Prevent the history-daemon from crashing in the event of an ended call not having any remote member. (d04aa87) * Prevent the history-daemon from crashing in the event of an ended call not having any remote member. (72f31f4) 2015-05-19 Tiago Salem Herrmann * add xvfb to qml tests (dcef4ea) * add xvfb to qml tests (8de036c) 2015-05-18 Tiago Salem Herrmann * use dbus (0682389) * use dbus (561689f) * remove unused modules (1ac49df) * remove unused modules (cc69e59) * test canFetchMore() (eee8a90) * test canFetchMore() (d8e7ceb) * use isEmpty() (e41a10b) * use isEmpty() (2ab0f8d) * Correctly set mCanFetchMore in the HistoryGroupedThreadsModel (a9f8e0c) * Correctly set mCanFetchMore in the HistoryGroupedThreadsModel (79d7fe0) 2015-05-15 Gustavo Pichorim Boiko * Fix flags. (9115efd) * Fix flags. (fba8f8e) 2015-05-05 Tiago Salem Herrmann * Add tests (ed57e6c) * Add tests (ed8c696) 2015-05-01 Tiago Salem Herrmann * Generate eventId when there is none Set status to "Accepted" if the channel does not support delivery reports (9dc6cbb) * Generate eventId when there is none Set status to "Accepted" if the channel does not support delivery reports (0a4eb1c) 2015-04-28 CI Train Bot * Releasing 0.1+15.04.20150428.1-0ubuntu1 (c4172b1) * Releasing 0.1+15.04.20150428.1-0ubuntu1 (2aaad27) 2015-04-28 Gustavo Pichorim Boiko * Fix a crash that was happening when matching new contacts. Also add a unit test to make sure it doesn't happen again. Fixes: #1449662 Approved by: PS Jenkins bot (def3c29) * Fix a crash that was happening when matching new contacts. Also add a unit test to make sure it doesn't happen again. Fixes: #1449662 Approved by: PS Jenkins bot (a39824e) * Fix contact removal info clearing too. (c555c9e) * Fix contact removal info clearing too. (3fff870) * Fix a crash that was happening when matching new contacts. Also add a unit test to make sure it doesn't happen again. (aeedc34) * Fix a crash that was happening when matching new contacts. Also add a unit test to make sure it doesn't happen again. (aa8651f) 2015-04-09 CI Train Bot * Releasing 0.1+15.04.20150409-0ubuntu1 (c2ad86d) * Releasing 0.1+15.04.20150409-0ubuntu1 (8d763a6) 2015-04-09 Gustavo Pichorim Boiko * Change the tests to use the same infrastructure as the one used in telephony-service, and add some tests for the contact matching changes. Approved by: PS Jenkins bot (8544319) * Change the tests to use the same infrastructure as the one used in telephony-service, and add some tests for the contact matching changes. Approved by: PS Jenkins bot (4d16b25) * Improve contact matching by accepting other addressable fields than just phone numbers. Approved by: PS Jenkins bot (f3550c2) * Fix contactmanager creation. (84195d6) * Fix contactmanager creation. (0e80fe3) * Get the correct field for the account protocol. (064554a) 2015-04-08 Gustavo Pichorim Boiko * Fix compilation when srcdir != builddir (e543c11) * Fix compilation when srcdir != builddir (9bbd408) * Add some tests to the contact matcher. Still not much is covered. (ec094c6) * Add some tests to the contact matcher. Still not much is covered. (8aa3d74) * Fix the non-passing tests. (5d45e5d) * Fix the non-passing tests. (f59e71f) 2015-04-07 Gustavo Pichorim Boiko * Update history-service to use the same tests infrastructure as telephony-service. (913b80d) * Update history-service to use the same tests infrastructure as telephony-service. (710f765) 2015-04-01 Gustavo Pichorim Boiko * Initial round of changes to support accounts not based on phone numbers. (bc328f1) 2015-03-30 Gustavo Pichorim Boiko * Move the telepathy helper to a static library as it will now be used in multiple places. (120af81) 2015-02-10 CI Train Bot * Releasing 0.1+15.04.20150210-0ubuntu1 (b7a5cb6) 2015-02-10 Tiago Salem Herrmann * - Add support for writing events from qml. - Iterate over attachments when qdbus_cast fails. Fixes: #1417353 Approved by: PS Jenkins bot (80972cf) 2015-02-09 Tiago Salem Herrmann * check if qdbus_cast fails and try to demarshal the attachments by hand (aaf195c) 2015-02-06 CI Train Bot * Releasing 0.1+15.04.20150206.1-0ubuntu1 (d339db8) 2015-02-06 Tiago Salem Herrmann * Add tools to populate the history database. (fb5ad44) * update VoiceEvent signature (9f181da) * merge trunk (3c8930a) 2015-02-05 Tiago Salem Herrmann * add support for writing events from qml (da07fc4) 2015-01-28 CI Train Bot * Releasing 0.1+15.04.20150128-0ubuntu1 (66341ed) 2015-01-28 Gustavo Pichorim Boiko * Use QSqlQuery::bindValue() to pass filter arguments to the query to prevent errors. (ab492ad) * Store the number that was used to call a given voice event. Fixes: #1410500 Approved by: PS Jenkins bot (eeda018) * Use QSqlQuery::bindValue() to pass filter arguments to the query to prevent errors. (1b148b5) 2015-01-27 Gustavo Pichorim Boiko * Merge latest changes from trunk (12d650b) 2015-01-25 CI Train Bot * Releasing 0.1+15.04.20150125-0ubuntu1 (07c54d4) 2015-01-25 Gustavo Pichorim Boiko * Make sure string filters are correctly escaped. Approved by: PS Jenkins bot (02c4040) * Use single quotes for string literals and escape the characters properly. (7ad9fe7) 2015-01-24 Gustavo Pichorim Boiko * Also escape partial matches. (79785d6) * Make sure string filters are correctly escaped. (21c78b2) 2015-01-20 CI Train Bot * Releasing 0.1+15.04.20150120-0ubuntu1 (15102df) 2015-01-20 Gustavo Pichorim Boiko * Convert the filter to a string in the plugin itself and add support for the MatchContains match flag. Fixes: #1376793 Approved by: PS Jenkins bot (2c0a7fa) 2015-01-16 Gustavo Pichorim Boiko * Store the number that was used to call a given voice event. (fe4fad1) 2015-01-08 Gustavo Pichorim Boiko * Fix partial string matching. (fcc7021) 2015-01-07 Gustavo Pichorim Boiko * Convert the filter to a string in the plugin itself and add support for the MatchContains match flag. (fdc1f5c) 2015-01-06 CI Train Bot * Releasing 0.1+15.04.20150106-0ubuntu1 (c75879c) 2015-01-06 Gustavo Pichorim Boiko * Make it possible to delete multiple entries from QML. Fixes: #1404286 Approved by: PS Jenkins bot (482d146) 2015-01-05 Gustavo Pichorim Boiko * Make it possible to delete multiple entries from QML. (cb700cb) 2014-11-10 CI bot * Releasing 0.1+15.04.20141110-0ubuntu1 (9cb8633) 2014-11-10 Gustavo Pichorim Boiko * Remove the tools and their dependencies: they are obsolete and not useful anymore. Approved by: PS Jenkins bot (3d9daff) * Save timestamps in UTC. Fixes: 1379018 Approved by: PS Jenkins bot (867c369) 2014-10-30 Tiago Salem Herrmann * add tools to populate the history database (08116ee) * merge remove_tools branch (331bb69) 2014-10-30 Gustavo Pichorim Boiko * Re-add the tools directory, as it might be useful in the future, just remove the tpl-import tool instead. (1f8ae60) * Remove the tools and their dependencies: they are obsolete and not useful anymore. (aef0dd8) 2014-10-22 Gustavo Pichorim Boiko * Fix tests. (6874725) 2014-10-20 Gustavo Pichorim Boiko * Update the existing timestamps to UTC. (1018fb3) 2014-10-17 Gustavo Pichorim Boiko * Save new event timestamps in UTC. (78c70fe) 2014-10-09 Timo Jyrinki * Manually merge 0.1+14.10.20141003-0ubuntu1 (af51e99) 2014-10-03 CI bot * Releasing 0.1+14.10.20141003-0ubuntu1 (f1707f6) 2014-10-03 Tiago Salem Herrmann * Add support for a new message Type: MessageTypeInformation Fixes: 1356950 Approved by: Gustavo Pichorim Boiko (26685d1) 2014-09-25 Tiago Salem Herrmann * fix broken test (d3790d6) 2014-09-24 Tiago Salem Herrmann * remove unused method expose enum for MessageType to qml (aa217b1) * add method to get item by eventId (13af6fd) 2014-09-23 Tiago Salem Herrmann * convert time to string including miliseconds (dc212d1) 2014-09-22 Tiago Salem Herrmann * use toHex() to convert to string (12c04d9) * fix typo (ac1950b) * initial support for information messageType (9749211) 2014-09-12 CI bot * Releasing 0.1+14.10.20140912-0ubuntu1 (5ccf39a) 2014-09-12 Gustavo Pichorim Boiko * If the models are used from QML, update the query right after the component setup is finished. Approved by: PS Jenkins bot, Tiago Salem Herrmann (0a90ef5) * Keep the models sorted all the time, even when new events arrive. Also replace the thread grouping proxy model by a real model that groups threads. (439783a) * Add support for matching contact info in the data models. Fixes: 1362387 Approved by: Renato Araujo Oliveira Filho (d5ce278) 2014-09-12 Tiago Salem Herrmann * Skip messages with the "skip-storage" flag set Fixes: 1340255 Approved by: Gustavo Pichorim Boiko, PS Jenkins bot (d66cb8e) * Create temporary tables in memory Approved by: Gustavo Pichorim Boiko, PS Jenkins bot (3f7d519) 2014-09-12 Gustavo Pichorim Boiko * Merge parent. (5f294c8) * Revert the change to debian/control. (31ae263) * Properly delete stuff on destruction. (14174f8) * Use the predefined participants string. (f2e1d3f) * Merge parent branch. (db845c2) * Keep versioning number consistent. (7a33fc3) * Remove leftover comment line. (d7fae77) * Create the singleton using static memory to make sure it is properly destroyed on app finishing, and make its destructor private to prevent other places from removing it. (96bae1f) * Fix typo. (26290af) * Invalidate the cache when the manager changes. (8057855) 2014-09-10 Gustavo Pichorim Boiko * If the models are used from QML, update the query right after the component setup is finished. (5cc9b97) 2014-09-10 Tiago Salem Herrmann * skip messages with the "skip-storage" flag set (3c58569) 2014-09-09 Gustavo Pichorim Boiko * Remove the sort proxy model. It is not needed anymore. (e07b4be) * Turn the HistoryThreadGroupingProxyModel into a real model operating directly on top of History::Threads. (b40fb85) * Insert new events and threads sorted. (82b61b3) * Return the shared roles in the grouped events model too. (9aba416) * Move the positioning functions to the base class so that they can be used by all models. (12504ad) * Refactory the common code between the Thread and Event model into a HistoryModel base class. (f122798) * Fetch only the required data. (8be584a) 2014-09-08 Gustavo Pichorim Boiko * Break long line. (abe759d) * Add support for matching contact info in the data models. (5047436) 2014-09-03 Tiago Salem Herrmann * use memory to create temporary tables (b5d4d09) 2014-08-21 CI bot * Releasing 0.1+14.10.20140821.1-0ubuntu1 (708e0ea) 2014-08-21 Gustavo Pichorim Boiko * Optimize the history-service by delaying the event and thread views on the model until the properties are all set. Approved by: PS Jenkins bot (edad673) * Add a model that groups events that have happened in a contiguous way. Approved by: PS Jenkins bot (468f4fd) * Merge latest changes from parent. (b0df4c0) * Check the upper bound too before accessing the QList. (b61e9e7) * merge latest changes from parent. (fc7824d) 2014-08-20 Gustavo Pichorim Boiko * Keep the grouped events list sorted. (a00da43) 2014-08-18 Gustavo Pichorim Boiko * Merge latest changes from parent. (92c60e8) * Merge latest changes from trunk. (caa1449) 2014-08-16 Gustavo Pichorim Boiko * Merge latest changes from parent branch. (621fb3e) * Make it possible to group by multiple properties at the same time. (5a7424a) 2014-08-13 CI bot * Releasing 0.1+14.10.20140813-0ubuntu1 (17f39d0) 2014-08-13 Renato Araujo Oliveira Filho * Added "count" property for sortproxymodel; Added "get" function for sortproxymodel; Approved by: Gustavo Pichorim Boiko, PS Jenkins bot (0841e1b) 2014-08-13 Tiago Salem Herrmann * Export canFetchMore to qml Approved by: Gustavo Pichorim Boiko, PS Jenkins bot (4503a65) 2014-08-13 Gustavo Pichorim Boiko * Optimize the history-service by delaying the event and thread views on the model until the properties are all set. (3748a95) 2014-08-12 Gustavo Pichorim Boiko * Handle events being added/modified/removed. (fd2038f) * Add a model that displays the events grouped by a defined role (650b444) 2014-08-07 Tiago Salem Herrmann * export canFetchMore to qml (2f5df6d) 2014-08-06 Renato Araujo Oliveira Filho * Added "count" property for sortproxymodel; Added "get" function for sortproxymodel; (16bc653) 2014-07-30 CI bot * Releasing 0.1+14.10.20140730-0ubuntu1 (982858c) 2014-07-30 Tiago Salem Herrmann * Fix unreadCount update. Approved by: Gustavo Pichorim Boiko, PS Jenkins bot (755dda7) * also update voice events (e03981d) 2014-07-29 Tiago Salem Herrmann * fix unreadCount update in triggers (3ad66a3) 2014-07-25 CI bot * Releasing 0.1+14.10.20140725.1-0ubuntu1 (667b781) 2014-07-25 Tiago Salem Herrmann * - fix attachments on thread queries - don't query the database for null filters. Apps should use an empty filter if they want to query the whole database - avoid performing queries when the filter is null (7e5ec40) * dont try to access the view if it is null (db7d717) * remove debug (e5e0667) * fix attachments on thread queries (babd3a7) * don't query the database for null filters. apps should use an empty filter if they wish to query the whole database (0e3b6d7) * avoid performing queries when the filter is null (d03db5e) 2014-07-24 CI bot * Releasing 0.1+14.10.20140724-0ubuntu1 (fd4b274) 2014-07-24 Tiago Salem Herrmann * Multiple fixes to HistoryThreadGroupingProxyModel: - Update grouping also on onDataChanged() - use rowsAboutToBeRemoved() instead of rowsRemoved() to avoid crashes - Do not emit dataChanged for removed indexes. - Set participants on groupForEntry() so phone comparison works Approved by: Gustavo Pichorim Boiko, PS Jenkins bot (7ca44c0) 2014-07-22 Tiago Salem Herrmann * remove debug (69e171d) * do not setSourceModel() (7f3f7f7) * more fixes (65bad5d) * use rowsAboutToBeRemoved() instead of rowsRemoved() (c4679f4) 2014-07-21 Tiago Salem Herrmann * more fixes (8563f59) * remove unused code (c727643) * use value from properties instead of method argument (96ed7a9) 2014-07-18 Tiago Salem Herrmann * set participants or phone comparison will fail (4806e28) * update grouping also on onDataChanged(), and the text event may not be received at the same time as the thread (9f91b2c) 2014-07-17 CI bot * Releasing 0.1+14.10.20140717.1-0ubuntu1 (06bb13d) 2014-07-17 Gustavo Pichorim Boiko * Add a proxy model to allow grouping different threads into one. Approved by: PS Jenkins bot, Tiago Salem Herrmann (df5774b) 2014-07-15 Gustavo Pichorim Boiko * Expose the threads to QML. (4deeb4d) 2014-07-14 Gustavo Pichorim Boiko * Add missing include. (8124297) * Add a proxy model to group threads from different accounts. (064defb) 2014-07-01 CI bot * Releasing 0.1+14.10.20140701-0ubuntu1 (bb7922e) 2014-07-01 Tiago Salem Herrmann * Implement MMS sending support. (e0b4b1b) 2014-06-27 CI bot * Releasing 0.1+14.10.20140627-0ubuntu1 (54ebbe3) 2014-06-27 Renato Araujo Oliveira Filho * Export HistoryEventModel.[canFetchMore | fetchMore] to QML (eab5bea) 2014-06-25 Tiago Salem Herrmann * fix typo (0bbbaa8) 2014-06-24 Tiago Salem Herrmann * merge trunk (764bef8) 2014-06-23 Renato Araujo Oliveira Filho * Export HistoryEventModel.[canFetchMore | fetchMore] to QML (8ec6553) 2014-06-19 CI bot * Releasing 0.1+14.10.20140619-0ubuntu1 (3dea3eb) 2014-06-19 Renato Araujo Oliveira Filho * Export property count and get to QML. (dddcbbd) 2014-06-19 Ken VanDine * Added count and at functions to HistoryEventModel to simplify getting single events from the model. (8ba7138) * Renamed the at method to get and forward signals for countChanged (f2de32e) * simplied the "at" method as well as the countChanged signal (d22d408) 2014-06-18 Renato Araujo Oliveira Filho * use mThreads[row].properties(); into the get property. (727dd9b) * return a QVariantMap with all thread data for "get" function. (5091bcf) * Export property count and get to QML. (c457d5b) 2014-06-10 Ken VanDine * Added count and at functions to HistoryEventModel (7c54115) 2014-05-23 Tiago Salem Herrmann * add support for sending mms's (845a836) 2014-05-22 CI bot * Releasing 0.1+14.10.20140522-0ubuntu1 (b8c25a4) 2014-05-22 Tiago Salem Herrmann * - add support for removing event attachments - fix dbus demarshaling for attachments so they don't get deleted when the event is marked as read. (1e2bf28) * save attachments on outgoing messages (4c9396c) 2014-05-05 Tiago Salem Herrmann * add support for removing event sttachments (7c7bfe1) 2014-04-23 Tiago Salem Herrmann * fix dbus demarshaling for attachments (17eca14) 2014-04-07 CI bot * Releasing 0.1+14.04.20140407-0ubuntu1 (d30b1ab) 2014-04-07 Tiago Salem Herrmann * Change observer filter to only receive non flash sms's. (c251aff) * remove filter check if the SMS interface exists manually (beba73d) 2014-04-03 Tiago Salem Herrmann * add tests back (25731f6) * merge trunk (f61fb25) 2014-03-26 CI bot * Releasing 0.1+14.04.20140326-0ubuntu1 (87e056d) 2014-03-26 Gustavo Pichorim Boiko * Delay the loading of model data until after the model properties are set. (61a6da3) 2014-03-25 Gustavo Pichorim Boiko * Delay the loading of model data until after the model properties are set. (11059b4) 2014-03-19 CI bot * Releasing 0.1+14.04.20140319-0ubuntu1 (cc8e201) 2014-03-19 Gustavo Pichorim Boiko * Make sure to only propagate events and threads of the correct type on views, even when the filter is null. (217829f) 2014-03-18 Gustavo Pichorim Boiko * Make sure to only return threads and events of the correct type when the filter is null. (ac1d56b) * Update to build with latest telepathy-qt5. (bad5b84) 2014-03-06 Tiago Salem Herrmann * fix tests only receive non flash sms's (b4085bb) 2014-03-05 CI bot * Releasing 0.1+14.04.20140305-0ubuntu1 (574525e) * No change rebuild against Qt 5.2.1. (71c59f6) 2014-02-28 CI bot * Releasing 0.1+14.04.20140228-0ubuntu1 (14464d1) 2014-02-28 Tiago Salem Herrmann * Fix broken test with qt5.2. Fixes: 1285007 (cc9e00a) 2014-02-27 Tiago Salem Herrmann * fix broken test with qt5.2 (e6c0bf3) 2014-02-17 CI bot * Releasing 0.1+14.04.20140217-0ubuntu1 (9b0ca24) 2014-02-17 Tiago Salem Herrmann * - History needs to also observe channels with TargetHandleType = 0 for group chat. - Don't iterate if the amount of participants is different (5040aaf) * fix createChannel signature (957eae7) 2014-02-10 CI bot * Releasing 0.1+14.04.20140210-0ubuntu1 (8ce7ae1) 2014-02-10 Tiago Salem Herrmann * Check if the model is already empty before removing items in order to avoid an assert() in qt 5.2. Fixes: 1275835 (0eb142f) 2014-02-04 Tiago Salem Herrmann * fix assert on qt 5.2 (4f47d5d) 2014-01-30 CI bot * Releasing 0.1+14.04.20140130-0ubuntu1 (ccc0b39) * Release history-service trunk in CITrain (c227ee2) 2014-01-30 Tiago Salem Herrmann * History needs to also observe channels with TargetHandleType = 0 for group chat. (9c0259c) 2014-01-24 Tiago Salem Herrmann * don't iterate if the amount of participants is different (082e7b4) 2014-01-21 Gustavo Pichorim Boiko * Do not create threads if they are not already there when a delivery report arrives. (a14a849) * Do not create threads if they are not already there when a delivery report arrives. (486c506) 2014-01-10 Automatic PS uploader * Releasing 0.1+14.04.20140110-0ubuntu1 (revision 125 from lp:history-service). (36d0ae4) * Releasing 0.1+14.04.20140110-0ubuntu1, based on r125 (2b91d02) 2014-01-08 Tiago Salem Herrmann * - Declare delivery report enums to be used from QML - Update all existing text message status to "accepted". (8d69b49) * - Declare delivery report enums to be used from QML - update all existing text message status to "accepted" (479d6a6) 2013-12-13 Gustavo Pichorim Boiko * Handle the delivery reports and use the data to update the message status. (8fddea3) * Enable error reporting on failed tests. (d9eafac) * Test the delivery report handling. (54d3b68) * Add tests for the Telepathy related code in history-service. (1604409) * Merge the telepathy tests. (529c222) * Add tests for calls. (e21586a) 2013-12-12 Gustavo Pichorim Boiko * Add missing include dir. (4229e38) * Add missing dependencies. (d0e153d) * Re-enable accidentally disabled tests. (aef41de) * Add tests for sending and receiving messages. (3f971e4) 2013-12-11 Gustavo Pichorim Boiko * Start implementing the mock CM to test the telepathy bits on history-service. (6879e3a) 2013-12-11 Automatic PS uploader * Releasing 0.1+14.04.20131209-0ubuntu1 (revision 120 from lp:history-service). (b4bad82) 2013-12-10 Gustavo Pichorim Boiko * Update the message status using information from the delivery reports. (ecb96f8) * Rename the messageFlags field to messageStatus to reflect its real purpose. (fefd28d) 2013-12-09 Gustavo Pichorim Boiko * Rename the field from messageFlags to messageStatus to reflect its real purpose. (61eba2a) 2013-12-09 Automatic PS uploader * Releasing 0.1+14.04.20131209-0ubuntu1, based on r120 (c87a948) 2013-12-05 Gustavo Pichorim Boiko * Add an initial set of tests for the thread and event views. (4843f15) * Add tests for the History::Manager class. (768cee0) 2013-12-04 Gustavo Pichorim Boiko * Add an initial set of tests for the thread and event views. (87d49de) 2013-12-03 Gustavo Pichorim Boiko * Test remove threads. (cbd6358) 2013-12-02 Gustavo Pichorim Boiko * Test getting a single event. (c74e410) * Update bzrignore file. (7388255) * Test events removal and fix a bug in the logic of thread removal notification. (a2d83d7) 2013-11-29 Gustavo Pichorim Boiko * Get the correct events to be modified. (46c096e) * Add more tests for the Manager class. (36743c7) * Fix notifying the threads that were modified by the writing of events. (e6603d2) 2013-11-28 Gustavo Pichorim Boiko * Remove files that are generaged automatically at build time. (3233bc7) * Really add the test file. (3782088) * Fix loading the plugin from the build dir. (7fbbd96) 2013-11-27 Gustavo Pichorim Boiko * Merge latest changes from trunk. (767b29f) * Add the infrastructure to run dbus based tests. (8e54ebe) * Make it possible to load plugins from the build dir. (ce0233b) * Add tests for the sqlite plugin. (ec02f53) 2013-11-20 Gustavo Pichorim Boiko * Add missing dependencies. (f2bad3f) 2013-11-19 Gustavo Pichorim Boiko * Add tests for the SqliteEventView class. (28ffc64) * Remove leftover debug print. (6678f51) * Really add the file. (8510e32) * Add tests for the SqliteHistoryThreadView class and fix some bugs found while writing the tests. (7c03629) 2013-11-17 Tiago Salem Herrmann * Accept non numeric id's. (f89a3bc) 2013-11-14 Gustavo Pichorim Boiko * Add the remaining tests of the plugin class. (377bcb1) 2013-11-13 Gustavo Pichorim Boiko * Test the removal of voice and text events. (f1dce1b) * Test writing voice events too. (cdd1a5e) * Test writing new text events. (5a7d7e7) * Test the query* methods. (3b46ac8) 2013-11-12 Gustavo Pichorim Boiko * Add more tests. (b8b17f6) 2013-11-11 Gustavo Pichorim Boiko * Make it possible to use a different database path, and implement the first test of the sqlite plugin. (5dbbdad) 2013-11-11 Tiago Salem Herrmann * revert back to comparePhoneNumbers() (1aad6f3) 2013-11-08 Tiago Salem Herrmann * fix copyright (cc5e936) * use the same PhoneUtils tests from telephony-service (71131b8) * use the same PhoneUtils implementation as telephony-service (184c096) 2013-11-04 Tiago Salem Herrmann * perform phone number comparison only if both senderId's are phone numbers (097ae56) 2013-11-04 Gustavo Pichorim Boiko * Fix calling History::Filter::toString() when the value is a boolean or a number. (09a1b17) 2013-11-01 Gustavo Pichorim Boiko * Use 1 and 0 for true and false values. (2b1fcd5) 2013-10-31 Gustavo Pichorim Boiko * Fix calling History::Filter::toString() when the value is a boolean or a number. (e50c1a7) 2013-10-29 Gustavo Pichorim Boiko * Add a pkgconfig file. (aff95c7) * Fix the lib name. (187d36a) * Add a pkgconfig file. (d466e33) * Pass filters in a way that they can be recreated at the service end. (ea1930a) 2013-10-25 Gustavo Pichorim Boiko * Merge latest changes from trunk. (7a461c8) * Fix the demarshalling of the filters when they travel via dbus. (bec3ceb) * Update the existing unit tests to improve the code coverage a bit. (6ca2ab0) 2013-10-24 Gustavo Pichorim Boiko * Fix calling the query methods. (7c1a400) 2013-10-23 Gustavo Pichorim Boiko * And finally, pass the filter as a QVariantMap on dbus. (9c4e69e) * Implement properties() and fromProperties() on union and intersection filters as well. (20113f3) * Add properties() and fromProperties() methods to the filter class. (78ef718) 2013-10-22 Gustavo Pichorim Boiko * Update the intersection and union filter tests. (ab9f78f) 2013-10-17 Gustavo Pichorim Boiko * Update filter tests. (1e00173) * Update thread tests. (80f688c) * Update the sort tests and rename the sort field enums to be consistent with the others. (93b3555) * Update the VoiceEvent tests. (f73cb72) * Update the text event tests. (ece0e93) 2013-10-16 Automatic PS uploader * Releasing 0.1+13.10.20131016-0ubuntu1 (revision 111 from lp:history-service). (4e09d0f) * Releasing 0.1+13.10.20131016-0ubuntu1, based on r111 (f12f215) 2013-10-15 Gustavo Pichorim Boiko * Fix the duration saving on new call entries. They were being saved as QTime, but they should be actually saved as just the duration in seconds. (6a379df) * Make sure we save the duration as seconds and not timestamp. (383606a) 2013-10-11 Automatic PS uploader * Releasing 0.1+13.10.20131011-0ubuntu1 (revision 109 from lp:history-service). (d1b01af) * Releasing 0.1+13.10.20131011-0ubuntu1, based on r109 (bfb1962) 2013-10-09 Gustavo Pichorim Boiko * Fix passing the call duration via dbus. (e354ed6) 2013-10-08 Gustavo Pichorim Boiko * Fix passing the call duration via dbus. (c2789c4) 2013-10-08 Automatic PS uploader * Releasing 0.1+13.10.20131008.1-0ubuntu1 (revision 107 from lp:history-service). (1403c78) * Releasing 0.1+13.10.20131008.1-0ubuntu1, based on r107 (1ef6b40) 2013-10-04 Gustavo Pichorim Boiko * Make sure the ThreadsModified signal is emitted with the threads up-to-date. Fixes: https://bugs.launchpad.net/bugs/1234611. (85b4f9e) 2013-10-03 Gustavo Pichorim Boiko * Make sure the ThreadsModified signal is emitted with the threads up-to-date. (81a750d) 2013-10-01 Automatic PS uploader * Releasing 0.1+13.10.20131001.4-0ubuntu1 (revision 105 from lp:history-service). (f831d75) * Releasing 0.1+13.10.20131001.4-0ubuntu1, based on r105 (27c5fc9) 2013-09-30 Gustavo Pichorim Boiko * Make sure the events emitted on signals contain the participants field. (64c9e34) * Make sure the events emitted on signals contain the participants field. (68a2eae) 2013-09-27 Automatic PS uploader * Releasing 0.1+13.10.20130927-0ubuntu1 (revision 103 from lp:history-service). (ff6ac06) * Releasing 0.1+13.10.20130927-0ubuntu1, based on r103 (91c23de) 2013-09-26 Gustavo Pichorim Boiko * Add a participants field on event to avoid having to query threads for each and every event just to discover the participants. (2ceb07e) * Add a participants field on event to avoid having to query threads for each and every event just to discover the participants. (8c25dbf) 2013-09-25 Gustavo Pichorim Boiko * Cache the threads to optimize the performance a bit when displaying events. (0692b17) * Cache the threads to optimize the performance a bit when displaying events. (cce633f) 2013-09-25 Automatic PS uploader * Releasing 0.1+13.10.20130925-0ubuntu1 (revision 100 from lp:history-service). (27790f4) * Releasing 0.1+13.10.20130925-0ubuntu1, based on r100 (d2c781a) 2013-09-23 Gustavo Pichorim Boiko * Clear the inconsistencies between thread_participants and threads on existing databases to make sure the service continues to work properly. Fixes: https://bugs.launchpad.net/bugs/1228446. (7b4f843) * Set the Recover property on the telepathy client file to make sure that if the history daemon crashes it will get restarted by telepathy mission control. Fixes: https://bugs.launchpad.net/bugs/1228446. (428dd79) * Clear the inconsistencies between thread_participants and threads on existing databases to make sure the service continues to work properly. (da7f516) * Set the Recover property on the telepathy client file to make sure that if the history daemon crashes it will get restarted by telepathy mission control. (6b22536) * Make it possible to modify events. (ca3e476) 2013-09-20 Gustavo Pichorim Boiko * Reset the timer every time a new item arrives (this helps improving the performance while scrolling the lists). (209eb40) * Return the correct value when writing text events. (6c06e38) * Make sure we don't duplicate the event on the model. (8594a67) 2013-09-19 Automatic PS uploader * Releasing 0.1+13.10.20130919.3-0ubuntu1 (revision 96 from lp:history-service). (5da2ccf) 2013-09-19 Gustavo Pichorim Boiko * Add support for modifying events. (cba9041) 2013-09-19 Automatic PS uploader * Releasing 0.1+13.10.20130919.3-0ubuntu1, based on r96 (bd2da84) 2013-09-18 Gustavo Pichorim Boiko * Do not use shared pointers for events, threads, filters and sort. They were causing too much trouble for little benefit. (9abe442) * Add some more tests to the text event attachment. (2234aec) 2013-09-18 Automatic PS uploader * Releasing 0.1+13.10.20130918.1-0ubuntu1 (revision 94 from lp:history-service). (8424ea9) 2013-09-18 Gustavo Pichorim Boiko * Do not use shared pointers for text event attachments. (01f43b5) 2013-09-18 Automatic PS uploader * Releasing 0.1+13.10.20130918.1-0ubuntu1, based on r94 (7698247) 2013-09-17 Gustavo Pichorim Boiko * Merge latest changes from trunk. (0d93568) * Use Filters instead of QList for clarity. (e8511be) * Do not use shared pointers for threads. (6b93662) * Do not use shared pointers for the events. (3e1253b) * Use dbus for all the communication between the history service and its clients. Clients no longer access the sqlite database directly. (ca9d9a7) * Do not use pointers for the sort either. (d2f1956) * Merge latest changes from trunk. (89f6f21) * Remove the use of shared pointers for filters. (58ef0c7) * Use the predefined property names in the tests too. (13d0a4d) * Fix the read timestamp reading and writing and replace some more hardcoded strings by predefined ones. (2f3da36) * Remove spurious file. (d92a3b2) 2013-09-17 Automatic PS uploader * Releasing 0.1+13.10.20130917-0ubuntu1 (revision 92 from lp:history-service). (f1d8502) 2013-09-17 Gustavo Pichorim Boiko * Do not allow for multiple instances of history-service. (8b3d999) 2013-09-17 Automatic PS uploader * Releasing 0.1+13.10.20130917-0ubuntu1, based on r92 (8864adc) 2013-09-16 Gustavo Pichorim Boiko * Fix timestamp displaying. (bd1876d) * Use the predefined strings instead of the hardcoded ones. (8fb4a41) * Implement the threadForParticipants() method in a way more consistent with the rest of the code. (a80d902) * ... and reimplement also saving the attachments. (784b672) * Reimplement the support for attachments. (5502d35) * Fix packaging. (989f13c) * Add dbus activation. (578f3ba) * Set the event types properly when writing new events from Telepathy. Thanks Tiago Salem Herrmann for the patch. (ec63f58) * Notify the threads modified. (a8bb6da) * Change the implementation of thread removal. (cf03ec3) * Merge latest changes from trunk. (f8e277d) * Implement thread removal. (412d1f1) * Connect the signal to the correct slot (e7a29be) * Call the functions from the dbus. (bc34427) * Implement the writer part of the dbus things. (5308e72) 2013-09-13 Gustavo Pichorim Boiko * Fix passing dates via DBus (patch by Tiago Salem Herrmann). (c61fa3b) * Implement the getSingle* methods. Change the code to be able to reuse some parts and avoid duplication. (ff1b627) * Remove leftover definition. (6893959) * Add missing copyright. (d349dbf) * Add support for server-side event views. (48c4fb0) 2013-09-13 Tiago Salem Herrmann * - Fix threadIdForParticipants() argument. matchFlags was not correctly being set. - Expose the "create" argument to qml so apps can create threads. (7ef48ec) 2013-09-13 Gustavo Pichorim Boiko * Update .bzrignore. (d2003ab) * Fix the thread view. (21f67b5) * Do not use shared pointers for the views. (5df4e6d) * Fix build when builddir != srcdir (48334e7) * Implement the thread view via dbus. (86a8bf6) * Merge latest changes from trunk. (3cfc560) 2013-09-13 Tiago Salem Herrmann * also update EventModel (5344326) 2013-09-13 Gustavo Pichorim Boiko * Start creating the server-side thread view. (26064c3) 2013-09-13 Tiago Salem Herrmann * expose the "create" argument to qml so apps can create threads (d383d15) * fix threadIdForParticipants() argument. matchFlags was not being correctly set. (b7d4db7) 2013-09-13 Gustavo Pichorim Boiko * Remove the Reader and Writer classes. All methods are implemented in the Plugin class directly to make it simpler. (dc4823c) * Add a missing build dependency on sqlite3 (required to generate the schema file). (71ebd7e) * Add a missing build dependency on sqlite3 (required to generate the schema file). (d6df84f) 2013-09-12 Gustavo Pichorim Boiko * Start implementing the server side of the service. (6b5e2b7) * Remove the Reader and Writer classes. All methods are implemented in the Plugin class directly to make it simpler. (adb45ee) * Implement the client side of history-service to go via dbus. (0a9c0a1) 2013-09-12 Tiago Salem Herrmann * Remove data from thread_participants when deleting threads. (e6fe174) * change 'from' to 'FROM' (f28560e) * remove schema.sql and add it to .bzrignore (fbfd4ea) 2013-09-11 Tiago Salem Herrmann * add trigger to remove items from the thread_participants table (c96f0ab) * revert previous commit (139fc6e) 2013-09-11 Gustavo Pichorim Boiko * Add more method templates. (ac79b38) 2013-09-11 Tiago Salem Herrmann * remove data from thread_participants when deleting threads (7ab5eab) 2013-09-11 Gustavo Pichorim Boiko * Start implement the communication via DBus. (27bdfcc) 2013-08-27 Automatic PS uploader * Releasing 0.1+13.10.20130827-0ubuntu1 (revision 87 from lp:history-service). (31ef7ff) * Releasing 0.1+13.10.20130827-0ubuntu1, based on r87 (5327033) 2013-08-26 Automatic PS uploader * Releasing 0.1+13.10.20130826.1-0ubuntu1 (revision 86 from lp:history-service). (db57f10) * Releasing 0.1+13.10.20130826.1-0ubuntu1, based on r86 (439db56) 2013-08-26 Timo Jyrinki * Packaging fixes. Multi-arch, remove debug package (available after archive upload), QML plugin rename and other fixes. (cb85403) * Really small fixes (2f9729c) * Add comment about team memberships to control. (16f5f55) * add upstream-name/source to debian/copyright (de84db2) * Improve package descriptions. (07582a6) * Rename QML plugin (bd60852) * bootstrap, remove debug package, add shlibs:depends (0509883) * Remove redundant Section lines (6f20a32) * wrap-and-sort -a -t (3db14b7) * Multi-arch (29b23a5) 2013-08-22 Gustavo Pichorim Boiko * Make it possible to do phone number matching for the accounts that use phone numbers as IDs. (287b63e) * Add missing dependency on sqlite3. (c641d50) 2013-08-21 Gustavo Pichorim Boiko * Make it possible to do phone number matching for the accounts that use phone numbers as IDs. (1d46bf0) * Add an invalidated() signal to the views so that they can notify their users about that. (9c7a5c5) * Add an initial set of unit tests for the classes in history-service. (cd6fb16) * Merge latest changes from trunk. (0e477ad) * Merge latest changes from trunk. (9544783) 2013-08-20 Tiago Salem Herrmann * Add initial mms support. (a771234) * fix sql upgrade script remove qml wapper instances on updateQuery() (4a3ca0c) * simplify code (c728e92) * no need to create another instance (9f5bd1f) 2013-08-20 Gustavo Pichorim Boiko * Add tests for the Sort class. (2b4cb51) 2013-08-20 Tiago Salem Herrmann * add doxygen comments to the TextEventAttachment class (982f8a4) * add new database schema (f7268e2) * merge trunk (3ea9e76) * revert schema file (ec79d02) 2013-08-20 Gustavo Pichorim Boiko * Make database upgrades easier by allowing to have sql files that update from one version to another. (f66f1ac) 2013-08-19 Gustavo Pichorim Boiko * Make sure the scripts for updating the schema work when the srcdir != builddir. (6f32f55) * Remove the IF NOT EXISTS from the schema sql files. (37c5bf0) 2013-08-19 Tiago Salem Herrmann * fix wrong column name (7da5422) 2013-08-19 Gustavo Pichorim Boiko * Add tests for the filter classes. (7916c9c) * Improve the schema upgrade. Now the schema.sql file is generated automatically based on all the version update scripts (v1.sql, v2.sql, etc). The schema version info and the resource file (historysqliteplugin.qrc) are also generated automatically to contain up-to-date information based on the contents of the plugins/sqlite/schema directory. (dfffa5b) 2013-08-16 Gustavo Pichorim Boiko * Add support for automatic updating the schema file based on all the version update scripts. (33c9b7c) 2013-08-16 Tiago Salem Herrmann * do not remove attachments. We will remove them using sql triggers (65cbab0) * use numbers instead of column names to improve performance (251487b) * add subject property to TextEvent and status to TextEventAttachment (839501b) 2013-08-15 Gustavo Pichorim Boiko * Make database upgrades easier by allowing to have sql files that update from one version to another. (6b150f3) 2013-08-15 Tiago Salem Herrmann * add qml side of mms changes (ee4343c) * fix sql query (462171f) 2013-08-14 Tiago Salem Herrmann * add mms support to sql plugin (bad9966) 2013-08-14 Gustavo Pichorim Boiko * Add tests for the Thread and ItemFactory classes. (45b9ede) 2013-08-14 Tiago Salem Herrmann * merge trunk (8e30c6a) * add initial mms support (820df45) 2013-08-14 Gustavo Pichorim Boiko * Remove leftover code. (ed86842) * Merge changes from trunk. (055eb0c) * Add tests for the Event derived classes. (679b862) * Remove empty constructor. (1d0f65e) * Add ignore rules. (e674965) * Remove some empty unused constructors. (05cef4d) * And one more. (e3d5250) * Remove some empty unused constructors. (fd9da59) 2013-08-06 Gustavo Pichorim Boiko * Add support for removing events and threads from the storage. (ee80d5c) * Fix the notification of removed threads. (d6891d7) * Add support for removing events and threads from the QML models. (223a688) * Fix the triggers executed when removing items. (09b6d4c) 2013-08-05 Gustavo Pichorim Boiko * Implement removing events and threads from the model. (0c42a17) * Add support for updating the database schema for existing files. (4756cac) * Add support for updating the database schema for existing files. (606f274) 2013-07-31 Gustavo Pichorim Boiko * Do not use cache when updating the threads. (7f30659) * Do not use cache when updating the threads. (1be6448) * Add an invalidated() signal to the views so that they can notify their users about that. (e90f0c5) * In order to avoid leaving queries opened, but still have a static result set, the views now create temporary tables with the results they use, fetch results from there, and close the query after fetching each page. (10dab99) 2013-07-30 Gustavo Pichorim Boiko * In order to avoid leaving queries opened, but still have a static result set, the views now create temporary tables with the results they use, fetch results from there, and close the query after fetching each page. (0117229) 2013-07-26 Gustavo Pichorim Boiko * Ignore filters that have empty values too. (7d0cbf0) * Ignore filters that have empty values too. (6433d65) 2013-07-25 Gustavo Pichorim Boiko * Add a "participants" role in the QML events model so that views that don't use threads can access this information. Also make sure that the cached items are used whenever possible to avoid extra database queries. (e701792) * Rename the QML plugin package to qtdeclarative5-ubuntu-history-plugin. (84fc422) * Make it possible to get the threadId for a list of participants from QML. (2cab1e4) * Add a "participants" role in the QML events model so that views that don't use threads can access this information. Also make sure that the cached items are used whenever possible to avoid extra database queries. (aae3a44) * Add a role that returns only the date for a given timestamp. (457e8d6) * Rename the QML plugin package to qtdeclarative5-ubuntu-history-plugin. (4618c15) * Add a role that returns only the date for a given timestamp. (9f42774) 2013-07-24 Gustavo Pichorim Boiko * Implement very basic sorting capabilities. (6bda5dd) * Make it possible to get the threadId for a list of participants from QML. (8459140) * Remove leftover debug print. (1095e81) * Fix the sorting of threads. (f1b6921) * Implement very basic sorting capabilities. (0e636eb) 2013-07-24 Tiago Salem Herrmann * add proxy model to sort items. (3fd36dd) * remove unused signal (1e3ee26) 2013-07-23 Tiago Salem Herrmann * remove unused methods (85597ea) * add proxy model to sort items (7b56d96) 2013-07-23 Gustavo Pichorim Boiko * Propagate changes in the history storage to all running clients via DBus. (24ec01a) * Use the currentDateTime for the sent messages. (ed3e864) * Fix the demarshalling of QDateTime parameters. (832ab74) * Ignore delivery reports, scrollback and rescued messages. (ecc536c) * Fix the property name loaded from the map. Thanks Tiago Salem for finding that. (72c7818) 2013-07-22 Gustavo Pichorim Boiko * Remove some debug and update the sample client. (87afbd2) * Notify when threads change and handle that in the model. (49306c6) * Fix the compilation. (7a93ab2) * Merge latest changes from trunk. (b31b071) * Make better use of memory by making sure there is only one instance of each event and thread in memory at any time. (088af4d) * Reduce the verbosity by removing some debug prints. (7ed9cc4) * Only append the "." when the property prefix is set. (69f9831) * Make sure there is only one existing entry in memory for each event or thread. (e09a1ea) * Add the build dir to the include directories. (3b9461b) * Add basic handling for new events in the QML plugin. (f94d7b7) * Properly emit and listen for evens on DBus (4b5bf1b) 2013-07-19 Gustavo Pichorim Boiko * Watch for events on dbus to update all clients. (e752064) * Move the write functions to the History::Manager so that it is the only single place where plugin management is required. (6e5e6f8) * And also add signals to handle updates in the EventView. (b503961) * Add support for update notification in the thread view. (1e68ee9) * Rename the Event::sender property to Event::senderId to be more consistent. Also add some signals to the view classes to handle changes in the data. (3e4b8b1) * Add a match() function that returns true if it matches a given set of properties. (7eb55bb) * Use typedef for defining the list of pointers. (70806d2) * Install the Telepathy Logger import tool. (ccc303b) 2013-07-18 Gustavo Pichorim Boiko * Add one missing file. (58d1a8b) * Install the Telepathy Logger import tool. (4ef133c) 2013-07-16 Gustavo Pichorim Boiko * Add debian packaging files. (975b9fa) 2013-07-15 Gustavo Pichorim Boiko * Remove unused modules from CMakeLists.txt. (6fb3f37) * Add copyright headers and a COPYING file. (94c7e38) * Change the Architectures to be 'any'. (91b06e3) * Add one missing copyright header. (658213c) * Add the split deb bzr rule. (9745b6c) 2013-07-12 Gustavo Pichorim Boiko * Add debian packaging files. (39cbf66) * Set the library version and soname. (e8dd561) * Set the library version and soname. (88a0927) * Add a COPYING file and copyright headers to all source files. (6e43560) 2013-07-11 Gustavo Pichorim Boiko * Add some telepathy service files to make sure the daemon is started automatically and install some extra needed files. (e1cf454) * Add one more QML sample. (c6479cc) * Set the threads as forwardOnly to save some memory. (c178ff8) * Make the output of the reader tool a little prettier. (e9c8710) * Add transaction support, and use it both for creating the database and for importing Telepathy Logger items. (817b135) * Replace some more references to item by event. (faa3efa) * Wrap all classes in a namespace, and rename some classes to better represent their purpose. (d115eb6) 2013-07-10 Gustavo Pichorim Boiko * Use pointers for the filters and sorting. (cb00ef9) * Add a tool to import entries from telepathy-logger. (91f57ca) * Fix the item query. (be138bb) * Get the last item from the threads in the same SQL query used to get the threads. (7b138e8) * Change the item retrieval to use the HistoryItemView object. (3877f9d) 2013-07-09 Gustavo Pichorim Boiko * Update the classes to use the newly created HistoryThreadView. (93e6d14) * Add the base class for the HistoryThreadView. (47f3ca8) * Make HistorySort use shared data. (cf9ad7b) * Fix the matching of boolean values in the queries (patch created by Tiago Salem Herrmann) (a67d899) * Create a model to load history items. (c9e56e4) * Remove unused class. (5982ff7) * Remove extra comma from the SQL command. (1e9044e) * Load the last item from the thread. (33c02cc) 2013-07-08 Gustavo Pichorim Boiko * Add a sample QML client. (595336c) * Add the thread related roles to the model. The item related ones are still missing. (1a4066e) * Add QML wrappers for the filter classes and start implementing the threads model and the QML plugin. (9566d40) * Add a operator= to HistoryFilter. (6b503de) * Remove implemented bit from the FIXME (1795eb0) * Use QStandardPaths::GenericDataLocation so that QML applications using qmlscene get the correct path too. (3e3cf6f) 2013-07-03 Gustavo Pichorim Boiko * Add support for paginated results in the sqlite plugin. (29e5ba6) 2013-07-02 Gustavo Pichorim Boiko * Wrap the strings in the test program using qPrintable to have a cleaner output. (34358f7) * Add the basic implementation of a union filter. (da14eba) * Add a basic implementation of the intersection filter. (4cb45eb) 2013-07-01 Gustavo Pichorim Boiko * Add basic output for items in the test program. (574b9f1) * Implement basic thread and item reading from the database. (16037e4) * Add a sample client to load content from the history. (c06e9fa) * Fix the database location. (360eddd) * Add basic skeleton for the HistoryReader. (2552b54) 2013-06-28 Gustavo Pichorim Boiko * Add the base skeleton for the HistoryManager class. (396b718) * Add the implementation for HistorySort. (5d4ac92) * Add the implementation of HistoryFilter. (968f97d) * Save the unread (newItem) status of items in the database, and create triggers to update counters and last item of threads automatically. (09c8b93) 2013-06-27 Gustavo Pichorim Boiko * Save also text messages. (103fece) * Save the duration in seconds. (ae2be1b) * Rename the library to libcommhistory and make it shared. Also implement a missing destructor. (fb910b1) * Add missing cmake modules directory. (a24be68) * Add proper code to save the voice and text items. (8479c52) * Add more tables to the schema. (1699ff1) * Add a call channel observer to watch for changes in the currently running calls. (45ee725) * Create the database structure from an external schema file. (13532e2) * Fix the SQL statements and how they are called from QSqlQuery. (a5e24a9) 2013-06-26 Gustavo Pichorim Boiko * Add some debug. (99f1f2e) 2013-06-25 Gustavo Pichorim Boiko * Add code to write the thread for new events. (50b76cd) * Add a basic SQLite plugin. The code is not complete yet though. (6870edf) * Add a class to load and manage plugins. (31a7ac5) 2013-06-20 Gustavo Pichorim Boiko * Add some more pretty headers to CMakeLists.txt (26fd76f) * Implement the HistoryThread class. (a0e906f) 2013-06-19 Gustavo Pichorim Boiko * Add missing method implementation. (3f8bc0c) * Add a sample code to write voice entries in the daemon. The code is not yet functional. (eb8312e) * Uncomment the history plugin classes. (f305edc) * Implement the VoiceItem class. (90a985d) * Implement the text item and add the missing history item implementation. (67709c3) * Implement the HistoryItem class. (ccefa9f) * Initialize TpQt types before using them, and track sent messages too. (79cffca) 2013-06-18 Gustavo Pichorim Boiko * Initial source code for the telepathy-logger replacement. (35a2a53) history-service-0.5/Lomiri/000077500000000000000000000000001455450246700157645ustar00rootroot00000000000000history-service-0.5/Lomiri/CMakeLists.txt000066400000000000000000000000321455450246700205170ustar00rootroot00000000000000add_subdirectory(History) history-service-0.5/Lomiri/History/000077500000000000000000000000001455450246700174255ustar00rootroot00000000000000history-service-0.5/Lomiri/History/CMakeLists.txt000066400000000000000000000021211455450246700221610ustar00rootroot00000000000000# QML plugin set(plugin_SRCS historyeventmodel.cpp historygroupedeventsmodel.cpp historygroupedthreadsmodel.cpp historymanager.cpp historymodel.cpp historyqmlfilter.cpp historyqmlintersectionfilter.cpp historyqmlplugin.cpp historyqmlsort.cpp historyqmltexteventattachment.cpp historyqmlunionfilter.cpp historythreadmodel.cpp ) set(plugin_HDRS historyeventmodel.h historygroupedeventsmodel.h historygroupedthreadsmodel.h historymanager.h historymodel.h historyqmlfilter.h historyqmlintersectionfilter.h historyqmlplugin.h historyqmlsort.h historyqmltexteventattachment.h historyqmlunionfilter.h historythreadmodel.h ) include_directories( ${CMAKE_SOURCE_DIR}/src ) add_library(history-qml MODULE ${plugin_SRCS} ${plugin_HDRS}) qt5_use_modules(history-qml Contacts Core Qml Quick) target_link_libraries(history-qml historyservice ) set(PLUGIN_DIR ${QT_INSTALL_QML}/Lomiri/History) install(TARGETS history-qml DESTINATION ${PLUGIN_DIR}) install(FILES qmldir DESTINATION ${PLUGIN_DIR}) history-service-0.5/Lomiri/History/historyeventmodel.cpp000066400000000000000000000354201455450246700237210ustar00rootroot00000000000000/* * Copyright (C) 2013-2017 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 "historyeventmodel.h" #include "eventview.h" #include "historyqmltexteventattachment.h" #include "manager.h" #include "contactmatcher_p.h" #include #include #include HistoryEventModel::HistoryEventModel(QObject *parent) : HistoryModel(parent), mCanFetchMore(true) { // configure the roles mRoles = HistoryModel::roleNames(); mRoles[EventIdRole] = "eventId"; mRoles[SenderIdRole] = "senderId"; mRoles[SenderRole] = "sender"; mRoles[TimestampRole] = "timestamp"; mRoles[SentTimeRole] = "sentTime"; mRoles[DateRole] = "date"; mRoles[NewEventRole] = "newEvent"; mRoles[TextMessageRole] = "textMessage"; mRoles[TextMessageTypeRole] = "textMessageType"; mRoles[TextMessageStatusRole] = "textMessageStatus"; mRoles[TextMessageAttachmentsRole] = "textMessageAttachments"; mRoles[TextReadTimestampRole] = "textReadTimestamp"; mRoles[TextReadSubjectRole] = "textSubject"; mRoles[TextInformationTypeRole] = "textInformationType"; mRoles[CallMissedRole] = "callMissed"; mRoles[CallDurationRole] = "callDuration"; mRoles[RemoteParticipantRole] = "remoteParticipant"; mRoles[SubjectAsAliasRole] = "subjectAsAlias"; connect(this, SIGNAL(countChanged()), this, SIGNAL(totalCountChanged())); } int HistoryEventModel::totalCount() const { if (mView.isNull()) { qWarning() << "component not ready"; return 0; } return mView->getTotalCount(); } int HistoryEventModel::rowCount(const QModelIndex &parent) const { if (parent.isValid()) { return 0; } return mEvents.count(); } QVariant HistoryEventModel::data(const QModelIndex &index, int role) const { if (!index.isValid() || index.row() < 0 || index.row() >= mEvents.count()) { return QVariant(); } QVariant result = eventData(mEvents[index.row()], role); if (result.isNull()) { result = HistoryModel::data(index, role); } return result; } QVariant HistoryEventModel::eventData(const History::Event &event, int role) const { History::TextEvent textEvent; History::VoiceEvent voiceEvent; switch (event.type()) { case History::EventTypeText: textEvent = event; break; case History::EventTypeVoice: voiceEvent = event; break; case History::EventTypeNull: qWarning("HistoryEventModel::eventData: Got EventTypeNull, ignoring this event!"); break; } QVariant result; switch (role) { case EventIdRole: result = event.eventId(); break; case SenderIdRole: result = History::ContactMatcher::normalizeId(event.senderId()); break; case SenderRole: if (mMatchContacts) { result = History::ContactMatcher::instance()->contactInfo(event.accountId(), event.senderId()); } else { QVariantMap map; map[History::FieldIdentifier] = event.senderId(); map[History::FieldAccountId] = event.accountId(); result = map; } break; case TimestampRole: result = event.timestamp(); break; case SentTimeRole: if (!textEvent.isNull()) { result = textEvent.sentTime(); } break; case DateRole: result = event.timestamp().date(); break; case NewEventRole: result = event.newEvent(); break; case PropertiesRole: result = event.properties(); break; case TextMessageRole: if (!textEvent.isNull()) { result = textEvent.message(); } break; case TextMessageTypeRole: if (!textEvent.isNull()) { result = (int)textEvent.messageType(); } break; case TextMessageStatusRole: if (!textEvent.isNull()) { result = (int)textEvent.messageStatus(); } break; case TextReadTimestampRole: if (!textEvent.isNull()) { result = textEvent.readTimestamp(); } break; case TextReadSubjectRole: if (!textEvent.isNull()) { result = textEvent.subject(); } break; case TextInformationTypeRole: if (!textEvent.isNull()) { result = (int)textEvent.informationType(); } break; case TextMessageAttachmentsRole: if (!textEvent.isNull()) { if (mAttachmentCache.contains(textEvent)) { result = mAttachmentCache.value(textEvent); } else { QList attachments; Q_FOREACH(const History::TextEventAttachment &attachment, textEvent.attachments()) { attachments << QVariant::fromValue(new HistoryQmlTextEventAttachment(attachment, const_cast(this))); } mAttachmentCache[textEvent] = attachments; result = attachments; } } break; case CallMissedRole: if (!voiceEvent.isNull()) { result = voiceEvent.missed(); } break; case CallDurationRole: if (!voiceEvent.isNull()) { result = voiceEvent.duration(); } break; case RemoteParticipantRole: if (!voiceEvent.isNull()) { result = History::ContactMatcher::normalizeId(voiceEvent.remoteParticipant()); } break; case SubjectAsAliasRole: if (!textEvent.isNull()) { if (mMatchContacts) { QVariantMap contactInfo = History::ContactMatcher::instance()->contactInfo(event.accountId(), textEvent.subject()); QString returnValue = contactInfo[History::FieldAlias].toString(); if (returnValue.isEmpty()) { returnValue = contactInfo[History::FieldIdentifier].toString(); } return returnValue; } return textEvent.subject(); } break; } return result; } bool HistoryEventModel::canFetchMore(const QModelIndex &parent) const { if (parent.isValid() || !mFilter || mView.isNull()) { return false; } return mCanFetchMore; } void HistoryEventModel::fetchMore(const QModelIndex &parent) { if (parent.isValid() || !mFilter || mView.isNull()) { return; } History::Events events = fetchNextPage(); if (events.isEmpty()) { mCanFetchMore = false; Q_EMIT canFetchMoreChanged(); } else { Q_FOREACH(const History::Event &event, events) { // watch for contact changes for the given identifiers Q_FOREACH(const History::Participant &participant, event.participants()) { watchContactInfo(event.accountId(), participant.identifier(), participant.properties()); } } beginInsertRows(QModelIndex(), mEvents.count(), mEvents.count() + events.count() - 1); mEvents << events; endInsertRows(); } } QHash HistoryEventModel::roleNames() const { return mRoles; } bool HistoryEventModel::removeEvents(const QVariantList &eventsProperties) { History::Events events; Q_FOREACH(const QVariant &entry, eventsProperties) { QVariantMap eventProperties = entry.toMap(); History::Event event; switch (eventProperties[History::FieldType].toInt()) { case History::EventTypeText: event = History::TextEvent::fromProperties(eventProperties); break; case History::EventTypeVoice: event = History::VoiceEvent::fromProperties(eventProperties); break; } if (!event.isNull()) { events << event; } } if (events.isEmpty()) { return false; } return History::Manager::instance()->removeEvents(events); } bool HistoryEventModel::writeEvents(const QVariantList &eventsProperties) { History::Events events; Q_FOREACH(const QVariant &entry, eventsProperties) { QVariantMap eventProperties = entry.toMap(); History::Event event; switch (eventProperties[History::FieldType].toInt()) { case History::EventTypeText: event = History::TextEvent::fromProperties(eventProperties); break; case History::EventTypeVoice: event = History::VoiceEvent::fromProperties(eventProperties); break; } if (!event.isNull()) { events << event; } } if (events.isEmpty()) { return false; } return History::Manager::instance()->writeEvents(events); } bool HistoryEventModel::removeEventAttachment(const QString &accountId, const QString &threadId, const QString &eventId, int eventType, const QString &attachmentId) { History::TextEvent textEvent; History::Event event = History::Manager::instance()->getSingleEvent((History::EventType)eventType, accountId, threadId, eventId); if (event.type() != History::EventTypeText) { qWarning() << "Trying to remove an attachment from a non text event"; return false; } QVariantMap properties = event.properties(); QList attachmentProperties = qdbus_cast >(properties[History::FieldAttachments]); QList newAttachmentProperties; int count = 0; Q_FOREACH(const QVariantMap &map, attachmentProperties) { if (map[History::FieldAttachmentId] != attachmentId) { count++; newAttachmentProperties << map; } } if (count == attachmentProperties.size()) { qWarning() << "No attachment found for id " << attachmentId; return false; } properties[History::FieldAttachments] = QVariant::fromValue(newAttachmentProperties); textEvent = History::TextEvent::fromProperties(properties); return History::Manager::instance()->writeEvents(History::Events() << textEvent); } void HistoryEventModel::updateQuery() { // remove all events from the model if (!mEvents.isEmpty()) { beginRemoveRows(QModelIndex(), 0, mEvents.count() - 1); mEvents.clear(); endRemoveRows(); } // and create the view again History::Filter queryFilter; History::Sort querySort; if (!mView.isNull()) { mView->disconnect(this); } if (mFilter && mFilter->filter().isValid()) { queryFilter = mFilter->filter(); } else { // we should not return anything if there is no filter return; } if (mSort) { querySort = mSort->sort(); } mView = History::Manager::instance()->queryEvents((History::EventType)mType, querySort, queryFilter); connect(mView.data(), SIGNAL(eventsAdded(History::Events)), SLOT(onEventsAdded(History::Events))); connect(mView.data(), SIGNAL(eventsModified(History::Events)), SLOT(onEventsModified(History::Events))); connect(mView.data(), SIGNAL(eventsRemoved(History::Events)), SLOT(onEventsRemoved(History::Events))); connect(mView.data(), SIGNAL(threadsRemoved(History::Threads)), SLOT(onThreadsRemoved(History::Threads))); connect(mView.data(), SIGNAL(invalidated()), SLOT(triggerQueryUpdate())); mCanFetchMore = true; Q_EMIT canFetchMoreChanged(); Q_FOREACH(const QVariant &attachment, mAttachmentCache) { HistoryQmlTextEventAttachment *qmlAttachment = attachment.value(); if(qmlAttachment) { qmlAttachment->deleteLater(); } } mAttachmentCache.clear(); fetchMore(QModelIndex()); } void HistoryEventModel::onEventsAdded(const History::Events &events) { if (!events.count()) { return; } Q_FOREACH(const History::Event &event, events) { // if the event is already on the model, skip it if (mEvents.contains(event)) { continue; } int pos = positionForItem(event.properties()); beginInsertRows(QModelIndex(), pos, pos); mEvents.insert(pos, event); endInsertRows(); } } void HistoryEventModel::onEventsModified(const History::Events &events) { History::Events newEvents; Q_FOREACH(const History::Event &event, events) { int pos = mEvents.indexOf(event); if (pos >= 0) { mEvents[pos] = event; QModelIndex idx = index(pos); if (event.type() == History::EventTypeText) { History::TextEvent textEvent = event; mAttachmentCache.remove(textEvent); } Q_EMIT dataChanged(idx, idx); } else { newEvents << event; } } // append the events that were not yet on the model if (!newEvents.isEmpty()) { onEventsAdded(newEvents); } } void HistoryEventModel::onEventsRemoved(const History::Events &events) { Q_FOREACH(const History::Event &event, events) { int pos = mEvents.indexOf(event); if (pos >= 0) { beginRemoveRows(QModelIndex(), pos, pos); mEvents.removeAt(pos); endRemoveRows(); } } // FIXME: there is a corner case here: if an event was not loaded yet, but was already // removed by another client, it will still show up when a new page is requested. Maybe it // should be handle internally in History::EventView? } void HistoryEventModel::onThreadsRemoved(const History::Threads &threads) { // When a thread is removed we don't get event removed signals, // so we compare and find if we have an event matching that thread. // in case we find it, we invalidate the whole view as there might be // out of date cached data on the daemon side int count = rowCount(); Q_FOREACH(const History::Thread &thread, threads) { for (int i = 0; i < count; ++i) { QModelIndex idx = index(i); if (idx.data(AccountIdRole).toString() == thread.accountId() && idx.data(ThreadIdRole).toString() == thread.threadId()) { triggerQueryUpdate(); return; } } } } History::Events HistoryEventModel::fetchNextPage() { return mView->nextPage(); } history-service-0.5/Lomiri/History/historyeventmodel.h000066400000000000000000000060101455450246700233570ustar00rootroot00000000000000/* * Copyright (C) 2013-2017 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 HISTORYEVENTMODEL_H #define HISTORYEVENTMODEL_H #include "historymodel.h" #include "textevent.h" #include "voiceevent.h" #include class HistoryEventModel : public HistoryModel { Q_OBJECT Q_PROPERTY(int totalCount READ totalCount NOTIFY totalCountChanged) Q_ENUMS(EventRole) public: enum EventRole { EventIdRole = HistoryModel::LastRole, SenderIdRole, SenderRole, TimestampRole, DateRole, NewEventRole, TextMessageRole, TextMessageTypeRole, TextMessageStatusRole, TextReadTimestampRole, TextReadSubjectRole, TextInformationTypeRole, TextMessageAttachmentsRole, CallMissedRole, CallDurationRole, RemoteParticipantRole, SubjectAsAliasRole, LastEventRole }; explicit HistoryEventModel(QObject *parent = 0); int totalCount() const; virtual int rowCount(const QModelIndex &parent = QModelIndex()) const; virtual QVariant data(const QModelIndex &index, int role) const; QVariant eventData(const History::Event &event, int role) const; Q_INVOKABLE virtual bool canFetchMore(const QModelIndex &parent = QModelIndex()) const; Q_INVOKABLE virtual void fetchMore(const QModelIndex &parent = QModelIndex()); virtual QHash roleNames() const; Q_INVOKABLE bool removeEvents(const QVariantList &eventsProperties); Q_INVOKABLE bool writeEvents(const QVariantList &eventsProperties); Q_INVOKABLE bool removeEventAttachment(const QString &accountId, const QString &threadId, const QString &eventId, int eventType, const QString &attachmentId); Q_SIGNALS: void totalCountChanged(); protected Q_SLOTS: virtual void updateQuery(); virtual void onEventsAdded(const History::Events &events); virtual void onEventsModified(const History::Events &events); virtual void onEventsRemoved(const History::Events &events); virtual void onThreadsRemoved(const History::Threads &threads); protected: History::Events fetchNextPage(); private: History::EventViewPtr mView; History::Events mEvents; bool mCanFetchMore; QHash mRoles; mutable QMap > mAttachmentCache; }; #endif // HISTORYEVENTMODEL_H history-service-0.5/Lomiri/History/historygroupedeventsmodel.cpp000066400000000000000000000230701455450246700254700ustar00rootroot00000000000000/* * Copyright (C) 2014 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 "historygroupedeventsmodel.h" #include "utils_p.h" #include "phoneutils_p.h" #include "sort.h" #include "historyqmlsort.h" #include "participant.h" HistoryGroupedEventsModel::HistoryGroupedEventsModel(QObject *parent) : HistoryEventModel(parent) { } int HistoryGroupedEventsModel::rowCount(const QModelIndex &parent) const { if (parent.isValid()) { return 0; } return mEventGroups.count(); } QVariant HistoryGroupedEventsModel::data(const QModelIndex &index, int role) const { if (!index.isValid() || index.row() < 0 || index.row() >= mEventGroups.count()) { return QVariant(); } HistoryEventGroup group = mEventGroups[index.row()]; QVariant result; QVariantList events; switch (role) { case EventsRole: Q_FOREACH(const History::Event &event, group.events) { events << event.properties(); } result = events; break; case EventCountRole: result = group.events.count(); break; default: result = eventData(group.displayedEvent, role); break; } if (result.isNull()) { // for the shared roles result = HistoryModel::data(index, role); } return result; } void HistoryGroupedEventsModel::fetchMore(const QModelIndex &parent) { if (!canFetchMore(parent)) { return; } History::Events events = fetchNextPage(); // History already deliver us the events in the right order // but we might have added new entries in the added, removed, modified events. // still, it is less expensive to do a sequential search starting from the bottom // than to do a binary search for each event, as it is very likely that the entries // belong to the bottom part of the model. Q_FOREACH(const History::Event event, events) { // watch for contact changes for the given identifiers Q_FOREACH(const History::Participant &participant, event.participants()) { watchContactInfo(event.accountId(), participant.identifier(), participant.properties()); } bool found = false; int pos = mEventGroups.count() -1; for (; pos >= 0; pos--) { HistoryEventGroup &group = mEventGroups[pos]; if (areOfSameGroup(event, group.displayedEvent)) { found = true; addEventToGroup(event, group, pos); break; } else if (isAscending() ? lessThan(group.displayedEvent.properties(), event.properties()) : lessThan(event.properties(), group.displayedEvent.properties())) { break; } } if (!found) { // the item goes into a new group right after the position found above pos++; HistoryEventGroup group; group.displayedEvent = event; group.events << event; beginInsertRows(QModelIndex(), pos, pos); mEventGroups.insert(pos, group); endInsertRows(); } } } QHash HistoryGroupedEventsModel::roleNames() const { QHash roles = HistoryEventModel::roleNames(); roles[EventsRole] = "events"; roles[EventCountRole] = "eventCount"; return roles; } void HistoryGroupedEventsModel::updateQuery() { // remove all event groups from the model if (!mEventGroups.isEmpty()) { beginRemoveRows(QModelIndex(), 0, mEventGroups.count() - 1); mEventGroups.clear(); endRemoveRows(); } // and ask HistoryEventModel to update the query and fetch items again HistoryEventModel::updateQuery(); } void HistoryGroupedEventsModel::onEventsAdded(const History::Events &events) { if (!events.count()) { return; } Q_FOREACH(const History::Event &event, events) { int pos = positionForItem(event.properties()); // check if the event belongs to the group at the position if (pos >= 0 && pos < mEventGroups.count()) { HistoryEventGroup &group = mEventGroups[pos]; if (areOfSameGroup(event, group.displayedEvent)) { addEventToGroup(event, group, pos); continue; } } // else, we just create a new group beginInsertRows(QModelIndex(), pos, pos); HistoryEventGroup group; group.displayedEvent = event; group.events << event; mEventGroups.insert(pos, group); endInsertRows(); } } void HistoryGroupedEventsModel::onEventsModified(const History::Events &events) { // FIXME: we are not yet handling events changing the property used for sorting // so for now the behavior is to find the item and check if it needs inserting or // updating in the group, which is exactly what onEventsAdded() does, so: onEventsAdded(events); } void HistoryGroupedEventsModel::onEventsRemoved(const History::Events &events) { Q_FOREACH(const History::Event &event, events) { int pos = positionForItem(event.properties()); if (pos < 0 || pos >= rowCount()) { continue; } HistoryEventGroup &group = mEventGroups[pos]; if (!group.events.contains(event)) { continue; } removeEventFromGroup(event, group, pos); } } bool HistoryGroupedEventsModel::areOfSameGroup(const History::Event &event1, const History::Event &event2) { QVariantMap props1 = event1.properties(); QVariantMap props2 = event2.properties(); Q_FOREACH(const QString &property, mGroupingProperties) { // first check if the property exists in the maps if (!props1.contains(property) || !props2.contains(property)) { return false; } // get one of the account ids to use for comparing QString accountId = props1[History::FieldAccountId].toString(); // now check if the values are the same if (property == History::FieldParticipants) { if (!History::Utils::compareParticipants(event1.participants().identifiers(), event2.participants().identifiers(), History::Utils::matchFlagsForAccount(accountId))) { return false; } } else if (props1[property] != props2[property]) { return false; } } // if it didn't fail before, the events are indeed of the same group return true; } void HistoryGroupedEventsModel::addEventToGroup(const History::Event &event, HistoryEventGroup &group, int row) { if (!group.events.contains(event)) { // insert the event in the correct position according to the sort criteria bool append = true; for (int i = 0; i < group.events.count(); ++i) { History::Event &otherEvent = group.events[i]; if (isAscending() ? lessThan(event.properties(), otherEvent.properties()) : lessThan(otherEvent.properties(), event.properties())) { group.events.insert(i, event); append = false; break; } } // if it is not above any item, just append it if (append) { group.events.append(event); } } // now check if the displayed event should be updated History::Event &firstEvent = group.events.first(); if (group.displayedEvent != firstEvent) { group.displayedEvent = firstEvent; QModelIndex idx(index(row)); Q_EMIT dataChanged(idx, idx); } } void HistoryGroupedEventsModel::removeEventFromGroup(const History::Event &event, HistoryEventGroup &group, int row) { if (group.events.contains(event)) { group.events.removeOne(event); } if (group.events.isEmpty()) { beginRemoveRows(QModelIndex(), row, row); mEventGroups.removeAt(row); endRemoveRows(); return; } if (group.displayedEvent == event) { // check what is the event that should be displayed group.displayedEvent = group.events.first(); Q_FOREACH(const History::Event &other, group.events) { if (isAscending() ? lessThan(other.properties(), group.displayedEvent.properties()) : lessThan(group.displayedEvent.properties(), other.properties())) { group.displayedEvent = other; } } } QModelIndex idx = index(row); Q_EMIT dataChanged(idx, idx); } QVariant HistoryGroupedEventsModel::get(int row) const { if (row >= rowCount() || row < 0) { return QVariant(); } return data(index(row), EventsRole); } QStringList HistoryGroupedEventsModel::groupingProperties() const { return mGroupingProperties; } void HistoryGroupedEventsModel::setGroupingProperties(const QStringList &properties) { mGroupingProperties = properties; Q_EMIT groupingPropertiesChanged(); triggerQueryUpdate(); } history-service-0.5/Lomiri/History/historygroupedeventsmodel.h000066400000000000000000000047531455450246700251440ustar00rootroot00000000000000/* * Copyright (C) 2014 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 HISTORYGROUPEDEVENTSMODEL_H #define HISTORYGROUPEDEVENTSMODEL_H #include "historyeventmodel.h" typedef struct { History::Events events; History::Event displayedEvent; } HistoryEventGroup; class HistoryGroupedEventsModel : public HistoryEventModel { Q_OBJECT Q_PROPERTY(QStringList groupingProperties READ groupingProperties WRITE setGroupingProperties NOTIFY groupingPropertiesChanged) Q_ENUMS(GroupedRole) public: enum GroupedRole { EventsRole = HistoryEventModel::LastEventRole, EventCountRole }; explicit HistoryGroupedEventsModel(QObject *parent = 0); // reimplemented from HistoryEventModel int rowCount(const QModelIndex &parent = QModelIndex()) const; QVariant data(const QModelIndex &index, int role) const; Q_INVOKABLE void fetchMore(const QModelIndex &parent = QModelIndex()); QHash roleNames() const; Q_INVOKABLE QVariant get(int row) const; QStringList groupingProperties() const; void setGroupingProperties(const QStringList &properties); Q_SIGNALS: void groupingPropertiesChanged(); protected Q_SLOTS: void updateQuery(); void onEventsAdded(const History::Events &events); void onEventsModified(const History::Events &events); void onEventsRemoved(const History::Events &events); protected: bool areOfSameGroup(const History::Event &event1, const History::Event &event2); void addEventToGroup(const History::Event &event, HistoryEventGroup &group, int row); void removeEventFromGroup(const History::Event &event, HistoryEventGroup &group, int row); private: QStringList mGroupingProperties; QList mEventGroups; }; #endif // HISTORYGROUPEDEVENTSMODEL_H history-service-0.5/Lomiri/History/historygroupedthreadsmodel.cpp000066400000000000000000000300701455450246700256140ustar00rootroot00000000000000/* * Copyright (C) 2013-2015 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 "historygroupedthreadsmodel.h" #include "utils_p.h" #include "manager.h" #include "phoneutils_p.h" #include #include #include HistoryGroupedThreadsModel::HistoryGroupedThreadsModel(QObject *parent) : HistoryThreadModel(parent) { qDBusRegisterMetaType >(); qRegisterMetaType >(); mGroupThreads = true; mRoles = HistoryThreadModel::roleNames(); mRoles[ThreadsRole] = "threads"; } QVariant HistoryGroupedThreadsModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) { return QVariant(); } const HistoryThreadGroup &group = mGroups[index.row()]; // get the data from the latest thread, and overwrite if necessary QVariant result = threadData(group.displayedThread, role); switch (role) { case HistoryThreadModel::CountRole: { int count = 0; Q_FOREACH(const History::Thread &thread, group.threads) { count += thread.count(); } result = count; break; } case HistoryThreadModel::UnreadCountRole: { int count = 0; Q_FOREACH(const History::Thread &thread, group.threads) { count += thread.unreadCount(); } result = count; break; } case ThreadsRole: { QVariantList threads; Q_FOREACH(const History::Thread &thread, group.threads) { threads << thread.properties(); } result = threads; break; } } if (result.isNull()) { // get the shared roles result = HistoryModel::data(index, role); } return result; } void HistoryGroupedThreadsModel::fetchMore(const QModelIndex &parent) { if (!canFetchMore(parent)) { return; } const History::Threads &threads = fetchNextPage(); Q_FOREACH(const History::Thread &thread, threads) { processThreadGrouping(thread); // insert the identifiers in the contact map Q_FOREACH(const History::Participant &participant, thread.participants()) { watchContactInfo(thread.accountId(), participant.identifier(), participant.properties()); } } notifyDataChanged(); if (threads.isEmpty()) { mCanFetchMore = false; Q_EMIT canFetchMoreChanged(); } } QHash HistoryGroupedThreadsModel::roleNames() const { return mRoles; } QVariant HistoryGroupedThreadsModel::get(int row) const { if (row >= rowCount() || row < 0) { return QVariant(); } return data(index(row), ThreadsRole); } int HistoryGroupedThreadsModel::existingPositionForEntry(const History::Thread &thread) const { int pos = -1; if (mGroupingProperty == History::FieldParticipants) { for (int i = 0; i < mGroups.count(); ++i) { const HistoryThreadGroup &group = mGroups[i]; Q_FOREACH(const History::Thread &groupedThread, group.threads) { History::Threads threads; // when removing threads, we cant get the grouped threads from history if (thread.groupedThreads().size() == 0) { threads << thread; } else { threads = thread.groupedThreads(); } Q_FOREACH(const History::Thread &groupedThread2, threads) { if (groupedThread == groupedThread2) { return i; } } } } return pos; } for (int i = 0; i < mGroups.count(); ++i) { const HistoryThreadGroup &group = mGroups[i]; if (thread.properties()[mGroupingProperty] == group.displayedThread.properties()[mGroupingProperty]) { pos = i; break; } } return pos; } void HistoryGroupedThreadsModel::removeGroup(const HistoryThreadGroup &group) { int pos = mGroups.indexOf(group); if (pos >= 0){ beginRemoveRows(QModelIndex(), pos, pos); mGroups.removeAt(pos); endRemoveRows(); } } void HistoryGroupedThreadsModel::updateDisplayedThread(HistoryThreadGroup &group) { int pos = mGroups.indexOf(group); if (pos < 0) { qWarning() << "Group not found!!"; return; } History::Thread displayedThread = group.threads.first(); QVariantMap displayedProperties = displayedThread.properties(); Q_FOREACH(const History::Thread &other, group.threads) { if (isAscending() ? lessThan(other.properties(), displayedProperties) : lessThan(displayedProperties, other.properties())) { displayedThread = other; displayedProperties = displayedThread.properties(); } } // check if we need to update the order int newPos = positionForItem(displayedProperties); // NOTE: only set the new displayedThread AFTER calling positionForItem group.displayedThread = displayedThread; // the positionForItem function might return the pos+1 value for the current item as it considers // this to be the position for a new insertion if (newPos != pos && newPos != pos+1) { beginMoveRows(QModelIndex(), pos, pos, QModelIndex(), newPos); // QList::move() behaves in a different way than the QAbstractItemModel moving functions // that's why the delta was added mGroups.move(pos, newPos > pos ? newPos-1 : newPos); endMoveRows(); } } History::Threads HistoryGroupedThreadsModel::restoreParticipants(const History::Threads &oldThreads, const History::Threads &newThreads) { History::Threads updated = newThreads; for(History::Thread &thread : updated) { if (!thread.participants().isEmpty()) { continue; } int i = oldThreads.indexOf(thread); if (i >=0) { thread.addParticipants(oldThreads[i].participants()); } } return updated; } void HistoryGroupedThreadsModel::updateQuery() { // remove all entries and call the query update if (!mGroups.isEmpty()) { beginRemoveRows(QModelIndex(), 0, rowCount() - 1); mGroups.clear(); endRemoveRows(); } HistoryThreadModel::updateQuery(); } void HistoryGroupedThreadsModel::onThreadsAdded(const History::Threads &threads) { Q_FOREACH(const History::Thread &thread, threads) { processThreadGrouping(thread); } fetchParticipantsIfNeeded(threads); notifyDataChanged(); } void HistoryGroupedThreadsModel::onThreadsModified(const History::Threads &threads) { Q_FOREACH(const History::Thread &thread, threads) { processThreadGrouping(thread); } fetchParticipantsIfNeeded(threads); notifyDataChanged(); } void HistoryGroupedThreadsModel::onThreadsRemoved(const History::Threads &threads) { Q_FOREACH(const History::Thread &thread, threads) { removeThreadFromGroup(thread); } notifyDataChanged(); } void HistoryGroupedThreadsModel::onThreadParticipantsChanged(const History::Thread &thread, const History::Participants &added, const History::Participants &removed, const History::Participants &modified) { int pos = existingPositionForEntry(thread); if (pos >= 0) { HistoryThreadGroup &group = mGroups[pos]; if (group.displayedThread == thread) { group.displayedThread.removeParticipants(removed); group.displayedThread.removeParticipants(modified); group.displayedThread.addParticipants(added); group.displayedThread.addParticipants(modified); } Q_FOREACH(const History::Thread &existingThread, group.threads) { if (existingThread == thread) { History::Thread modifiedThread = existingThread; group.threads.removeOne(existingThread); modifiedThread.removeParticipants(removed); modifiedThread.removeParticipants(modified); modifiedThread.addParticipants(added); modifiedThread.addParticipants(modified); group.threads.append(modifiedThread); } } QModelIndex idx = index(pos); Q_EMIT dataChanged(idx, idx); } // watch the contact info for the received participants Q_FOREACH(const History::Participant &participant, added) { watchContactInfo(thread.accountId(), participant.identifier(), participant.properties()); } Q_FOREACH(const History::Participant &participant, modified) { watchContactInfo(thread.accountId(), participant.identifier(), participant.properties()); } } void HistoryGroupedThreadsModel::processThreadGrouping(const History::Thread &thread) { QVariantMap queryProperties; queryProperties[History::FieldGroupingProperty] = mGroupingProperty; History::Thread groupedThread = History::Manager::instance()->getSingleThread((History::EventType)mType, thread.accountId(), thread.threadId(), queryProperties); if (groupedThread.properties().isEmpty()) { removeThreadFromGroup(thread); return; } int pos = existingPositionForEntry(groupedThread); // if the group is empty, we need to insert it into the map if (pos < 0) { HistoryThreadGroup group; int newPos = positionForItem(groupedThread.properties()); group.threads = groupedThread.groupedThreads(); group.displayedThread = groupedThread; beginInsertRows(QModelIndex(), newPos, newPos); mGroups.insert(newPos, group); endInsertRows(); return; } HistoryThreadGroup &group = mGroups[pos]; group.threads = restoreParticipants(group.threads, groupedThread.groupedThreads()); updateDisplayedThread(group); markGroupAsChanged(group); } void HistoryGroupedThreadsModel::removeThreadFromGroup(const History::Thread &thread) { QVariantMap properties = thread.properties(); int pos = existingPositionForEntry(thread); if (pos < 0) { qWarning() << "Could not find group for property " << properties[mGroupingProperty]; return; } HistoryThreadGroup &group = mGroups[pos]; group.threads.removeAll(thread); if (group.threads.isEmpty()) { removeGroup(group); } else { updateDisplayedThread(group); markGroupAsChanged(group); } } void HistoryGroupedThreadsModel::markGroupAsChanged(const HistoryThreadGroup &group) { if (!mChangedGroups.contains(group)) { mChangedGroups.append(group); } } void HistoryGroupedThreadsModel::notifyDataChanged() { Q_FOREACH(const HistoryThreadGroup &group, mChangedGroups) { int pos = mGroups.indexOf(group); if (pos >= 0) { QModelIndex idx = index(pos); Q_EMIT dataChanged(idx, idx); } else { qWarning() << "Group not found!"; } } mChangedGroups.clear(); } QString HistoryGroupedThreadsModel::groupingProperty() const { return mGroupingProperty; } void HistoryGroupedThreadsModel::setGroupingProperty(const QString &value) { mGroupingProperty = value; Q_EMIT groupingPropertyChanged(); triggerQueryUpdate(); } int HistoryGroupedThreadsModel::rowCount(const QModelIndex &parent) const { if (parent.isValid()) { return 0; } return mGroups.count(); } bool HistoryThreadGroup::operator==(const HistoryThreadGroup &other) const { return displayedThread == other.displayedThread; } history-service-0.5/Lomiri/History/historygroupedthreadsmodel.h000066400000000000000000000063151455450246700252660ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 HISTORYGROUPEDTHREADSMODEL_H #define HISTORYGROUPEDTHREADSMODEL_H #include "historythreadmodel.h" #include class HistoryThreadGroup { public: History::Thread displayedThread; History::Threads threads; bool operator==(const HistoryThreadGroup &other) const; }; typedef QList HistoryThreadGroupList; class HistoryGroupedThreadsModel : public HistoryThreadModel { Q_OBJECT Q_PROPERTY(QString groupingProperty READ groupingProperty WRITE setGroupingProperty NOTIFY groupingPropertyChanged) Q_ENUMS(CustomRoles) public: enum CustomRoles { ThreadsRole = LastThreadRole }; explicit HistoryGroupedThreadsModel(QObject *parent = 0); QString groupingProperty() const; void setGroupingProperty(const QString &value); virtual int rowCount(const QModelIndex &parent = QModelIndex()) const; virtual QVariant data(const QModelIndex &index, int role) const; Q_INVOKABLE void fetchMore(const QModelIndex &parent = QModelIndex()); virtual QHash roleNames() const; Q_INVOKABLE QVariant get(int row) const; Q_SIGNALS: void groupingPropertyChanged(); protected: int existingPositionForEntry(const History::Thread &thread) const; void removeGroup(const HistoryThreadGroup &group); void updateDisplayedThread(HistoryThreadGroup &group); History::Threads restoreParticipants(const History::Threads &oldThreads, const History::Threads &newThreads); protected Q_SLOTS: virtual void updateQuery(); virtual void onThreadsAdded(const History::Threads &threads); virtual void onThreadsModified(const History::Threads &threads); virtual void onThreadsRemoved(const History::Threads &threads); void onThreadParticipantsChanged(const History::Thread &thread, const History::Participants &added, const History::Participants &removed, const History::Participants &modified) override; private Q_SLOTS: void processThreadGrouping(const History::Thread &thread); void removeThreadFromGroup(const History::Thread &thread); void markGroupAsChanged(const HistoryThreadGroup &group); void notifyDataChanged(); private: QString mGroupingProperty; HistoryThreadGroupList mGroups; QList mChangedGroups; QHash mRoles; }; #endif // HISTORYGROUPEDTHREADSMODEL_H history-service-0.5/Lomiri/History/historymanager.cpp000066400000000000000000000060331455450246700231670ustar00rootroot00000000000000/* * Copyright (C) 2021 UBports Foundation * * Authors: * Lionel Duboeuf * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 "historymanager.h" #include "event.h" #include "manager.h" #include "types.h" #include #include HistoryManager::HistoryManager(QObject *parent) : QObject(parent), mPendingOperation(false) { } void HistoryManager::removeEvents(int eventType, const QString &maxDate, const QJSValue &callback) { if (!callback.isCallable()) { qCritical() << "no callback found!"; return; } QJSValue result(callback); if (mPendingOperation) { result.call(QJSValueList { 0, OperationError::OPERATION_ALREADY_PENDING }); qWarning() << "there is a pending operation, request cancelled"; return; } QDateTime fromDate = QDateTime::fromString(maxDate, Qt::ISODate); History::EventType type = (History::EventType) eventType; if (type == History::EventTypeNull|| !fromDate.isValid()) { result.call(QJSValueList { 0, OperationError::OPERATION_INVALID }); qWarning() << "invalid type or date, request cancelled"; return; } History::Filter queryFilter(History::FieldTimestamp, QVariant(maxDate), History::MatchLess); if (!queryFilter.isValid()) { result.call(QJSValueList { 0, OperationError::OPERATION_INVALID }); qWarning() << "invalid filter, operation cancelled"; return; } auto onCompleted = [this, callback](int removedCount, bool isError) { QJSValue result(callback); OperationError error = isError ? OperationError::OPERATION_FAILED : OperationError::NO_ERROR; result.call(QJSValueList { removedCount, error }); }; History::Manager::instance()->removeEvents(type, queryFilter, onCompleted); } int HistoryManager::getEventsCount(int eventType, const QString &maxDate) { QDateTime fromDate = QDateTime::fromString(maxDate, Qt::ISODate); History::EventType type = (History::EventType) eventType; if (type == History::EventTypeNull|| !fromDate.isValid()) { qWarning() << "invalid type or date, request cancelled"; return -1; } History::Filter queryFilter(History::FieldTimestamp, QVariant(maxDate), History::MatchLess); if (!queryFilter.isValid()) { qWarning() << "invalid filter, request cancelled"; return -1; } return History::Manager::instance()->getEventsCount(type, queryFilter); } history-service-0.5/Lomiri/History/historymanager.h000066400000000000000000000040451455450246700226350ustar00rootroot00000000000000/* * Copyright (C) 2021 UBports Foundation * * Authors: * Lionel Duboeuf * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 HISTORYMANAGER_H #define HISTORYMANAGER_H #include #include #include "historymodel.h" class HistoryManager : public QObject { Q_OBJECT public: enum OperationError { NO_ERROR, OPERATION_ALREADY_PENDING, OPERATION_INVALID, OPERATION_FAILED, OPERATION_TIMEOUT }; Q_ENUM(OperationError) explicit HistoryManager(QObject *parent = 0); /*! * \brief removeEvents remove all events given eventType and created before maxDate, * \param eventType event type according to History.EventType * \param maxDate QString date in ISO format * \param callback expect a javascript function(deletedEventsCount, error) * deletedEventsCount number of deleted events * error HistoryManager::OperationError enum type, */ Q_INVOKABLE void removeEvents(int eventType, const QString &maxDate, const QJSValue &callback); /*! * \brief getEventsCount return the number of events given eventType and created before maxDate * \param eventType event type according to History.EventType * \param maxDate QString date in ISO format * \return number of events count */ Q_INVOKABLE int getEventsCount(int eventType, const QString &maxDate); private: bool mPendingOperation; }; #endif // HISTORYMANAGER_H history-service-0.5/Lomiri/History/historymodel.cpp000066400000000000000000000523221455450246700226570ustar00rootroot00000000000000/* * Copyright (C) 2013-2016 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 "historymodel.h" #include "historyqmlfilter.h" #include "historyqmlsort.h" #include "contactmatcher_p.h" #include "phoneutils_p.h" #include "thread.h" #include "textevent.h" #include "manager.h" #include "utils_p.h" #include "voiceevent.h" #include #include #include HistoryModel::HistoryModel(QObject *parent) : QAbstractListModel(parent), mFilter(0), mSort(new HistoryQmlSort(this)), mType(EventTypeText), mMatchContacts(false), mUpdateTimer(0), mEventWritingTimer(0), mThreadWritingTimer(0), mWaitingForQml(false) { // configure the roles mRoles[AccountIdRole] = "accountId"; mRoles[ThreadIdRole] = "threadId"; mRoles[ParticipantsRole] = "participants"; mRoles[ParticipantsRemotePendingRole] = "remotePendingParticipants"; mRoles[ParticipantsLocalPendingRole] = "localPendingParticipants"; mRoles[TypeRole] = "type"; mRoles[TimestampRole] = "timestamp"; mRoles[SentTimeRole] = "sentTime"; mRoles[PropertiesRole] = "properties"; connect(this, SIGNAL(rowsInserted(QModelIndex,int,int)), this, SIGNAL(countChanged())); connect(this, SIGNAL(rowsRemoved(QModelIndex,int,int)), this, SIGNAL(countChanged())); connect(this, SIGNAL(modelReset()), this, SIGNAL(countChanged())); // reset the view when the service is stopped or started connect(History::Manager::instance(), SIGNAL(serviceRunningChanged()), this, SLOT(triggerQueryUpdate())); // create the view and get some objects triggerQueryUpdate(); } bool HistoryModel::canFetchMore(const QModelIndex& /* parent */) const { return false; } void HistoryModel::fetchMore(const QModelIndex &parent) { Q_UNUSED(parent) // do nothing, just make the method invokable } QHash HistoryModel::roleNames() const { return mRoles; } QVariant HistoryModel::data(const QModelIndex &index, int role) const { if (!index.isValid() || index.row() < 0 || index.row() >= rowCount()) { return QVariant(); } QVariantMap properties = index.data(PropertiesRole).toMap(); QVariant result; switch (role) { case AccountIdRole: result = properties[History::FieldAccountId]; break; case ThreadIdRole: result = properties[History::FieldThreadId]; break; case TypeRole: result = properties[History::FieldType]; break; case ParticipantsRole: { History::Participants participants = History::Participants::fromVariantList(properties[History::FieldParticipants].toList()) .filterByState(History::ParticipantStateRegular); if (mMatchContacts) { QVariantList finalParticipantsList; QVariantList participantsInfo = History::ContactMatcher::instance()->contactInfo(properties[History::FieldAccountId].toString(), participants.identifiers()); for (int i = 0; i < participantsInfo.count(); ++i) { QVariantMap newMap = participantsInfo[i].toMap(); History::Participant participant = participants[i]; newMap[History::FieldParticipantState] = participant.state(); newMap[History::FieldParticipantRoles] = participant.roles(); finalParticipantsList << newMap; } result = finalParticipantsList; } else { //FIXME: handle contact changes result = participants.identifiers(); } break; } case ParticipantsRemotePendingRole: { History::Participants participants = History::Participants::fromVariantList(properties[History::FieldParticipants].toList()) .filterByState(History::ParticipantStateRemotePending); if (mMatchContacts) { QVariantList finalParticipantsList; QVariantList participantsInfo = History::ContactMatcher::instance()->contactInfo(properties[History::FieldAccountId].toString(), participants.identifiers()); int count = 0; Q_FOREACH(const QVariant &participantInfo, participantsInfo) { QVariantMap newMap = participantInfo.toMap(); newMap[History::FieldParticipantState] = participants.at(count).state(); newMap[History::FieldParticipantRoles] = participants.at(count++).roles(); finalParticipantsList << newMap; } result = finalParticipantsList; } else { //FIXME: handle contact changes result = participants.identifiers(); } break; } case ParticipantsLocalPendingRole: { History::Participants participants = History::Participants::fromVariantList(properties[History::FieldParticipants].toList()) .filterByState(History::ParticipantStateLocalPending); if (mMatchContacts) { QVariantList finalParticipantsList; QVariantList participantsInfo = History::ContactMatcher::instance()->contactInfo(properties[History::FieldAccountId].toString(), participants.identifiers()); int count = 0; Q_FOREACH(const QVariant &participantInfo, participantsInfo) { QVariantMap newMap = participantInfo.toMap(); newMap[History::FieldParticipantState] = participants.at(count).state(); newMap[History::FieldParticipantRoles] = participants.at(count++).roles(); finalParticipantsList << newMap; } result = finalParticipantsList; } else { //FIXME: handle contact changes result = participants.identifiers(); } break; } case ParticipantIdsRole: result = History::Participants::fromVariantList(properties[History::FieldParticipants].toList()).identifiers(); break; case TimestampRole: result = QDateTime::fromString(properties[History::FieldTimestamp].toString(), Qt::ISODate); break; case SentTimeRole: result = QDateTime::fromString(properties[History::FieldSentTime].toString(), Qt::ISODate); break; } return result; } HistoryQmlFilter *HistoryModel::filter() const { return mFilter; } void HistoryModel::setFilter(HistoryQmlFilter *value) { if (mFilter) { mFilter->disconnect(this); } mFilter = value; if (mFilter) { connect(mFilter, SIGNAL(filterChanged()), SLOT(triggerQueryUpdate())); } Q_EMIT filterChanged(); triggerQueryUpdate(); } HistoryQmlSort *HistoryModel::sort() const { return mSort; } void HistoryModel::setSort(HistoryQmlSort *value) { // disconnect the previous sort if (mSort) { mSort->disconnect(this); } mSort = value; if (mSort) { connect(mSort, SIGNAL(sortChanged()), SLOT(triggerQueryUpdate())); } Q_EMIT sortChanged(); triggerQueryUpdate(); } HistoryModel::EventType HistoryModel::type() const { return mType; } void HistoryModel::setType(EventType value) { mType = value; Q_EMIT typeChanged(); triggerQueryUpdate(); } bool HistoryModel::matchContacts() const { return mMatchContacts; } void HistoryModel::setMatchContacts(bool value) { if (mMatchContacts == value) { return; } mMatchContacts = value; Q_EMIT matchContactsChanged(); if (mMatchContacts) { connect(History::ContactMatcher::instance(), SIGNAL(contactInfoChanged(QString,QString,QVariantMap)), SLOT(onContactInfoChanged(QString,QString,QVariantMap))); } else { History::ContactMatcher::instance()->disconnect(this); } // mark all indexes as changed if (rowCount() > 0) { Q_EMIT dataChanged(index(0), index(rowCount()-1)); } } QVariantMap HistoryModel::threadForProperties(const QString &accountId, int eventType, const QVariantMap &properties, int matchFlags, bool create) { QVariantMap newProperties = properties; if (properties.isEmpty()) { return QVariantMap(); } if (newProperties.contains(History::FieldParticipantIds)) { newProperties[History::FieldParticipantIds] = newProperties[History::FieldParticipantIds].toStringList(); } History::Thread thread = History::Manager::instance()->threadForProperties(accountId, (History::EventType)eventType, newProperties, (History::MatchFlags)matchFlags, create); if (!thread.isNull()) { return thread.properties(); } return QVariantMap(); } QString HistoryModel::threadIdForProperties(const QString &accountId, int eventType, const QVariantMap &properties, int matchFlags, bool create) { QVariantMap newProperties = properties; if (properties.isEmpty()) { return QString(); } if (newProperties.contains(History::FieldParticipantIds)) { newProperties[History::FieldParticipantIds] = newProperties[History::FieldParticipantIds].toStringList(); } History::Thread thread = History::Manager::instance()->threadForProperties(accountId, (History::EventType)eventType, newProperties, (History::MatchFlags)matchFlags, create); if (!thread.isNull()) { return thread.threadId(); } return QString(); } QVariantMap HistoryModel::threadForParticipants(const QString &accountId, int eventType, const QStringList &participants, int matchFlags, bool create) { if (participants.isEmpty()) { return QVariantMap(); } QVariantMap properties; properties[History::FieldParticipantIds] = participants; History::Thread thread = History::Manager::instance()->threadForProperties(accountId, (History::EventType)eventType, properties, (History::MatchFlags)matchFlags, create); if (!thread.isNull()) { return thread.properties(); } return QVariantMap(); } QString HistoryModel::threadIdForParticipants(const QString &accountId, int eventType, const QStringList &participants, int matchFlags, bool create) { if (participants.isEmpty()) { return QString(); } QVariantMap properties; properties[History::FieldParticipantIds] = participants; History::Thread thread = History::Manager::instance()->threadForProperties(accountId, (History::EventType)eventType, properties, (History::MatchFlags)matchFlags, create); if (!thread.isNull()) { return thread.threadId(); } return QString(); } void HistoryModel::requestThreadParticipants(const QVariantList &threads) { History::Threads theThreads; Q_FOREACH(const QVariant &threadVariant, threads) { History::Thread theThread = History::Thread::fromProperties(threadVariant.toMap()); // if the given thread already has the list of participants, there is no point // in fetching it again if (theThread.participants().isEmpty()) { theThreads << theThread; } } History::Manager::instance()->requestThreadParticipants(theThreads); } bool HistoryModel::writeTextInformationEvent(const QString &accountId, const QString &threadId, const QStringList &participants, const QString &message, int informationType, const QString &subject) { if (participants.isEmpty() || threadId.isEmpty() || accountId.isEmpty()) { return false; } History::TextEvent historyEvent = History::TextEvent(accountId, threadId, QString(QCryptographicHash::hash(QByteArray( QDateTime::currentDateTimeUtc().toString("yyyyMMddhhmmsszzz").toLatin1()), QCryptographicHash::Md5).toHex()), "self", QDateTime::currentDateTime(), QDateTime::currentDateTime(), false, message, History::MessageTypeInformation, History::MessageStatusUnknown, QDateTime::currentDateTime(), subject, (History::InformationType)informationType); History::Events events; events << historyEvent; return History::Manager::instance()->writeEvents(events); } void HistoryModel::onContactInfoChanged(const QString &accountId, const QString &identifier, const QVariantMap &contactInfo) { Q_UNUSED(contactInfo) if (!mMatchContacts) { return; } QList changedIndexes; int count = rowCount(); for (int i = 0; i < count; ++i) { // WARNING: do not use mEvents directly to verify which indexes to change as there is the // HistoryGroupedEventsModel which is based on this model and handles the items in a different way QModelIndex idx = index(i); QVariantMap properties = idx.data(PropertiesRole).toMap(); History::Participants participants = History::Participants::fromVariantList(properties[History::FieldParticipants].toList()); Q_FOREACH(const History::Participant &participant, participants) { // FIXME: right now we might be grouping threads from different accounts, so we are not enforcing // the accountId to be the same as the one from the contact info, but maybe we need to do that // in the future? if (History::Utils::compareIds(accountId, History::ContactMatcher::normalizeId(participant.identifier()), identifier)) { changedIndexes << idx; } } } // now emit the dataChanged signal to all changed indexes Q_FOREACH(const QModelIndex &idx, changedIndexes) { Q_EMIT dataChanged(idx, idx); } } void HistoryModel::watchContactInfo(const QString &accountId, const QString &identifier, const QVariantMap ¤tInfo) { if (mMatchContacts) { History::ContactMatcher::instance()->watchIdentifier(accountId, identifier, currentInfo); } } void HistoryModel::timerEvent(QTimerEvent *event) { if (event->timerId() == mUpdateTimer) { if (!mWaitingForQml) { killTimer(mUpdateTimer); mUpdateTimer = 0; updateQuery(); } } else if (event->timerId() == mEventWritingTimer) { killTimer(mEventWritingTimer); mEventWritingTimer = 0; if (mEventWritingQueue.isEmpty()) { return; } if (History::Manager::instance()->writeEvents(mEventWritingQueue)) { mEventWritingQueue.clear(); } } else if (event->timerId() == mThreadWritingTimer) { killTimer(mThreadWritingTimer); mThreadWritingTimer = 0; if (mThreadWritingQueue.isEmpty()) { return; } History::Manager::instance()->markThreadsAsRead(mThreadWritingQueue); mThreadWritingQueue.clear(); } } bool HistoryModel::lessThan(const QVariantMap &left, const QVariantMap &right) const { QStringList sortFields = sort()->sortField().split(","); while(!sortFields.isEmpty()) { QString sortField = sortFields.takeFirst().trimmed(); QVariant leftValue = left.value(sortField, QVariant()); QVariant rightValue = right.value(sortField, QVariant()); if (leftValue != rightValue) { return leftValue < rightValue; } } return false; } int HistoryModel::positionForItem(const QVariantMap &item) const { // do a binary search for the item position on the list int lowerBound = 0; int upperBound = rowCount() - 1; if (upperBound < 0) { return 0; } while (true) { int pos = (upperBound + lowerBound) / 2; const QVariantMap posItem = index(pos).data(PropertiesRole).toMap(); if (lowerBound == pos) { if (isAscending() ? lessThan(item, posItem) : lessThan(posItem, item)) { return pos; } } if (isAscending() ? lessThan(posItem, item) : lessThan(item, posItem)) { lowerBound = pos + 1; // its in the upper if (lowerBound > upperBound) { return pos += 1; } } else if (lowerBound > upperBound) { return pos; } else { upperBound = pos - 1; // its in the lower } } } bool HistoryModel::isAscending() const { return mSort && mSort->sort().sortOrder() == Qt::AscendingOrder; } QVariant HistoryModel::get(int row) const { QVariantMap data; QModelIndex idx = index(row, 0); if (idx.isValid()) { QHash roles = roleNames(); Q_FOREACH(int role, roles.keys()) { data.insert(roles[role], idx.data(role)); } } return data; } bool HistoryModel::markEventAsRead(const QVariantMap &eventProperties) { History::Event event; History::EventType type = (History::EventType) eventProperties[History::FieldType].toInt(); switch (type) { case History::EventTypeText: event = History::TextEvent::fromProperties(eventProperties); break; case History::EventTypeVoice: event = History::VoiceEvent::fromProperties(eventProperties); break; case History::EventTypeNull: qWarning("HistoryModel::markEventAsRead: Got EventTypeNull, ignoring this event!"); break; } event.setNewEvent(false); if (event.type() == History::EventTypeText) { History::TextEvent textEvent = event; textEvent.setReadTimestamp(QDateTime::currentDateTime()); event = textEvent; } // for repeated events, keep the last called one only if (mEventWritingQueue.contains(event)) { mEventWritingQueue.removeOne(event); } mEventWritingQueue << event; if (mEventWritingTimer != 0) { killTimer(mEventWritingTimer); } mEventWritingTimer = startTimer(500); return true; } void HistoryModel::markThreadsAsRead(const QVariantList &threadsProperties) { Q_FOREACH(const QVariant &entry, threadsProperties) { QVariantMap threadProperties = entry.toMap(); History::Thread thread = History::Thread::fromProperties(threadProperties); if (!thread.isNull()) { if (mThreadWritingQueue.contains(thread)) { continue; } mThreadWritingQueue << thread; } } if (mThreadWritingTimer != 0) { killTimer(mThreadWritingTimer); } mThreadWritingTimer = startTimer(2000); } void HistoryModel::classBegin() { mWaitingForQml = true; } void HistoryModel::componentComplete() { mWaitingForQml = false; if (mUpdateTimer) { killTimer(mUpdateTimer); mUpdateTimer = 0; } updateQuery(); } void HistoryModel::triggerQueryUpdate() { if (mUpdateTimer) { killTimer(mUpdateTimer); } // delay the loading of the model data until the settings settle down mUpdateTimer = startTimer(100); } history-service-0.5/Lomiri/History/historymodel.h000066400000000000000000000220451455450246700223230ustar00rootroot00000000000000/* * Copyright (C) 2013-2016 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 HISTORYMODEL_H #define HISTORYMODEL_H #include "types.h" #include "event.h" #include "thread.h" #include "historyqmlfilter.h" #include "historyqmlsort.h" #include #include #include class HistoryModel : public QAbstractListModel, public QQmlParserStatus { Q_OBJECT Q_INTERFACES(QQmlParserStatus) Q_PROPERTY(int count READ rowCount NOTIFY countChanged) Q_PROPERTY(HistoryQmlFilter *filter READ filter WRITE setFilter NOTIFY filterChanged) Q_PROPERTY(HistoryQmlSort *sort READ sort WRITE setSort NOTIFY sortChanged) Q_PROPERTY(EventType type READ type WRITE setType NOTIFY typeChanged) Q_PROPERTY(bool matchContacts READ matchContacts WRITE setMatchContacts NOTIFY matchContactsChanged) Q_PROPERTY(bool canFetchMore READ canFetchMore NOTIFY canFetchMoreChanged) Q_ENUMS(ChatType) Q_ENUMS(EventType) Q_ENUMS(MessageType) Q_ENUMS(MatchFlag) Q_ENUMS(MessageStatus) Q_ENUMS(AttachmentFlag) Q_ENUMS(Role) Q_ENUMS(InformationType) public: enum ChatType { ChatTypeNone = History::ChatTypeNone, ChatTypeContact = History::ChatTypeContact, ChatTypeRoom = History::ChatTypeRoom }; enum EventType { EventTypeText = History::EventTypeText, EventTypeVoice = History::EventTypeVoice }; enum MessageType { MessageTypeText = History::MessageTypeText, MessageTypeMultiPart = History::MessageTypeMultiPart, MessageTypeInformation = History::MessageTypeInformation }; enum MatchFlag { MatchCaseSensitive = History::MatchCaseSensitive, MatchCaseInsensitive = History::MatchCaseInsensitive, MatchContains = History::MatchContains, MatchPhoneNumber = History::MatchPhoneNumber, MatchNotEquals = History::MatchNotEquals, MatchLess = History::MatchLess, MatchGreater = History::MatchGreater, MatchLessOrEquals = History::MatchLessOrEquals, MatchGreaterOrEquals = History::MatchGreaterOrEquals }; enum MessageStatus { MessageStatusUnknown = History::MessageStatusUnknown, MessageStatusDelivered = History::MessageStatusDelivered, MessageStatusTemporarilyFailed = History::MessageStatusTemporarilyFailed, MessageStatusPermanentlyFailed = History::MessageStatusPermanentlyFailed, MessageStatusAccepted = History::MessageStatusAccepted, MessageStatusRead = History::MessageStatusRead, MessageStatusDeleted = History::MessageStatusDeleted, MessageStatusPending = History::MessageStatusPending, // pending attachment download MessageStatusDraft = History::MessageStatusDraft }; enum AttachmentFlag { AttachmentDownloaded = History::AttachmentDownloaded, AttachmentPending = History::AttachmentPending, AttachmentError = History::AttachmentError }; enum InformationType { InformationTypeNone = History::InformationTypeNone, InformationTypeSimChange = History::InformationTypeSimChange, InformationTypeText = History::InformationTypeText, InformationTypeSelfJoined = History::InformationTypeSelfJoined, InformationTypeJoined = History::InformationTypeJoined, InformationTypeTitleChanged = History::InformationTypeTitleChanged, InformationTypeInvitationSent = History::InformationTypeInvitationSent, InformationTypeLeaving = History::InformationTypeLeaving, InformationTypeSelfLeaving = History::InformationTypeSelfLeaving, InformationTypeAdminGranted = History::InformationTypeAdminGranted, InformationTypeAdminRemoved = History::InformationTypeAdminRemoved, InformationTypeSelfAdminGranted = History::InformationTypeSelfAdminGranted, InformationTypeSelfAdminRemoved = History::InformationTypeSelfAdminRemoved, InformationTypeSelfKicked = History::InformationTypeSelfKicked, InformationTypeGroupGone = History::InformationTypeGroupGone }; enum Role { AccountIdRole = Qt::UserRole, ThreadIdRole, ParticipantsRole, ParticipantsLocalPendingRole, ParticipantsRemotePendingRole, ParticipantIdsRole, TypeRole, TimestampRole, SentTimeRole, PropertiesRole, LastRole }; explicit HistoryModel(QObject *parent = 0); Q_INVOKABLE virtual bool canFetchMore(const QModelIndex &parent = QModelIndex()) const; Q_INVOKABLE virtual void fetchMore(const QModelIndex &parent = QModelIndex()); virtual QHash roleNames() const; virtual QVariant data(const QModelIndex &index, int role) const; HistoryQmlFilter *filter() const; void setFilter(HistoryQmlFilter *value); HistoryQmlSort *sort() const; void setSort(HistoryQmlSort *value); EventType type() const; void setType(EventType value); bool matchContacts() const; void setMatchContacts(bool value); Q_INVOKABLE QVariantMap threadForProperties(const QString &accountId, int eventType, const QVariantMap &properties, int matchFlags = (int)History::MatchCaseSensitive, bool create = false); Q_INVOKABLE QString threadIdForProperties(const QString &accountId, int eventType, const QVariantMap &properties, int matchFlags = (int)History::MatchCaseSensitive, bool create = false); Q_INVOKABLE QVariantMap threadForParticipants(const QString &accountId, int eventType, const QStringList &participants, int matchFlags = (int)History::MatchCaseSensitive, bool create = false); Q_INVOKABLE QString threadIdForParticipants(const QString &accountId, int eventType, const QStringList &participants, int matchFlags = (int)History::MatchCaseSensitive, bool create = false); Q_INVOKABLE void requestThreadParticipants(const QVariantList &threads); Q_INVOKABLE bool writeTextInformationEvent(const QString &accountId, const QString &threadId, const QStringList &participants, const QString &message, int informationType = (int)History::InformationTypeNone, const QString &subject = QString()); Q_INVOKABLE virtual QVariant get(int row) const; // Marking events and threads as read Q_INVOKABLE bool markEventAsRead(const QVariantMap &eventProperties); Q_INVOKABLE void markThreadsAsRead(const QVariantList &threadsProperties); // QML parser status things void classBegin(); void componentComplete(); Q_SIGNALS: void countChanged(); void filterChanged(); void sortChanged(); void typeChanged(); void matchContactsChanged(); void canFetchMoreChanged(); protected Q_SLOTS: void triggerQueryUpdate(); virtual void updateQuery() = 0; void onContactInfoChanged(const QString &accountId, const QString &identifier, const QVariantMap &contactInfo); void watchContactInfo(const QString &accountId, const QString &identifier, const QVariantMap ¤tInfo); protected: virtual void timerEvent(QTimerEvent *event); bool lessThan(const QVariantMap &left, const QVariantMap &right) const; int positionForItem(const QVariantMap &item) const; bool isAscending() const; HistoryQmlFilter *mFilter; HistoryQmlSort *mSort; EventType mType; bool mMatchContacts; private: History::Events mEventWritingQueue; int mUpdateTimer; int mEventWritingTimer; int mThreadWritingTimer; bool mWaitingForQml; History::Threads mThreadWritingQueue; QHash mRoles; }; #endif // HISTORYMODEL_H history-service-0.5/Lomiri/History/historyqmlfilter.cpp000066400000000000000000000074211455450246700235560ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 "historyqmlfilter.h" #include "filter.h" HistoryQmlFilter::HistoryQmlFilter(QObject *parent) : QObject(parent) { connect(this, SIGNAL(filterPropertyChanged()), SIGNAL(filterChanged())); connect(this, SIGNAL(filterValueChanged()), SIGNAL(filterChanged())); connect(this, SIGNAL(matchFlagsChanged()), SIGNAL(filterChanged())); } QString HistoryQmlFilter::filterProperty() const { return mFilter.filterProperty(); } void HistoryQmlFilter::setFilterProperty(const QString &value) { mFilter.setFilterProperty(value); Q_EMIT filterPropertyChanged(); } QVariant HistoryQmlFilter::filterValue() const { return mFilter.filterValue(); } void HistoryQmlFilter::setFilterValue(const QVariant &value) { mFilter.setFilterValue(value); Q_EMIT filterValueChanged(); } int HistoryQmlFilter::matchFlags() const { return mFilter.matchFlags(); } void HistoryQmlFilter::setMatchFlags(int flags) { mFilter.setMatchFlags((History::MatchFlags)flags); Q_EMIT matchFlagsChanged(); } History::Filter HistoryQmlFilter::filter() const { return mFilter; } HistoryQmlCompoundFilter::HistoryQmlCompoundFilter(QObject *parent) : HistoryQmlFilter(parent) { } HistoryQmlCompoundFilter::~HistoryQmlCompoundFilter() { } QQmlListProperty HistoryQmlCompoundFilter::filters() { return QQmlListProperty(this, 0, // opaque data filtersAppend, filtersCount, filtersAt, filtersClear); } void HistoryQmlCompoundFilter::filtersAppend(QQmlListProperty *prop, HistoryQmlFilter *filter) { HistoryQmlCompoundFilter* compoundFilter = static_cast(prop->object); compoundFilter->mFilters.append(filter); QObject::connect(filter, SIGNAL(filterChanged()), compoundFilter, SIGNAL(filterChanged()), Qt::UniqueConnection); Q_EMIT compoundFilter->filterChanged(); } int HistoryQmlCompoundFilter::filtersCount(QQmlListProperty *prop) { HistoryQmlCompoundFilter *compoundFilter = static_cast(prop->object); return compoundFilter->mFilters.count(); } HistoryQmlFilter *HistoryQmlCompoundFilter::filtersAt(QQmlListProperty *prop, int index) { HistoryQmlCompoundFilter* compoundFilter = static_cast(prop->object); return compoundFilter->mFilters[index]; } void HistoryQmlCompoundFilter::filtersClear(QQmlListProperty *prop) { HistoryQmlCompoundFilter* compoundFilter = static_cast(prop->object); if (!compoundFilter->mFilters.isEmpty()) { Q_FOREACH(HistoryQmlFilter *filter, compoundFilter->mFilters) { filter->disconnect(compoundFilter); } compoundFilter->mFilters.clear(); } } history-service-0.5/Lomiri/History/historyqmlfilter.h000066400000000000000000000061101455450246700232150ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 HISTORYQMLFILTER_H #define HISTORYQMLFILTER_H #include #include #include "types.h" #include "filter.h" class HistoryQmlFilter : public QObject { Q_OBJECT Q_ENUMS(MatchFlag) Q_PROPERTY(QString filterProperty READ filterProperty WRITE setFilterProperty NOTIFY filterPropertyChanged) Q_PROPERTY(QVariant filterValue READ filterValue WRITE setFilterValue NOTIFY filterValueChanged) Q_PROPERTY(int matchFlags READ matchFlags WRITE setMatchFlags NOTIFY matchFlagsChanged) public: enum MatchFlag { MatchCaseSensitive = History::MatchCaseSensitive, MatchCaseInsensitive = History::MatchCaseInsensitive, MatchContains = History::MatchContains, MatchPhoneNumber = History::MatchPhoneNumber, MatchNotEquals = History::MatchNotEquals, MatchLess = History::MatchLess, MatchGreater = History::MatchGreater, MatchLessOrEquals = History::MatchLessOrEquals, MatchGreaterOrEquals = History::MatchGreaterOrEquals }; explicit HistoryQmlFilter(QObject *parent = 0); QString filterProperty() const; void setFilterProperty(const QString &value); QVariant filterValue() const; void setFilterValue(const QVariant &value); int matchFlags() const; void setMatchFlags(int flags); virtual History::Filter filter() const; Q_SIGNALS: void filterPropertyChanged(); void filterValueChanged(); void matchFlagsChanged(); void filterChanged(); protected: History::Filter mFilter; }; // compound filter class HistoryQmlCompoundFilter : public HistoryQmlFilter { Q_OBJECT Q_PROPERTY(QQmlListProperty filters READ filters NOTIFY filtersChanged) Q_CLASSINFO("DefaultProperty", "filters") public: explicit HistoryQmlCompoundFilter(QObject* parent = 0); virtual ~HistoryQmlCompoundFilter(); QQmlListProperty filters(); static void filtersAppend(QQmlListProperty* prop, HistoryQmlFilter* filter); static int filtersCount(QQmlListProperty* prop); static HistoryQmlFilter* filtersAt(QQmlListProperty* prop, int index); static void filtersClear(QQmlListProperty* prop); Q_SIGNALS: void filtersChanged(); protected: QList mFilters; }; #endif // HISTORYQMLFILTER_H history-service-0.5/Lomiri/History/historyqmlintersectionfilter.cpp000066400000000000000000000022701455450246700262020ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 "historyqmlintersectionfilter.h" #include "intersectionfilter.h" HistoryQmlIntersectionFilter::HistoryQmlIntersectionFilter(QObject *parent) : HistoryQmlCompoundFilter(parent) { } History::Filter HistoryQmlIntersectionFilter::filter() const { History::IntersectionFilter intersectionFilter; Q_FOREACH(HistoryQmlFilter *filter, mFilters) { intersectionFilter.append(filter->filter()); } return intersectionFilter; } history-service-0.5/Lomiri/History/historyqmlintersectionfilter.h000066400000000000000000000022001455450246700256400ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 HISTORYQMLINTERSECTIONFILTER_H #define HISTORYQMLINTERSECTIONFILTER_H #include "historyqmlfilter.h" #include "types.h" #include "intersectionfilter.h" class HistoryQmlIntersectionFilter : public HistoryQmlCompoundFilter { Q_OBJECT public: explicit HistoryQmlIntersectionFilter(QObject *parent = 0); History::Filter filter() const; }; #endif // HISTORYQMLINTERSECTIONFILTER_H history-service-0.5/Lomiri/History/historyqmlplugin.cpp000066400000000000000000000043641455450246700235720ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 "historyqmlplugin.h" #include "historyqmlfilter.h" #include "historyqmlintersectionfilter.h" #include "historyqmlsort.h" #include "historyqmlunionfilter.h" #include "historythreadmodel.h" #include "historygroupedthreadsmodel.h" #include "historyeventmodel.h" #include "historygroupedeventsmodel.h" #include "historyqmltexteventattachment.h" #include "historymanager.h" #include #include void HistoryQmlPlugin::initializeEngine(QQmlEngine *engine, const char *uri) { // FIXME: check what to do here Q_UNUSED(engine) Q_UNUSED(uri) } void HistoryQmlPlugin::registerTypes(const char *uri) { // @uri History qmlRegisterType(uri, 0, 1, "HistoryEventModel"); qmlRegisterType(uri, 0, 1, "HistoryGroupedEventsModel"); qmlRegisterType(uri, 0, 1, "HistoryThreadModel"); qmlRegisterType(uri, 0, 1, "HistoryGroupedThreadsModel"); qmlRegisterType(uri, 0, 1, "HistoryFilter"); qmlRegisterType(uri, 0, 1, "HistoryIntersectionFilter"); qmlRegisterType(uri, 0, 1, "HistorySort"); qmlRegisterType(uri, 0, 1, "HistoryUnionFilter"); qmlRegisterType(uri, 0, 1, "HistoryManager"); qmlRegisterUncreatableType(uri, 0, 1, "HistoryTextEventAttachment", ""); qmlRegisterUncreatableType(uri, 0, 1, "QAbstractItemModel", ""); } history-service-0.5/Lomiri/History/historyqmlplugin.h000066400000000000000000000021441455450246700232310ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 HISTORYQMLPLUGIN_H #define HISTORYQMLPLUGIN_H #include class HistoryQmlPlugin : public QQmlExtensionPlugin { Q_OBJECT Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface") public: void initializeEngine(QQmlEngine *engine, const char *uri); void registerTypes(const char *uri); }; #endif // HISTORYQMLPLUGIN_H history-service-0.5/Lomiri/History/historyqmlsort.cpp000066400000000000000000000037141455450246700232610ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 "historyqmlsort.h" #include "sort.h" HistoryQmlSort::HistoryQmlSort(QObject *parent) : QObject(parent) { connect(this, SIGNAL(sortFieldChanged()), SIGNAL(sortChanged())); connect(this, SIGNAL(sortOrderChanged()), SIGNAL(sortChanged())); connect(this, SIGNAL(caseSensitivityChanged()), SIGNAL(sortChanged())); } QString HistoryQmlSort::sortField() const { return mSort.sortField(); } void HistoryQmlSort::setSortField(const QString &value) { mSort.setSortField(value); Q_EMIT sortFieldChanged(); } HistoryQmlSort::SortOrder HistoryQmlSort::sortOrder() const { return (SortOrder) mSort.sortOrder(); } void HistoryQmlSort::setSortOrder(HistoryQmlSort::SortOrder order) { mSort.setSortOrder((Qt::SortOrder) order); Q_EMIT sortOrderChanged(); } HistoryQmlSort::CaseSensitivity HistoryQmlSort::caseSensitivity() const { return (CaseSensitivity) mSort.caseSensitivity(); } void HistoryQmlSort::setCaseSensitivity(HistoryQmlSort::CaseSensitivity value) { mSort.setCaseSensitivity((Qt::CaseSensitivity) value); Q_EMIT caseSensitivityChanged(); } History::Sort HistoryQmlSort::sort() const { return mSort; } history-service-0.5/Lomiri/History/historyqmlsort.h000066400000000000000000000040101455450246700227140ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 HISTORYQMLSORT_H #define HISTORYQMLSORT_H #include #include "types.h" #include "sort.h" class HistoryQmlSort : public QObject { Q_OBJECT Q_ENUMS(SortOrder) Q_ENUMS(CaseSensitivity) Q_PROPERTY(QString sortField READ sortField WRITE setSortField NOTIFY sortFieldChanged) Q_PROPERTY(SortOrder sortOrder READ sortOrder WRITE setSortOrder NOTIFY sortOrderChanged) Q_PROPERTY(CaseSensitivity caseSensitivity READ caseSensitivity WRITE setCaseSensitivity NOTIFY caseSensitivityChanged) public: enum SortOrder { AscendingOrder = Qt::AscendingOrder, DescendingOrder = Qt::DescendingOrder }; enum CaseSensitivity { CaseInsensitive = Qt::CaseInsensitive, CaseSensitive = Qt::CaseSensitive }; explicit HistoryQmlSort(QObject *parent = 0); QString sortField() const; void setSortField(const QString &value); SortOrder sortOrder() const; void setSortOrder(SortOrder order); CaseSensitivity caseSensitivity() const; void setCaseSensitivity(CaseSensitivity value); History::Sort sort() const; Q_SIGNALS: void sortChanged(); void sortFieldChanged(); void sortOrderChanged(); void caseSensitivityChanged(); private: History::Sort mSort; }; #endif // HISTORYQMLSORT_H history-service-0.5/Lomiri/History/historyqmltexteventattachment.cpp000066400000000000000000000033161455450246700263670ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * Tiago Salem Herrmann * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 "texteventattachment.h" #include "historyqmltexteventattachment.h" HistoryQmlTextEventAttachment::HistoryQmlTextEventAttachment(const History::TextEventAttachment &attachment, QObject *parent) : QObject(parent), mAttachment(attachment) { } QString HistoryQmlTextEventAttachment::accountId() const { return mAttachment.accountId(); } QString HistoryQmlTextEventAttachment::threadId() const { return mAttachment.threadId(); } QString HistoryQmlTextEventAttachment::eventId() const { return mAttachment.eventId(); } QString HistoryQmlTextEventAttachment::attachmentId() const { return mAttachment.attachmentId(); } QString HistoryQmlTextEventAttachment::contentType() const { return mAttachment.contentType(); } QString HistoryQmlTextEventAttachment::filePath() const { return mAttachment.filePath(); } int HistoryQmlTextEventAttachment::status() const { return mAttachment.status(); } history-service-0.5/Lomiri/History/historyqmltexteventattachment.h000066400000000000000000000041601455450246700260320ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * Tiago Salem Herrmann * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 HISTORYQMLTEXTEVENTATTACHMENT_H #define HISTORYQMLTEXTEVENTATTACHMENT_H #include #include #include "historyqmltexteventattachment.h" #include "types.h" #include "texteventattachment.h" class HistoryQmlTextEventAttachment : public QObject { Q_OBJECT Q_ENUMS(AttachmentFlag) Q_PROPERTY(QString accountId READ accountId CONSTANT) Q_PROPERTY(QString threadId READ threadId CONSTANT) Q_PROPERTY(QString eventId READ eventId CONSTANT) Q_PROPERTY(QString attachmentId READ attachmentId CONSTANT) Q_PROPERTY(QString contentType READ contentType CONSTANT) Q_PROPERTY(QString filePath READ filePath CONSTANT) Q_PROPERTY(int status READ status CONSTANT) public: enum AttachmentFlag { AttachmentDownloaded = History::AttachmentDownloaded, AttachmentPending = History::AttachmentPending, AttachmentError = History::AttachmentError }; explicit HistoryQmlTextEventAttachment(const History::TextEventAttachment &attachment, QObject *parent = 0); QString accountId() const; QString threadId() const; QString eventId() const; QString attachmentId() const; QString contentType() const; QString filePath() const; int status() const; protected: History::TextEventAttachment mAttachment; }; #endif // HISTORYQMLTEXTEVENTATTACHMENT_H history-service-0.5/Lomiri/History/historyqmlunionfilter.cpp000066400000000000000000000021711455450246700246240ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 "historyqmlunionfilter.h" #include "unionfilter.h" HistoryQmlUnionFilter::HistoryQmlUnionFilter(QObject *parent) : HistoryQmlCompoundFilter(parent) { } History::Filter HistoryQmlUnionFilter::filter() const { History::UnionFilter unionFilter; Q_FOREACH(HistoryQmlFilter *filter, mFilters) { unionFilter.append(filter->filter()); } return unionFilter; } history-service-0.5/Lomiri/History/historyqmlunionfilter.h000066400000000000000000000021221455450246700242650ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 HISTORYQMLUNIONFILTER_H #define HISTORYQMLUNIONFILTER_H #include "historyqmlfilter.h" #include #include class HistoryQmlUnionFilter : public HistoryQmlCompoundFilter { Q_OBJECT public: explicit HistoryQmlUnionFilter(QObject *parent = 0); History::Filter filter() const; }; #endif // HISTORYQMLUNIONFILTER_H history-service-0.5/Lomiri/History/historythreadmodel.cpp000066400000000000000000000311371455450246700240500ustar00rootroot00000000000000/* * Copyright (C) 2013-2016 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 "historythreadmodel.h" #include "historyqmltexteventattachment.h" #include "manager.h" #include "threadview.h" #include "voiceevent.h" #include #include Q_DECLARE_METATYPE(History::TextEventAttachments) Q_DECLARE_METATYPE(QList) HistoryThreadModel::HistoryThreadModel(QObject *parent) : HistoryModel(parent), mCanFetchMore(true), mGroupThreads(false) { qRegisterMetaType >(); qDBusRegisterMetaType >(); // configure the roles mRoles = HistoryModel::roleNames(); mRoles[CountRole] = "count"; mRoles[UnreadCountRole] = "unreadCount"; mRoles[ChatType] = "chatType"; mRoles[ChatRoomInfo] = "chatRoomInfo"; // roles related to the thread´s last event mRoles[LastEventIdRole] = "eventId"; mRoles[LastEventSenderIdRole] = "eventSenderId"; mRoles[LastEventTimestampRole] = "eventTimestamp"; mRoles[LastEventDateRole] = "eventDate"; mRoles[LastEventNewRole] = "eventNew"; mRoles[LastEventTextMessageRole] = "eventTextMessage"; mRoles[LastEventTextMessageTypeRole] = "eventTextMessageType"; mRoles[LastEventTextMessageStatusRole] = "eventTextMessageStatus"; mRoles[LastEventTextReadTimestampRole] = "eventTextReadTimestamp"; mRoles[LastEventTextAttachmentsRole] = "eventTextAttachments"; mRoles[LastEventTextSubjectRole] = "eventTextSubject"; mRoles[LastEventCallMissedRole] = "eventCallMissed"; mRoles[LastEventCallDurationRole] = "eventCallDuration"; } int HistoryThreadModel::rowCount(const QModelIndex &parent) const { if (parent.isValid()) { return 0; } return mThreads.count(); } QVariant HistoryThreadModel::data(const QModelIndex &index, int role) const { if (!index.isValid() || index.row() < 0 || index.row() >= mThreads.count()) { return QVariant(); } History::Thread thread = mThreads[index.row()]; QVariant result = threadData(thread, role); if (result.isNull()) { result = HistoryModel::data(index, role); } return result; } QVariant HistoryThreadModel::threadData(const History::Thread &thread, int role) const { History::Event event = thread.lastEvent(); History::TextEvent textEvent; History::VoiceEvent voiceEvent; if (!event.isNull()) { switch (event.type()) { case History::EventTypeText: textEvent = event; break; case History::EventTypeVoice: voiceEvent = event; break; case History::EventTypeNull: qWarning("HistoryThreadModel::threadData: Got EventTypeNull, ignoring this event!"); break; } } QVariant result; switch (role) { case CountRole: result = thread.count(); break; case UnreadCountRole: result = thread.unreadCount(); break; case ChatType: result = thread.chatType(); break; case ChatRoomInfo: result = thread.chatRoomInfo(); break; case PropertiesRole: result = thread.properties(); break; case LastEventIdRole: if (!event.isNull()) { result = event.eventId(); } break; case LastEventSenderIdRole: if (!event.isNull()) { result = event.senderId(); } break; case LastEventTimestampRole: if (!event.isNull()) { result = event.timestamp(); } break; case LastEventDateRole: if (!event.isNull()) { result = event.timestamp().date(); } break; case LastEventNewRole: if (!event.isNull()) { result = event.newEvent(); } break; case LastEventTextMessageRole: if (!textEvent.isNull()) { result = textEvent.message(); } break; case LastEventTextMessageTypeRole: if (!textEvent.isNull()) { result = (int) textEvent.messageType(); } break; case LastEventTextMessageStatusRole: if (!textEvent.isNull()) { result = (int) textEvent.messageStatus(); } break; case LastEventTextReadTimestampRole: if (!textEvent.isNull()) { result = textEvent.readTimestamp(); } break; case LastEventTextSubjectRole: if (!textEvent.isNull()) { result = textEvent.subject(); } break; case LastEventTextAttachmentsRole: if (!textEvent.isNull()) { if (mAttachmentCache.contains(textEvent)) { result = mAttachmentCache.value(textEvent); } else { QList attachments; Q_FOREACH(const History::TextEventAttachment &attachment, textEvent.attachments()) { attachments << QVariant::fromValue(new HistoryQmlTextEventAttachment(attachment, const_cast(this))); } mAttachmentCache[textEvent] = attachments; result = attachments; } } break; case LastEventCallMissedRole: if (!voiceEvent.isNull()) { result = voiceEvent.missed(); } break; case LastEventCallDurationRole: if (!voiceEvent.isNull()) { result = voiceEvent.duration(); } break; } return result; } bool HistoryThreadModel::canFetchMore(const QModelIndex &parent) const { if (parent.isValid() || !mFilter || mThreadView.isNull()) { return false; } return mCanFetchMore; } void HistoryThreadModel::fetchMore(const QModelIndex &parent) { if (parent.isValid() || mThreadView.isNull()) { return; } History::Threads threads = fetchNextPage(); if (threads.isEmpty()) { mCanFetchMore = false; Q_EMIT canFetchMoreChanged(); } else { beginInsertRows(QModelIndex(), mThreads.count(), mThreads.count() + threads.count() - 1); mThreads << threads; endInsertRows(); } } QHash HistoryThreadModel::roleNames() const { return mRoles; } bool HistoryThreadModel::removeThreads(const QVariantList &threadsProperties) { History::Threads threads; Q_FOREACH(const QVariant &entry, threadsProperties) { QVariantMap threadProperties = entry.toMap(); History::Thread thread = History::Thread::fromProperties(threadProperties); if (!thread.isNull()) { threads << thread; } } if (threads.isEmpty()) { return false; } return History::Manager::instance()->removeThreads(threads); } void HistoryThreadModel::updateQuery() { // remove all events from the model if (!mThreads.isEmpty()) { beginRemoveRows(QModelIndex(), 0, mThreads.count() - 1); mThreads.clear(); endRemoveRows(); } History::Filter queryFilter; History::Sort querySort; if (!mThreadView.isNull()) { mThreadView->disconnect(this); } if (mFilter) { queryFilter = mFilter->filter(); } else { // we should not return anything if there is no filter return; } if (mSort) { querySort = mSort->sort(); } QVariantMap properties; if (mGroupThreads) { properties[History::FieldGroupingProperty] = History::FieldParticipants; } mThreadView = History::Manager::instance()->queryThreads((History::EventType)mType, querySort, queryFilter, properties); connect(mThreadView.data(), SIGNAL(threadsAdded(History::Threads)), SLOT(onThreadsAdded(History::Threads))); connect(mThreadView.data(), SIGNAL(threadsModified(History::Threads)), SLOT(onThreadsModified(History::Threads))); connect(mThreadView.data(), SIGNAL(threadsRemoved(History::Threads)), SLOT(onThreadsRemoved(History::Threads))); connect(mThreadView.data(), SIGNAL(threadParticipantsChanged(History::Thread, History::Participants, History::Participants, History::Participants)), SLOT(onThreadParticipantsChanged(History::Thread, History::Participants, History::Participants, History::Participants))); connect(mThreadView.data(), SIGNAL(invalidated()), SLOT(triggerQueryUpdate())); Q_FOREACH(const QVariant &attachment, mAttachmentCache) { HistoryQmlTextEventAttachment *qmlAttachment = attachment.value(); if(qmlAttachment) { qmlAttachment->deleteLater(); } } mAttachmentCache.clear(); // and fetch again mCanFetchMore = true; Q_EMIT canFetchMoreChanged(); fetchMore(QModelIndex()); } void HistoryThreadModel::onThreadParticipantsChanged(const History::Thread &thread, const History::Participants &added, const History::Participants &removed, const History::Participants &modified) { int pos = mThreads.indexOf(thread); if (pos >= 0) { mThreads[pos].removeParticipants(removed); mThreads[pos].removeParticipants(modified); mThreads[pos].addParticipants(added); mThreads[pos].addParticipants(modified); QModelIndex idx = index(pos); Q_EMIT dataChanged(idx, idx); } // watch the contact info for the received participants Q_FOREACH(const History::Participant &participant, added) { watchContactInfo(thread.accountId(), participant.identifier(), participant.properties()); } Q_FOREACH(const History::Participant &participant, modified) { watchContactInfo(thread.accountId(), participant.identifier(), participant.properties()); } } void HistoryThreadModel::fetchParticipantsIfNeeded(const History::Threads &threads) { History::Threads filtered; Q_FOREACH(const History::Thread &thread, threads) { if (thread.type() == History::EventTypeText && thread.participants().isEmpty() && (thread.chatType() != History::ChatTypeRoom || thread.accountId().startsWith("ofono"))) { filtered << thread; } } if (filtered.isEmpty()) { return; } History::Manager::instance()->requestThreadParticipants(filtered); } void HistoryThreadModel::onThreadsAdded(const History::Threads &threads) { if (threads.isEmpty()) { return; } Q_FOREACH(const History::Thread &thread, threads) { // if the thread is already inserted, skip it if (mThreads.contains(thread)) { continue; } int pos = positionForItem(thread.properties()); beginInsertRows(QModelIndex(), pos, pos); mThreads.insert(pos, thread); endInsertRows(); } fetchParticipantsIfNeeded(threads); } void HistoryThreadModel::onThreadsModified(const History::Threads &threads) { History::Threads newThreads; Q_FOREACH(const History::Thread &thread, threads) { int pos = mThreads.indexOf(thread); if (pos >= 0) { mThreads[pos] = thread; QModelIndex idx = index(pos); Q_EMIT dataChanged(idx, idx); } else { newThreads << thread; } } // add threads that were not yet on the model if (!newThreads.isEmpty()) { onThreadsAdded(newThreads); } fetchParticipantsIfNeeded(threads); } void HistoryThreadModel::onThreadsRemoved(const History::Threads &threads) { Q_FOREACH(const History::Thread &thread, threads) { int pos = mThreads.indexOf(thread); if (pos >= 0) { beginRemoveRows(QModelIndex(), pos, pos); mThreads.removeAt(pos); endRemoveRows(); } } // FIXME: there is a corner case here: if a thread was not loaded yet, but was already // removed by another client, it will still show up when a new page is requested. Maybe it // should be handle internally in History::ThreadView? } History::Threads HistoryThreadModel::fetchNextPage() { History::Threads threads = mThreadView->nextPage(); fetchParticipantsIfNeeded(threads); return threads; } history-service-0.5/Lomiri/History/historythreadmodel.h000066400000000000000000000056721455450246700235220ustar00rootroot00000000000000/* * Copyright (C) 2013-2016 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 HISTORYTHREADMODEL_H #define HISTORYTHREADMODEL_H #include "historymodel.h" #include "types.h" #include "textevent.h" #include "thread.h" class HistoryQmlFilter; class HistoryQmlSort; class HistoryThreadModel : public HistoryModel { Q_OBJECT Q_ENUMS(ThreadRole) public: enum ThreadRole { CountRole = HistoryModel::LastRole, UnreadCountRole, ChatType, ChatRoomInfo, LastEventIdRole, LastEventSenderIdRole, LastEventTimestampRole, LastEventDateRole, LastEventNewRole, LastEventTextMessageRole, LastEventTextMessageTypeRole, LastEventTextMessageStatusRole, LastEventTextReadTimestampRole, LastEventTextSubjectRole, LastEventTextAttachmentsRole, LastEventCallMissedRole, LastEventCallDurationRole, LastThreadRole }; explicit HistoryThreadModel(QObject *parent = 0); virtual int rowCount(const QModelIndex &parent = QModelIndex()) const; virtual QVariant data(const QModelIndex &index, int role) const; QVariant threadData(const History::Thread &thread, int role) const; bool canFetchMore(const QModelIndex &parent = QModelIndex()) const; void fetchMore(const QModelIndex &parent); virtual QHash roleNames() const; Q_INVOKABLE bool removeThreads(const QVariantList &threadsProperties); protected Q_SLOTS: virtual void updateQuery(); virtual void onThreadsAdded(const History::Threads &threads); virtual void onThreadsModified(const History::Threads &threads); virtual void onThreadsRemoved(const History::Threads &threads); virtual void onThreadParticipantsChanged(const History::Thread &thread, const History::Participants &added, const History::Participants &removed, const History::Participants &modified); protected: void fetchParticipantsIfNeeded(const History::Threads &threads); History::Threads fetchNextPage(); bool mCanFetchMore; bool mGroupThreads; private: History::ThreadViewPtr mThreadView; History::Threads mThreads; QHash mRoles; mutable QMap > mAttachmentCache; }; #endif // HISTORYTHREADMODEL_H history-service-0.5/Lomiri/History/qmldir000066400000000000000000000000511455450246700206340ustar00rootroot00000000000000module Lomiri.History plugin history-qml history-service-0.5/README.md000066400000000000000000000017141455450246700160130ustar00rootroot00000000000000# history-service Service that provides call log and conversation history ## Getting Started History service provide the database and an API to store/retrieve the call log (used by dialer-app ) and the sms/mms history ( used by messaging-app ). See as well telepathy-ofono for incoming message events. Database location: ~.local/share/history-service/history.sqlite ### Installing You can use [crossbuilder](http://docs.ubports.com/en/latest/systemdev/testing-locally.html#cross-building-with-crossbuilder) ( you may need to add manually dh-translations, e.g `crossbuilder inst-foreign dh-translations) ``` crossbuilder ``` ## Running the tests Run tests within the container `crossbuilder shell` and find the generated tests, currently on `history-service/obj-..../tests/` ## Contributing Please read [CONTRIBUTING.md](http://docs.ubports.com/en/latest/systemdev/testing-locally.html). ## License GPL v3.0 - see the [COPYING](COPYING) file for details history-service-0.5/TODO000066400000000000000000000000571455450246700152230ustar00rootroot00000000000000- Use implicit sharing for Threads and Events. history-service-0.5/clickable.yml000066400000000000000000000004161455450246700171660ustar00rootroot00000000000000builder: cmake dependencies_host: - libmessaging-menu-dev - libusermetricsinput1-dev - liburl-dispatcher1-dev - telepathy-mission-control-5 - lcov - gcovr - libphonenumber-dev - libqt5sql5-sqlite - libsqlite3-dev - libtelepathy-qt5-dev - qtpim5-devhistory-service-0.5/cmake/000077500000000000000000000000001455450246700156115ustar00rootroot00000000000000history-service-0.5/cmake/modules/000077500000000000000000000000001455450246700172615ustar00rootroot00000000000000history-service-0.5/cmake/modules/FindLibPhoneNumber.cmake000066400000000000000000000014141455450246700237350ustar00rootroot00000000000000set(CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake/modules) include(GNUInstallDirs) include(LibFindMacros) # Include dir find_path(LibPhoneNumber_INCLUDE_DIR NAMES phonenumberutil.h PATHS "/usr/local/${CMAKE_INSTALL_INCLUDEDIR}" ${CMAKE_INSTALL_FULL_INCLUDEDIR} PATH_SUFFIXES "phonenumbers" ) # library itself find_library(LibPhoneNumber_LIBRARY NAMES phonenumber PATHS "/usr/local/${CMAKE_INSTALL_LIBDIR}" ${CMAKE_INSTALL_FULL_LIBDIR} ) # Set the include dir variables and the libraries and let libfind_process do the rest. # NOTE: Singular variables for this library, plural for libraries this this lib depends on. set(LibPhoneNumber_PROCESS_INCLUDES LibPhoneNumber_INCLUDE_DIR) set(LibPhoneNumber_PROCESS_LIBS LibPhoneNumber_LIBRARY) libfind_process(LibPhoneNumber) history-service-0.5/cmake/modules/GenerateTest.cmake000066400000000000000000000140521455450246700226570ustar00rootroot00000000000000# # Copyright (C) 2015 Canonical, Ltd. # # Authors: # Gustavo Pichorim Boiko # # This file is part of history-service. # # history-service 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; version 3. # # history-service 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(CMakeParseArguments) find_program(DCONF_SERVICE dconf-service PATHS /usr/local/libexec /usr/local/lib/dconf /usr/libexec /usr/lib/dconf NO_DEFAULT_PATH) find_program(DBUS_RUNNER dbus-test-runner) find_program(XVFB_RUN_BIN NAMES xvfb-run ) function(generate_test TESTNAME) set(options USE_DBUS USE_UI USE_XVFB) set(oneValueArgs TIMEOUT WORKING_DIRECTORY QML_TEST WAIT_FOR) set(multiValueArgs TASKS LIBRARIES QT5_MODULES SOURCES ENVIRONMENT) cmake_parse_arguments(ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN} ) MESSAGE(STATUS "Adding test: ${TESTNAME}") # set reasonable defaults for the arguments if (NOT DEFINED ARG_WORKING_DIRECTORY) set(ARG_WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) endif () if (NOT DEFINED ARG_TIMEOUT) set(ARG_TIMEOUT 120) endif () if (NOT DEFINED ARG_QT5_MODULES) set(ARG_QT5_MODULES Core Test) endif () if (${ARG_USE_UI}) if (${ARG_USE_DBUS}) set(PLATFORM -p -platform -p offscreen) else () set(PLATFORM -platform offscreen) endif () endif() # Generate QML tests if (DEFINED ARG_QML_TEST) add_test(NAME ${TESTNAME} WORKING_DIRECTORY ${ARG_WORKING_DIRECTORY} COMMAND qmltestrunner -platform offscreen -import ${CMAKE_BINARY_DIR} -input ${CMAKE_CURRENT_SOURCE_DIR}/${ARG_QML_TEST}) set_tests_properties(${TESTNAME} PROPERTIES ENVIRONMENT "QT_QPA_FONTDIR=${CMAKE_BINARY_DIR}") else () # For sanity checking, make sure DBUS_RUNNER is available for DBUS tests if (${ARG_USE_DBUS} AND "${DBUS_RUNNER}" STREQUAL "") message(WARNING "Test ${TESTNAME} disabled because dbus-test-runner was not found.") return() endif () # No QML test, regular binary compiled test. add_executable(${TESTNAME} ${ARG_SOURCES}) qt5_use_modules(${TESTNAME} ${ARG_QT5_MODULES}) if (${ARG_USE_DBUS}) execute_process(COMMAND mktemp -d OUTPUT_VARIABLE TMPDIR) string(REPLACE "\n" "" TMPDIR ${TMPDIR}) if (NOT DEFINED ARG_ENVIRONMENT) set(ARG_ENVIRONMENT HOME=${TMPDIR} HISTORY_PLUGIN_PATH=${CMAKE_BINARY_DIR}/plugins/sqlite HISTORY_SQLITE_DBPATH=:memory: HISTORY_LOCK_FILE=${TMPDIR}/history-service.lock MC_ACCOUNT_DIR=${TMPDIR} MC_MANAGER_DIR=${TMPDIR}) endif () if (${ARG_USE_XVFB}) SET(XVFB_RUN ${XVFB_RUN_BIN} -a -s "-screen 0 1024x768x24") endif () set(TEST_COMMAND ${CMAKE_CURRENT_BINARY_DIR}/${TESTNAME} ${PLATFORM} -p -o -p -,txt -p -o -p ${CMAKE_BINARY_DIR}/test_${TESTNAME}.xml,xunitxml) if (DEFINED ARG_WAIT_FOR) SET(TEST_COMMAND ${TEST_COMMAND} --wait-for ${ARG_WAIT_FOR}) endif () add_test(${TESTNAME} ${XVFB_RUN} ${DBUS_RUNNER} --keep-env --dbus-config=${CMAKE_BINARY_DIR}/tests/common/dbus-session.conf --max-wait=${ARG_TIMEOUT} ${ARG_TASKS} --task ${TEST_COMMAND} --task-name ${TESTNAME}) else () add_test(${TESTNAME} ${CMAKE_CURRENT_BINARY_DIR}/${TESTNAME} ${PLATFORM} -o -,txt -o ${CMAKE_BINARY_DIR}/test_${TESTNAME}.xml,xunitxml) endif() set_tests_properties(${TESTNAME} PROPERTIES ENVIRONMENT "${ARG_ENVIRONMENT}" TIMEOUT ${ARG_TIMEOUT}) if (DEFINED ARG_LIBRARIES) target_link_libraries(${TESTNAME} ${ARG_LIBRARIES}) endif () enable_coverage(${TESTNAME}) endif () endfunction(generate_test) function(generate_telepathy_test TESTNAME) set(options "") set(oneValueArgs "") set(multiValueArgs TASKS LIBRARIES QT5_MODULES) cmake_parse_arguments(ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN} ) if ("${DCONF_SERVICE}" STREQUAL "") message(WARNING "Test ${TESTNAME} disabled because dconf-service was not found.") return() endif () set(TASKS --task gnome-keyring-daemon -p -r -p -d --task-name gnome-keyring --ignore-return --task "${DCONF_SERVICE}" --task-name dconf-service --ignore-return --task dconf -p write -p /org/gnome/empathy/use-conn -p false --task-name dconf-write --wait-for ca.desrt.dconf --ignore-return --task /usr/lib/telepathy/mission-control-5 --task-name mission-control --wait-for ca.desrt.dconf --ignore-return --task ${CMAKE_BINARY_DIR}/tests/common/mock/telepathy-mock --task-name telepathy-mock --wait-for org.freedesktop.Telepathy.MissionControl5 --ignore-return ${ARG_TASKS}) if (NOT DEFINED ARG_LIBRARIES) set(ARG_LIBRARIES ${TP_QT5_LIBRARIES} historyservice mockcontroller telepathytest) endif(NOT DEFINED ARG_LIBRARIES) if (NOT DEFINED ARG_QT5_MODULES) set(ARG_QT5_MODULES Core DBus Test Qml) endif (NOT DEFINED ARG_QT5_MODULES) generate_test(${TESTNAME} ${ARGN} TASKS ${TASKS} LIBRARIES ${ARG_LIBRARIES} QT5_MODULES ${ARG_QT5_MODULES} USE_DBUS) endfunction(generate_telepathy_test) history-service-0.5/cmake/modules/LibFindMacros.cmake000066400000000000000000000110601455450246700227350ustar00rootroot00000000000000# Version 1.0 (2013-04-12) # Public Domain, originally written by Lasse Kärkkäinen # Published at http://www.cmake.org/Wiki/CMake:How_To_Find_Libraries # If you improve the script, please modify the forementioned wiki page because # I no longer maintain my scripts (hosted as static files at zi.fi). Feel free # to remove this entire header if you use real version control instead. # Changelog: # 2013-04-12 Added version number (1.0) and this header, no other changes # 2009-10-08 Originally published # Works the same as find_package, but forwards the "REQUIRED" and "QUIET" arguments # used for the current package. For this to work, the first parameter must be the # prefix of the current package, then the prefix of the new package etc, which are # passed to find_package. macro (libfind_package PREFIX) set (LIBFIND_PACKAGE_ARGS ${ARGN}) if (${PREFIX}_FIND_QUIETLY) set (LIBFIND_PACKAGE_ARGS ${LIBFIND_PACKAGE_ARGS} QUIET) endif (${PREFIX}_FIND_QUIETLY) if (${PREFIX}_FIND_REQUIRED) set (LIBFIND_PACKAGE_ARGS ${LIBFIND_PACKAGE_ARGS} REQUIRED) endif (${PREFIX}_FIND_REQUIRED) find_package(${LIBFIND_PACKAGE_ARGS}) endmacro (libfind_package) # CMake developers made the UsePkgConfig system deprecated in the same release (2.6) # where they added pkg_check_modules. Consequently I need to support both in my scripts # to avoid those deprecated warnings. Here's a helper that does just that. # Works identically to pkg_check_modules, except that no checks are needed prior to use. macro (libfind_pkg_check_modules PREFIX PKGNAME) if (${CMAKE_MAJOR_VERSION} EQUAL 2 AND ${CMAKE_MINOR_VERSION} EQUAL 4) include(UsePkgConfig) pkgconfig(${PKGNAME} ${PREFIX}_INCLUDE_DIRS ${PREFIX}_LIBRARY_DIRS ${PREFIX}_LDFLAGS ${PREFIX}_CFLAGS) else (${CMAKE_MAJOR_VERSION} EQUAL 2 AND ${CMAKE_MINOR_VERSION} EQUAL 4) find_package(PkgConfig) if (PKG_CONFIG_FOUND) pkg_check_modules(${PREFIX} ${PKGNAME}) endif (PKG_CONFIG_FOUND) endif (${CMAKE_MAJOR_VERSION} EQUAL 2 AND ${CMAKE_MINOR_VERSION} EQUAL 4) endmacro (libfind_pkg_check_modules) # Do the final processing once the paths have been detected. # If include dirs are needed, ${PREFIX}_PROCESS_INCLUDES should be set to contain # all the variables, each of which contain one include directory. # Ditto for ${PREFIX}_PROCESS_LIBS and library files. # Will set ${PREFIX}_FOUND, ${PREFIX}_INCLUDE_DIRS and ${PREFIX}_LIBRARIES. # Also handles errors in case library detection was required, etc. macro (libfind_process PREFIX) # Skip processing if already processed during this run if (NOT ${PREFIX}_FOUND) # Start with the assumption that the library was found set (${PREFIX}_FOUND TRUE) # Process all includes and set _FOUND to false if any are missing foreach (i ${${PREFIX}_PROCESS_INCLUDES}) if (${i}) set (${PREFIX}_INCLUDE_DIRS ${${PREFIX}_INCLUDE_DIRS} ${${i}}) mark_as_advanced(${i}) else (${i}) set (${PREFIX}_FOUND FALSE) endif (${i}) endforeach (i) # Process all libraries and set _FOUND to false if any are missing foreach (i ${${PREFIX}_PROCESS_LIBS}) if (${i}) set (${PREFIX}_LIBRARIES ${${PREFIX}_LIBRARIES} ${${i}}) mark_as_advanced(${i}) else (${i}) set (${PREFIX}_FOUND FALSE) endif (${i}) endforeach (i) # Print message and/or exit on fatal error if (${PREFIX}_FOUND) if (NOT ${PREFIX}_FIND_QUIETLY) message (STATUS "Found ${PREFIX} ${${PREFIX}_VERSION}") endif (NOT ${PREFIX}_FIND_QUIETLY) else (${PREFIX}_FOUND) if (${PREFIX}_FIND_REQUIRED) foreach (i ${${PREFIX}_PROCESS_INCLUDES} ${${PREFIX}_PROCESS_LIBS}) message("${i}=${${i}}") endforeach (i) message (FATAL_ERROR "Required library ${PREFIX} NOT FOUND.\nInstall the library (dev version) and try again. If the library is already installed, use ccmake to set the missing variables manually.") endif (${PREFIX}_FIND_REQUIRED) endif (${PREFIX}_FOUND) endif (NOT ${PREFIX}_FOUND) endmacro (libfind_process) macro(libfind_library PREFIX basename) set(TMP "") if(MSVC80) set(TMP -vc80) endif(MSVC80) if(MSVC90) set(TMP -vc90) endif(MSVC90) set(${PREFIX}_LIBNAMES ${basename}${TMP}) if(${ARGC} GREATER 2) set(${PREFIX}_LIBNAMES ${basename}${TMP}-${ARGV2}) string(REGEX REPLACE "\\." "_" TMP ${${PREFIX}_LIBNAMES}) set(${PREFIX}_LIBNAMES ${${PREFIX}_LIBNAMES} ${TMP}) endif(${ARGC} GREATER 2) find_library(${PREFIX}_LIBRARY NAMES ${${PREFIX}_LIBNAMES} PATHS ${${PREFIX}_PKGCONF_LIBRARY_DIRS} ) endmacro(libfind_library) history-service-0.5/cmake/modules/ParseArguments.cmake000066400000000000000000000034061455450246700232260ustar00rootroot00000000000000# Parse arguments passed to a function into several lists separated by # upper-case identifiers and options that do not have an associated list e.g.: # # SET(arguments # hello OPTION3 world # LIST3 foo bar # OPTION2 # LIST1 fuz baz # ) # PARSE_ARGUMENTS(ARG "LIST1;LIST2;LIST3" "OPTION1;OPTION2;OPTION3" ${arguments}) # # results in 7 distinct variables: # * ARG_DEFAULT_ARGS: hello;world # * ARG_LIST1: fuz;baz # * ARG_LIST2: # * ARG_LIST3: foo;bar # * ARG_OPTION1: FALSE # * ARG_OPTION2: TRUE # * ARG_OPTION3: TRUE # # taken from http://www.cmake.org/Wiki/CMakeMacroParseArguments MACRO(PARSE_ARGUMENTS prefix arg_names option_names) SET(DEFAULT_ARGS) FOREACH(arg_name ${arg_names}) SET(${prefix}_${arg_name}) ENDFOREACH(arg_name) FOREACH(option ${option_names}) SET(${prefix}_${option} FALSE) ENDFOREACH(option) SET(current_arg_name DEFAULT_ARGS) SET(current_arg_list) FOREACH(arg ${ARGN}) SET(larg_names ${arg_names}) LIST(FIND larg_names "${arg}" is_arg_name) IF (is_arg_name GREATER -1) SET(${prefix}_${current_arg_name} ${current_arg_list}) SET(current_arg_name ${arg}) SET(current_arg_list) ELSE (is_arg_name GREATER -1) SET(loption_names ${option_names}) LIST(FIND loption_names "${arg}" is_option) IF (is_option GREATER -1) SET(${prefix}_${arg} TRUE) ELSE (is_option GREATER -1) SET(current_arg_list ${current_arg_list} ${arg}) ENDIF (is_option GREATER -1) ENDIF (is_arg_name GREATER -1) ENDFOREACH(arg) SET(${prefix}_${current_arg_name} ${current_arg_list}) ENDMACRO(PARSE_ARGUMENTS) history-service-0.5/cmake/modules/qt5.cmake000066400000000000000000000031061455450246700207740ustar00rootroot00000000000000# shamelessly copied over from oxide’s build system # to enable ARM cross compilation if(CMAKE_CROSSCOMPILING) # QT_MOC_EXECUTABLE is set by Qt5CoreConfigExtras, but it sets it to # the target executable rather than the host executable, which is no # use for cross-compiling. For cross-compiling, we have a guess and # override it ourselves if(NOT TARGET Qt5::moc) find_program( QT_MOC_EXECUTABLE moc PATHS /usr/lib/qt5/bin /usr/lib/${HOST_ARCHITECTURE}/qt5/bin NO_DEFAULT_PATH) if(QT_MOC_EXECUTABLE STREQUAL "QT_MOC_EXECUTABLE-NOTFOUND") message(FATAL_ERROR "Can't find a moc executable for the host arch") endif() add_executable(Qt5::moc IMPORTED) set_target_properties(Qt5::moc PROPERTIES IMPORTED_LOCATION "${QT_MOC_EXECUTABLE}") endif() # Dummy targets - not used anywhere, but this stops Qt5CoreConfigExtras.cmake # from creating them and checking if the binary exists, which is broken when # cross-building because it checks for the target system binary. We need the # host system binaries installed, because they are in the same package as the # moc in Ubuntu (qtbase5-dev-tools), which is not currently multi-arch if(NOT TARGET Qt5::qmake) add_executable(Qt5::qmake IMPORTED) endif() if(NOT TARGET Qt5::rcc) add_executable(Qt5::rcc IMPORTED) endif() if(NOT TARGET Qt5::uic) add_executable(Qt5::uic IMPORTED) endif() if(NOT TARGET Qt5::DBus) add_executable(Qt5::DBus IMPORTED) endif() else() # This should be enough to initialize QT_MOC_EXECUTABLE find_package(Qt5Core) endif() history-service-0.5/config.h.in000066400000000000000000000015131455450246700165540ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * This file is part of phone-app. * * phone-app 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; version 3. * * phone-app 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 CONFIG_H #define CONFIG_H #define HISTORY_PLUGIN_PATH "@CMAKE_INSTALL_PREFIX@/@HISTORY_PLUGIN_PATH@" #define TEST_DATA_DIR "@TEST_DATA_DIR@" #endif history-service-0.5/daemon/000077500000000000000000000000001455450246700157745ustar00rootroot00000000000000history-service-0.5/daemon/CMakeLists.txt000066400000000000000000000030441455450246700205350ustar00rootroot00000000000000 set(qt_SRCS callchannelobserver.cpp emblemcountmanager.cpp historydaemon.cpp historyservicedbus.cpp pluginmanager.cpp rolesinterface.cpp textchannelobserver.cpp ) set(daemon_SRCS main.cpp ${qt_SRCS}) include_directories( ${TP_QT5_INCLUDE_DIRS} ${CMAKE_SOURCE_DIR}/src ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR} ) qt5_add_dbus_adaptor(daemon_SRCS HistoryService.xml historyservicedbus.h HistoryServiceDBus) add_executable(history-daemon ${daemon_SRCS} ${daemon_HDRS}) qt5_use_modules(history-daemon Core DBus) target_link_libraries(history-daemon ${TP_QT5_LIBRARIES} historyservice ) pkg_get_variable(SYSTEMD_USER_UNIT_DIR systemd systemduserunitdir) configure_file(org.freedesktop.Telepathy.Client.HistoryDaemonObserver.service.in org.freedesktop.Telepathy.Client.HistoryDaemonObserver.service) configure_file(com.lomiri.HistoryService.service.in com.lomiri.HistoryService.service) configure_file(history-daemon.service.in ${CMAKE_CURRENT_BINARY_DIR}/history-daemon.service) install(TARGETS history-daemon RUNTIME DESTINATION ${CMAKE_INSTALL_FULL_LIBEXECDIR}) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/org.freedesktop.Telepathy.Client.HistoryDaemonObserver.service ${CMAKE_CURRENT_BINARY_DIR}/com.lomiri.HistoryService.service DESTINATION share/dbus-1/services ) install(FILES HistoryDaemonObserver.client DESTINATION share/telepathy/clients) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/history-daemon.service DESTINATION ${SYSTEMD_USER_UNIT_DIR}) history-service-0.5/daemon/HistoryDaemonObserver.client000066400000000000000000000013761455450246700235000ustar00rootroot00000000000000[org.freedesktop.Telepathy.Client] Interfaces=org.freedesktop.Telepathy.Client.Observer; [org.freedesktop.Telepathy.Client.Observer.ObserverChannelFilter 0] org.freedesktop.Telepathy.Channel.ChannelType s=org.freedesktop.Telepathy.Channel.Type.Call1 org.freedesktop.Telepathy.Channel.TargetHandleType u=1 org.freedesktop.Telepathy.Channel.Type.Call1.InitialAudio b=true [org.freedesktop.Telepathy.Client.Observer.ObserverChannelFilter 1] org.freedesktop.Telepathy.Channel.ChannelType s=org.freedesktop.Telepathy.Channel.Type.Text [org.freedesktop.Telepathy.Client.Observer.Capabilities] org.freedesktop.Telepathy.Channel.Type.Call1/audio=true org.freedesktop.Telepathy.Channel.Type.Call1/audio/speex=true [org.freedesktop.Telepathy.Client.Observer] Recover=true history-service-0.5/daemon/HistoryService.xml000066400000000000000000000252711455450246700215070ustar00rootroot00000000000000 An interface to the history service history-service-0.5/daemon/callchannelobserver.cpp000066400000000000000000000050031455450246700225120ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 "callchannelobserver.h" CallChannelObserver::CallChannelObserver(QObject *parent) : QObject(parent) { } void CallChannelObserver::onCallChannelAvailable(Tp::CallChannelPtr callChannel) { // save the timestamp as a property in the call channel callChannel->setProperty("timestamp", QDateTime::currentDateTime()); if (callChannel->callState() == Tp::CallStateActive) { callChannel->setProperty("activeTimestamp", QDateTime::currentDateTime()); } connect(callChannel.data(), SIGNAL(callStateChanged(Tp::CallState)), SLOT(onCallStateChanged(Tp::CallState))); mChannels.append(callChannel); mCallStates[callChannel.data()] = callChannel->callState(); } void CallChannelObserver::onCallStateChanged(Tp::CallState state) { Tp::CallChannel *channel = qobject_cast(sender()); if (!channel) { return; } switch (state) { case Tp::CallStateEnded: { bool incoming = !channel->isRequested(); bool missed = incoming && channel->callStateReason().reason == Tp::CallStateChangeReasonNoAnswer; // If the call state is not missed at this point, we need to check from which state we transitioned to ended, // if from Initialised, it means it was indeed missed if (incoming && !missed) { missed = mCallStates[channel] == Tp::CallStateInitialised; } mCallStates.remove(channel); mChannels.removeOne(Tp::CallChannelPtr(channel)); Q_EMIT callEnded(Tp::CallChannelPtr(channel), missed); break; } case Tp::CallStateActive: channel->setProperty("activeTimestamp", QDateTime::currentDateTime()); // fall through default: mCallStates[channel] = state; break; } } history-service-0.5/daemon/callchannelobserver.h000066400000000000000000000025241455450246700221640ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 CALLCHANNELOBSERVER_H #define CALLCHANNELOBSERVER_H #include #include class CallChannelObserver : public QObject { Q_OBJECT public: explicit CallChannelObserver(QObject *parent = 0); public Q_SLOTS: void onCallChannelAvailable(Tp::CallChannelPtr callChannel); Q_SIGNALS: void callEnded(Tp::CallChannelPtr callChannel, bool missed); protected Q_SLOTS: void onCallStateChanged(Tp::CallState state); private: QList mChannels; QMap mCallStates; }; #endif // CALLCHANNELOBSERVER_H history-service-0.5/daemon/com.lomiri.HistoryService.service.in000066400000000000000000000002511455450246700250120ustar00rootroot00000000000000[D-BUS Service] Name=com.lomiri.HistoryService Exec=@CMAKE_INSTALL_FULL_LIBEXECDIR@/history-daemon SystemdService=history-daemon.service AssumedAppArmorLabel=unconfined history-service-0.5/daemon/emblemcountmanager.cpp000066400000000000000000000062121455450246700223460ustar00rootroot00000000000000/* * Copyright (C) 2022 Ubports Foundation. * * Authors: * Lionel Duboeuf * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 "emblemcountmanager.h" #include "historydaemon.h" #include "types.h" #include #include #define LAUNCHER_SERVICE "com.lomiri.Shell.Launcher" #define LAUNCHER_ITEM_IFACE "com.lomiri.Shell.Launcher.Item" #define MESSAGING_APP_EMBLEM_OBJECT_PATH "/com/lomiri/Shell/Launcher/messaging_2Dapp" #define DIALER_APP_EMBLEM_OBJECT_PATH "/com/lomiri/Shell/Launcher/dialer_2Dapp" EmblemCountManager::EmblemCountManager(QObject *parent) : QObject(parent), mWatcher(LAUNCHER_SERVICE, QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForRegistration) { connect(&mWatcher, &QDBusServiceWatcher::serviceRegistered, this, [this]() { updateCounters(); }); updateCounters(); qDebug() << "emblem counters initialized"; } EmblemCountManager::~EmblemCountManager() { } QDBusInterface *EmblemCountManager::getEmblemCounter(History::EventType type) { QDBusInterface* emblemCounter = nullptr; switch (type) { case History::EventTypeText: emblemCounter = new QDBusInterface( LAUNCHER_SERVICE, MESSAGING_APP_EMBLEM_OBJECT_PATH, LAUNCHER_ITEM_IFACE); break; case History::EventTypeVoice: emblemCounter = new QDBusInterface( LAUNCHER_SERVICE, DIALER_APP_EMBLEM_OBJECT_PATH, LAUNCHER_ITEM_IFACE); break; default: qWarning() << "fail to get emblem counter dbus interface, got event type null"; break; } return emblemCounter; } void EmblemCountManager::updateCounter(History::EventType type) { QDBusInterface* emblemCounter = getEmblemCounter(type); if (emblemCounter == nullptr) { return; } // only update counter if app is pinned if (emblemCounter->isValid() && emblemCounter->property("count").isValid()) { int count = HistoryDaemon::instance()->getUnreadCount(type); // when messaging-app/dialer app is not pinned, launcher service only creates the emblem on countVisible property, // in that case the count is initialized to zero. This allows to force Item creation before setting the count emblemCounter->setProperty("countVisible", count > 0); emblemCounter->setProperty("count", count); } emblemCounter->deleteLater(); } void EmblemCountManager::updateCounters() { updateCounter(History::EventTypeText); updateCounter(History::EventTypeVoice); } history-service-0.5/daemon/emblemcountmanager.h000066400000000000000000000023601455450246700220130ustar00rootroot00000000000000/* * Copyright (C) 2022 Ubports Foundation. * * Authors: * Lionel Duboeuf * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 EMBLEMCOUNTMANAGER_H #define EMBLEMCOUNTMANAGER_H #include #include #include #include "types.h" class EmblemCountManager : public QObject { Q_OBJECT public: ~EmblemCountManager(); explicit EmblemCountManager(QObject *parent = 0); void updateCounter(History::EventType type); void updateCounters(); private: QDBusInterface *getEmblemCounter(History::EventType type); QDBusServiceWatcher mWatcher; }; #endif // EMBLEMCOUNTMANAGER_H history-service-0.5/daemon/history-daemon.service.in000066400000000000000000000003221455450246700227220ustar00rootroot00000000000000[Unit] Description=History service to store messages and calls Wants=address-book-service.service [Service] Type=dbus BusName=com.lomiri.HistoryService ExecStart=@CMAKE_INSTALL_FULL_LIBEXECDIR@/history-daemon history-service-0.5/daemon/historydaemon.cpp000066400000000000000000001724351455450246700214010ustar00rootroot00000000000000/* * Copyright (C) 2013-2017 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 "historydaemon.h" #include "mmsstoragemanager_p.h" #include "telepathyhelper_p.h" #include "filter.h" #include "sort.h" #include "utils_p.h" #include "pluginmanager.h" #include "plugin.h" #include "pluginthreadview.h" #include "plugineventview.h" #include "textevent.h" #include #include #include #include #include Q_DECLARE_METATYPE(RolesMap) const constexpr static int AdminRole = 2; enum ChannelGroupChangeReason { ChannelGroupChangeReasonNone = 0, ChannelGroupChangeReasonOffline = 1, ChannelGroupChangeReasonKicked = 2, ChannelGroupChangeReasonBusy = 3, ChannelGroupChangeReasonInvited = 4, ChannelGroupChangeReasonBanned = 5, ChannelGroupChangeReasonError = 6, ChannelGroupChangeReasonInvalidContact = 7, ChannelGroupChangeReasonNoAnswer = 8, ChannelGroupChangeReasonRenamed = 9, ChannelGroupChangeReasonPermissionDenied = 10, ChannelGroupChangeReasonSeparated = 11, // additional enum values not included in original ChannelGroupChangeReason // telepathy enumeration but needed here to provide extra info to client when group // is cancelled ChannelGroupChangeReasonGone = 12, ChannelGroupChangeReasonRejected = 13 }; const QDBusArgument &operator>>(const QDBusArgument &argument, RolesMap &roles) { argument.beginMap(); while ( !argument.atEnd() ) { argument.beginMapEntry(); uint key,value; argument >> key >> value; argument.endMapEntry(); roles[key] = value; } argument.endMap(); return argument; } bool foundAsMemberInThread(const Tp::ContactPtr& contact, QVariantMap thread) { Q_FOREACH (QVariant participant, thread[History::FieldParticipants].toList()) { // found if same identifier and as member into thread info QVariantMap participantMap = participant.toMap(); if (History::Utils::compareIds(thread[History::FieldAccountId].toString(), contact->id(), participantMap[History::FieldIdentifier].toString()) && participantMap[History::FieldParticipantState].toUInt() == History::ParticipantStateRegular) { return true; } } return false; } bool foundInThread(const Tp::ContactPtr& contact, QVariantMap thread) { Q_FOREACH (QVariant participant, thread[History::FieldParticipants].toList()) { if (History::Utils::compareIds(thread[History::FieldAccountId].toString(), contact->id(), participant.toMap()[History::FieldIdentifier].toString())) { return true; } } return false; } HistoryDaemon::HistoryDaemon(QObject *parent) : QObject(parent), mCallObserver(this), mTextObserver(this) { qRegisterMetaType(); qDBusRegisterMetaType(); // get the first plugin if (!History::PluginManager::instance()->plugins().isEmpty()) { mBackend = History::PluginManager::instance()->plugins().first(); } // FIXME: maybe we should only set the plugin as ready after the contact cache was generated connect(History::TelepathyHelper::instance(), &History::TelepathyHelper::setupReady, [&]() { mBackend->generateContactCache(); mDBus.connectToBus(); mEmblemCountManager = new EmblemCountManager(); }); connect(History::TelepathyHelper::instance(), &History::TelepathyHelper::accountAdded, [&](const Tp::AccountPtr &account) { // handle ofono accountId name change mBackend->verifyAccount(account->uniqueIdentifier()); }); connect(History::TelepathyHelper::instance(), SIGNAL(channelObserverCreated(ChannelObserver*)), SLOT(onObserverCreated())); History::TelepathyHelper::instance()->registerChannelObserver(); connect(&mCallObserver, SIGNAL(callEnded(Tp::CallChannelPtr, bool)), SLOT(onCallEnded(Tp::CallChannelPtr, bool))); connect(&mTextObserver, SIGNAL(messageReceived(Tp::TextChannelPtr,Tp::ReceivedMessage)), SLOT(onMessageReceived(Tp::TextChannelPtr,Tp::ReceivedMessage))); connect(&mTextObserver, SIGNAL(messageSent(Tp::TextChannelPtr,Tp::Message,QString)), SLOT(onMessageSent(Tp::TextChannelPtr,Tp::Message,QString))); connect(&mTextObserver, SIGNAL(channelAvailable(Tp::TextChannelPtr)), SLOT(onTextChannelAvailable(Tp::TextChannelPtr))); connect(&mTextObserver, SIGNAL(textChannelInvalidated(Tp::TextChannelPtr)), SLOT(onTextChannelInvalidated(Tp::TextChannelPtr))); // FIXME: we need to do this in a better way, but for now this should do mProtocolFlags["ofono"] = History::MatchPhoneNumber; mProtocolFlags["multimedia"] = History::MatchPhoneNumber; } HistoryDaemon::~HistoryDaemon() { mEmblemCountManager->deleteLater(); } HistoryDaemon *HistoryDaemon::instance() { static HistoryDaemon *self = new HistoryDaemon(); return self; } void HistoryDaemon::onRolesChanged(const HandleRolesMap &added, const HandleRolesMap &removed) { ChannelInterfaceRolesInterface *roles_interface = qobject_cast(sender()); Tp::TextChannelPtr channel(qobject_cast(sender()->parent())); RolesMap rolesMap; if (!mRolesMap.contains(channel->objectPath())) { rolesMap = roles_interface->getRoles(); } else { rolesMap = mRolesMap[channel->objectPath()]; } QMapIterator it(removed); while (it.hasNext()) { it.next(); rolesMap.remove(it.key()); } QMapIterator it2(added); while (it2.hasNext()) { it2.next(); rolesMap[it2.key()] = it2.value(); } mRolesMap[channel->objectPath()] = rolesMap; QVariantMap properties = propertiesFromChannel(channel); QVariantMap thread = threadForProperties(channel->property(History::FieldAccountId).toString(), History::EventTypeText, properties, matchFlagsForChannel(channel), false); if (!thread.isEmpty()) { writeRolesInformationEvents(thread, channel, rolesMap); updateRoomRoles(channel, rolesMap); } } QVariantMap HistoryDaemon::propertiesFromChannel(const Tp::ChannelPtr &textChannel) { QVariantMap properties; QVariantList participants; QStringList participantIds; QString accountId = textChannel->property(History::FieldAccountId).toString(); if (History::Utils::shouldIncludeParticipants(accountId, fromTelepathyHandleType(textChannel->targetHandleType()))) { ChannelInterfaceRolesInterface *roles_interface = textChannel->optionalInterface(); RolesMap roles; if (roles_interface) { if (mRolesMap.contains(textChannel->objectPath())) { roles = mRolesMap[textChannel->objectPath()]; } } Q_FOREACH(const Tp::ContactPtr contact, textChannel->groupContacts(false)) { QVariantMap contactProperties; contactProperties[History::FieldAlias] = contact->alias(); contactProperties[History::FieldAccountId] = accountId; contactProperties[History::FieldIdentifier] = contact->id(); contactProperties[History::FieldParticipantState] = History::ParticipantStateRegular; contactProperties[History::FieldParticipantRoles] = roles[contact->handle().at(0)]; participantIds << contact->id(); participants << contactProperties; } Q_FOREACH(const Tp::ContactPtr contact, textChannel->groupRemotePendingContacts(false)) { QVariantMap contactProperties; contactProperties[History::FieldAlias] = contact->alias(); contactProperties[History::FieldAccountId] = accountId; contactProperties[History::FieldIdentifier] = contact->id(); contactProperties[History::FieldParticipantState] = History::ParticipantStateRemotePending; contactProperties[History::FieldParticipantRoles] = roles[contact->handle().at(0)]; participantIds << contact->id(); participants << contactProperties; } Q_FOREACH(const Tp::ContactPtr contact, textChannel->groupLocalPendingContacts(false)) { QVariantMap contactProperties; contactProperties[History::FieldAlias] = contact->alias(); contactProperties[History::FieldAccountId] = accountId; contactProperties[History::FieldIdentifier] = contact->id(); contactProperties[History::FieldParticipantState] = History::ParticipantStateLocalPending; contactProperties[History::FieldParticipantRoles] = roles[contact->handle().at(0)]; participantIds << contact->id(); participants << contactProperties; } if (participants.isEmpty() && textChannel->targetHandleType() == Tp::HandleTypeContact && textChannel->targetContact() == textChannel->connection()->selfContact()) { QVariantMap contactProperties; contactProperties[History::FieldAlias] = textChannel->targetContact()->alias(); contactProperties[History::FieldAccountId] = accountId; contactProperties[History::FieldIdentifier] = textChannel->targetContact()->id(); contactProperties[History::FieldParticipantState] = History::ParticipantStateRegular; participantIds << textChannel->targetContact()->id(); participants << contactProperties; } } // We map chatType directly from telepathy targetHandleType: None, Contact, Room properties[History::FieldChatType] = textChannel->targetHandleType(); properties[History::FieldParticipants] = participants; properties[History::FieldParticipantIds] = participantIds; QVariantMap roomProperties; switch(textChannel->targetHandleType()) { case Tp::HandleTypeRoom: if (textChannel->hasInterface(TP_QT_IFACE_CHANNEL_INTERFACE_ROOM)) { auto room_interface = textChannel->optionalInterface(); QVariantMap map = getInterfaceProperties(room_interface); for (QVariantMap::const_iterator iter = map.begin(); iter != map.end(); ++iter) { if (iter.value().isValid()) { roomProperties[iter.key()] = iter.value(); } } } if (textChannel->hasInterface(TP_QT_IFACE_CHANNEL_INTERFACE_ROOM_CONFIG)) { auto room_config_interface = textChannel->optionalInterface(); QVariantMap map = getInterfaceProperties(room_config_interface); for (QVariantMap::const_iterator iter = map.begin(); iter != map.end(); ++iter) { if (iter.value().isValid()) { roomProperties[iter.key()] = iter.value(); } } } if (textChannel->hasInterface(TP_QT_IFACE_CHANNEL_INTERFACE_SUBJECT)) { auto subject_interface = textChannel->optionalInterface(); QVariantMap map = getInterfaceProperties(subject_interface); for (QVariantMap::const_iterator iter = map.begin(); iter != map.end(); ++iter) { if (iter.value().isValid()) { roomProperties[iter.key()] = iter.value(); } } } properties[History::FieldChatRoomInfo] = roomProperties; properties[History::FieldThreadId] = textChannel->targetId(); break; case Tp::HandleTypeContact: case Tp::HandleTypeNone: default: break; } return properties; } QVariantMap HistoryDaemon::threadForProperties(const QString &accountId, History::EventType type, const QVariantMap &properties, History::MatchFlags matchFlags, bool create) { if (!mBackend) { return QVariantMap(); } QVariantMap thread = mBackend->threadForProperties(accountId, type, properties, matchFlags); if (thread.isEmpty() && create) { thread = mBackend->createThreadForProperties(accountId, type, properties); if (!thread.isEmpty()) { if (properties.contains("Requested") && properties[History::FieldChatType].toInt() == History::ChatTypeRoom) { QVariantMap map = thread[History::FieldChatRoomInfo].toMap(); map["Requested"] = properties["Requested"]; thread[History::FieldChatRoomInfo] = map; } mDBus.notifyThreadsAdded(QList() << thread); } } return thread; } QString HistoryDaemon::threadIdForProperties(const QString &accountId, History::EventType type, const QVariantMap &properties, History::MatchFlags matchFlags, bool create) { if (!mBackend) { return QString(); } QString threadId = mBackend->threadIdForProperties(accountId, type, properties, matchFlags); if (threadId.isEmpty() && create) { QVariantMap thread = mBackend->createThreadForProperties(accountId, type, properties); if (!thread.isEmpty()) { if (properties.contains("Requested") && properties[History::FieldChatType].toInt() == History::ChatTypeRoom) { QVariantMap map = thread[History::FieldChatRoomInfo].toMap(); map["Requested"] = properties["Requested"]; thread[History::FieldChatRoomInfo] = map; } mDBus.notifyThreadsAdded(QList() << thread); threadId = thread[History::FieldThreadId].toString(); } } return threadId; } QList HistoryDaemon::participantsForThreads(const QList &threadIds) { if (!mBackend) { return QList(); } return mBackend->participantsForThreads(threadIds); } QString HistoryDaemon::queryThreads(int type, const QVariantMap &sort, const QVariantMap &filter, const QVariantMap &properties) { if (!mBackend) { return QString(); } History::Sort theSort = History::Sort::fromProperties(sort); History::Filter theFilter = History::Filter::fromProperties(filter); History::PluginThreadView *view = mBackend->queryThreads((History::EventType)type, theSort, theFilter, properties); if (!view) { return QString(); } // FIXME: maybe we should keep a list of views to manually remove them at some point? view->setParent(this); return view->objectPath(); } QString HistoryDaemon::queryEvents(int type, const QVariantMap &sort, const QVariantMap &filter) { if (!mBackend) { return QString(); } History::Sort theSort = History::Sort::fromProperties(sort); History::Filter theFilter = History::Filter::fromProperties(filter); History::PluginEventView *view = mBackend->queryEvents((History::EventType)type, theSort, theFilter); if (!view) { return QString(); } // FIXME: maybe we should keep a list of views to manually remove them at some point? view->setParent(this); return view->objectPath(); } QVariantMap HistoryDaemon::getSingleThread(int type, const QString &accountId, const QString &threadId, const QVariantMap &properties) { if (!mBackend) { return QVariantMap(); } return mBackend->getSingleThread((History::EventType)type, accountId, threadId, properties); } QVariantMap HistoryDaemon::getSingleEvent(int type, const QString &accountId, const QString &threadId, const QString &eventId) { if (!mBackend) { return QVariantMap(); } return mBackend->getSingleEvent((History::EventType)type, accountId, threadId, eventId); } bool HistoryDaemon::writeEvents(const QList &events, const QVariantMap &properties, bool notify) { if (!mBackend) { return false; } QList newEvents; QList modifiedEvents; QMap threads; mBackend->beginBatchOperation(); Q_FOREACH(const QVariantMap &event, events) { History::EventType type = (History::EventType) event[History::FieldType].toInt(); History::EventWriteResult result = History::EventWriteNone; // get the threads for the events to notify their modifications QString accountId = event[History::FieldAccountId].toString(); QString threadId = event[History::FieldThreadId].toString(); QVariantMap savedEvent = event; // and finally write the event switch (type) { case History::EventTypeText: result = mBackend->writeTextEvent(savedEvent); break; case History::EventTypeVoice: result = mBackend->writeVoiceEvent(savedEvent); break; case History::EventTypeNull: qWarning("HistoryDaemon::writeEvents: Got EventTypeNull, ignoring this event!"); continue; } // only get the thread AFTER the event is written to make sure it is up-to-date QVariantMap thread = getSingleThread(type, accountId, threadId, properties); QString hash = hashThread(thread); threads[hash] = thread; // set the participants field in the event if (type == History::EventTypeVoice) { savedEvent[History::FieldParticipants] = thread[History::FieldParticipants]; } // check if the event was a new one or a modification to an existing one switch (result) { case History::EventWriteCreated: newEvents << savedEvent; break; case History::EventWriteModified: modifiedEvents << savedEvent; break; case History::EventWriteError: mBackend->rollbackBatchOperation(); return false; case History::EventWriteNone: break; } } mBackend->endBatchOperation(); mEmblemCountManager->updateCounters(); // and last but not least, notify the results if (!newEvents.isEmpty() && notify) { mDBus.notifyEventsAdded(newEvents); } if (!modifiedEvents.isEmpty() && notify) { mDBus.notifyEventsModified(modifiedEvents); } if (!threads.isEmpty() && notify) { mDBus.notifyThreadsModified(threads.values()); } return true; } bool HistoryDaemon::removeEvents(const QList &events) { if (!mBackend) { return false; } mBackend->beginBatchOperation(); Q_FOREACH(const QVariantMap &event, events) { History::EventType type = (History::EventType) event[History::FieldType].toInt(); bool success = true; switch (type) { case History::EventTypeText: success = mBackend->removeTextEvent(event); break; case History::EventTypeVoice: success = mBackend->removeVoiceEvent(event); break; case History::EventTypeNull: qWarning("HistoryDaemon::removeEvents: Got EventTypeNull, ignoring this event!"); break; } if (!success) { mBackend->rollbackBatchOperation(); return false; } } // now we need to get all the threads that were affected by the removal of events // this loop needs to be separate from the item removal loop because we rely on the // count property of threads to decide if they were just modified or if they need to // be removed. QMap removedThreads; QMap modifiedThreads; Q_FOREACH(const QVariantMap &event, events) { History::EventType type = (History::EventType) event[History::FieldType].toInt(); QString accountId = event[History::FieldAccountId].toString(); QString threadId = event[History::FieldThreadId].toString(); QVariantMap thread = mBackend->getSingleThread(type, accountId, threadId, QVariantMap()); if (thread.isEmpty()) { continue; } QString hash = hashThread(thread); if (thread[History::FieldCount].toInt() > 0) { // the thread still has items and we should notify it was modified modifiedThreads[hash] = thread; } else { removedThreads[hash] = thread; } } // finally remove the threads that are now empty Q_FOREACH(const QVariantMap &thread, removedThreads.values()) { // the thread is now empty and needs to be removed if (!mBackend->removeThread(thread)) { mBackend->rollbackBatchOperation(); return false; } } mBackend->endBatchOperation(); History::MmsStorageManager::instance()->removeAttachmentsFromEvents(events); History::MmsStorageManager::instance()->removeAttachmentsFromThreads(removedThreads.values()); mDBus.notifyEventsRemoved(events); if (!removedThreads.isEmpty()) { mDBus.notifyThreadsRemoved(removedThreads.values()); } if (!modifiedThreads.isEmpty()) { mDBus.notifyThreadsModified(modifiedThreads.values()); } return true; } bool HistoryDaemon::removeEvents(int type, const QVariantMap &filter, int &removedCount) { if (!mBackend) { return false; } History::EventType eventType = (History::EventType) type; History::Filter theFilter = History::Filter::fromProperties(filter); QList attachments; if (eventType == History::EventTypeText) { attachments = mBackend->attachmentFilePathsForFilter(theFilter); } mBackend->beginBatchOperation(); removedCount = mBackend->removeEvents(eventType, theFilter); if (removedCount == -1) { mBackend->rollbackBatchOperation(); return false; } mBackend->endBatchOperation(); History::MmsStorageManager::instance()->removeAttachments(attachments); mEmblemCountManager->updateCounter((History::EventType) type); return true; } int HistoryDaemon::getEventsCount(int type, const QVariantMap &filter) { History::Filter theFilter = History::Filter::fromProperties(filter); return mBackend->eventsCount((History::EventType)type, theFilter); } int HistoryDaemon::getUnreadCount(History::EventType type) { return mBackend->unreadCount(type); } void HistoryDaemon::markThreadsAsRead(const QList &threads) { if (!mBackend) { return; } QList modifiedThreads; Q_FOREACH(const QVariantMap &thread, threads) { mBackend->beginBatchOperation(); QVariantMap newThread = mBackend->markThreadAsRead(thread); if (!newThread.isEmpty()) { modifiedThreads << newThread; } mBackend->endBatchOperation(); } if (!modifiedThreads.isEmpty()) { mDBus.notifyThreadsModified(modifiedThreads); mEmblemCountManager->updateCounters(); } } bool HistoryDaemon::removeThreads(const QList &threads) { if (!mBackend) { return false; } // If the thread has events mBackend->beginBatchOperation(); Q_FOREACH(const QVariantMap &thread, threads) { if (!mBackend->removeThread(thread)) { mBackend->rollbackBatchOperation(); return false; } } mBackend->endBatchOperation(); mEmblemCountManager->updateCounters(); mDBus.notifyThreadsRemoved(threads); History::MmsStorageManager::instance()->removeAttachmentsFromThreads(threads); return true; } void HistoryDaemon::onObserverCreated() { History::ChannelObserver *observer = History::TelepathyHelper::instance()->channelObserver(); connect(observer, SIGNAL(callChannelAvailable(Tp::CallChannelPtr)), &mCallObserver, SLOT(onCallChannelAvailable(Tp::CallChannelPtr))); connect(observer, SIGNAL(textChannelAvailable(Tp::TextChannelPtr)), &mTextObserver, SLOT(onTextChannelAvailable(Tp::TextChannelPtr))); } void HistoryDaemon::onCallEnded(const Tp::CallChannelPtr &channel, bool missed) { QVariantMap properties = propertiesFromChannel(channel); QVariantList participants; Q_FOREACH(const Tp::ContactPtr contact, channel->remoteMembers()) { QVariantMap contactProperties; contactProperties[History::FieldAlias] = contact->alias(); contactProperties[History::FieldIdentifier] = contact->id(); contactProperties[History::FieldAccountId] = channel->property(History::FieldAccountId).toString(); participants << contactProperties; } // it shouldn't happen, but in case it does, we won't crash if (participants.isEmpty()) { qWarning() << "Participants list was empty for call channel" << channel; return; } QString accountId = channel->property(History::FieldAccountId).toString(); QVariantMap thread = threadForProperties(accountId, History::EventTypeVoice, properties, matchFlagsForChannel(channel), true); // fill the call info QDateTime timestamp = channel->property(History::FieldTimestamp).toDateTime(); // FIXME: check if checking for isRequested() is enough bool incoming = !channel->isRequested(); int duration = 0; if (!missed) { QDateTime activeTime = channel->property("activeTimestamp").toDateTime(); duration = activeTime.secsTo(QDateTime::currentDateTime()); } QString eventId = QString("%1:%2").arg(thread[History::FieldThreadId].toString()).arg(timestamp.toString()); QVariantMap event; event[History::FieldType] = History::EventTypeVoice; event[History::FieldAccountId] = thread[History::FieldAccountId]; event[History::FieldThreadId] = thread[History::FieldThreadId]; event[History::FieldEventId] = eventId; event[History::FieldSenderId] = incoming ? channel->initiatorContact()->id() : "self"; event[History::FieldTimestamp] = timestamp.toString("yyyy-MM-ddTHH:mm:ss.zzz"); event[History::FieldNewEvent] = missed; // only mark as a new (unseen) event if it is a missed call event[History::FieldMissed] = missed; event[History::FieldDuration] = duration; // FIXME: check what to do when there are more than just one remote participant event[History::FieldRemoteParticipant] = participants[0].toMap()[History::FieldIdentifier]; writeEvents(QList() << event, properties); } void HistoryDaemon::onTextChannelInvalidated(const Tp::TextChannelPtr channel) { mRolesMap.remove(channel->objectPath()); QString accountId = channel->property(History::FieldAccountId).toString(); QVariantMap properties = propertiesFromChannel(channel); // first try to fetch the existing thread to see if there is any. QVariantMap thread = threadForProperties(accountId, History::EventTypeText, properties, matchFlagsForChannel(channel), false); QVariantMap roomInfo = thread[History::FieldChatRoomInfo].toMap(); if ((roomInfo.contains("Persistent") && !roomInfo["Persistent"].toBool()) && History::TelepathyHelper::instance()->accountForId(accountId)->protocolName() != "ofono") { writeInformationEvent(thread, History::InformationTypeSelfLeaving); // update backend updateRoomProperties(channel, QVariantMap{{"Joined", false}}); } channel->disconnect(this); } void HistoryDaemon::onTextChannelAvailable(const Tp::TextChannelPtr channel) { // for Rooms we need to explicitly create the thread to allow users to send messages to groups even // before they receive any message. // for other types, we can wait until messages are received bool notify = false; if (channel->targetHandleType() == Tp::HandleTypeRoom) { QString accountId = channel->property(History::FieldAccountId).toString(); QVariantMap properties = propertiesFromChannel(channel); // first try to fetch the existing thread to see if there is any. QVariantMap thread = threadForProperties(accountId, History::EventTypeText, properties, matchFlagsForChannel(channel), false); if (thread.isEmpty()) { // if there no existing thread, create one properties["Requested"] = channel->isRequested(); thread = threadForProperties(accountId, History::EventTypeText, properties, matchFlagsForChannel(channel), true); // write information event including all initial invitees Q_FOREACH(const Tp::ContactPtr contact, channel->groupRemotePendingContacts(false)) { writeInformationEvent(thread, History::InformationTypeInvitationSent, contact->alias(), QString(), QString(), false); } // update participants only if the thread is not available previously. Otherwise we'll wait for membersChanged event // for reflect in conversation information events for modified participants. updateRoomParticipants(channel, false); notify = true; } // write an entry saying you joined the group if 'joined' flag in thread is false and modify that flag. if (!thread[History::FieldChatRoomInfo].toMap()["Joined"].toBool()) { // only write self joined notification if protocol is not a phone one. // FIXME (rmescandon): as a first solution, let's take only ofono as phone protocol if (History::TelepathyHelper::instance()->accountForId(accountId)->protocolName() != "ofono") { writeInformationEvent(thread, History::InformationTypeSelfJoined); } // update backend updateRoomProperties(channel, QVariantMap{{"Joined", true}}, false); notify = true; } Tp::AbstractInterface *room_interface = channel->optionalInterface(); Tp::AbstractInterface *room_config_interface = channel->optionalInterface(); Tp::AbstractInterface *subject_interface = channel->optionalInterface(); ChannelInterfaceRolesInterface *roles_interface = channel->optionalInterface(); QList interfaces; interfaces << room_interface << room_config_interface << subject_interface << roles_interface; for (auto interface : interfaces) { if (interface) { interface->setMonitorProperties(true); interface->setProperty(History::FieldAccountId, accountId); interface->setProperty(History::FieldThreadId, thread[History::FieldThreadId].toString()); interface->setProperty(History::FieldType, thread[History::FieldType].toInt()); connect(interface, SIGNAL(propertiesChanged(const QVariantMap &,const QStringList &)), SLOT(onRoomPropertiesChanged(const QVariantMap &,const QStringList &))); // update the stored info Q_EMIT interface->propertiesChanged(getInterfaceProperties(interface), QStringList()); } } connect(channel.data(), SIGNAL(groupMembersChanged(const Tp::Contacts &, const Tp::Contacts &, const Tp::Contacts &, const Tp::Contacts &, const Tp::Channel::GroupMemberChangeDetails &)), SLOT(onGroupMembersChanged(const Tp::Contacts &, const Tp::Contacts &, const Tp::Contacts &, const Tp::Contacts &, const Tp::Channel::GroupMemberChangeDetails &))); connect(roles_interface, SIGNAL(RolesChanged(const HandleRolesMap&, const HandleRolesMap&)), SLOT(onRolesChanged(const HandleRolesMap&, const HandleRolesMap&))); } if (notify) { updateRoomParticipants(channel, true); } } void HistoryDaemon::onGroupMembersChanged(const Tp::Contacts &groupMembersAdded, const Tp::Contacts& /* groupLocalPendingMembersAdded */, const Tp::Contacts &groupRemotePendingMembersAdded, const Tp::Contacts &groupMembersRemoved, const Tp::Channel::GroupMemberChangeDetails& /* details */) { Tp::TextChannelPtr channel(qobject_cast(sender())); QVariantMap properties; QVariantMap thread; // information events for members updates. bool hasRemotePendingMembersAdded = groupRemotePendingMembersAdded.size() > 0; bool hasMembersAdded = groupMembersAdded.size() > 0; bool hasMembersRemoved = groupMembersRemoved.size() > 0; Tp::ContactPtr selfContact = channel->connection()->selfContact(); bool selfContactIsPending = channel->groupRemotePendingContacts(true).contains(selfContact); if (hasRemotePendingMembersAdded || hasMembersAdded || hasMembersRemoved) { properties = propertiesFromChannel(channel); thread = threadForProperties(channel->property(History::FieldAccountId).toString(), History::EventTypeText, properties, matchFlagsForChannel(channel), false); if (!thread.isEmpty() && !selfContactIsPending) { QList added; QList removed; QList modified; if (hasRemotePendingMembersAdded) { Q_FOREACH (const Tp::ContactPtr& contact, groupRemotePendingMembersAdded) { if (!foundInThread(contact, thread)) { writeInformationEvent(thread, History::InformationTypeInvitationSent, contact->alias(), QString(), QString(), true); QVariantMap participant; participant[History::FieldIdentifier] = contact->id(); participant[History::FieldAlias] = contact->alias(); participant[History::FieldParticipantState] = History::ParticipantStateRemotePending; added << participant; } } } if (hasMembersAdded) { Q_FOREACH (const Tp::ContactPtr& contact, groupMembersAdded) { // if this member was not previously regular member in thread, notify about his join if (!foundAsMemberInThread(contact, thread) && contact->id() != channel->groupSelfContact()->id()) { writeInformationEvent(thread, History::InformationTypeJoined, contact->alias(), QString(), QString(), true); QVariantMap participant; participant[History::FieldIdentifier] = contact->id(); participant[History::FieldAlias] = contact->alias(); participant[History::FieldParticipantState] = History::ParticipantStateRegular; added << participant; } } } if (hasMembersRemoved) { if (channel->groupSelfContactRemoveInfo().isValid()) { // evaluate if we are leaving by our own or we are kicked History::InformationType type = History::InformationTypeSelfLeaving; if (channel->groupSelfContactRemoveInfo().hasReason()) { switch (channel->groupSelfContactRemoveInfo().reason()) { case ChannelGroupChangeReasonKicked: type = History::InformationTypeSelfKicked; break; // As ChannelGroupChangeReasonGone is not in telepathy, we need to ignore the warning #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wswitch" case ChannelGroupChangeReasonGone: type = History::InformationTypeGroupGone; break; #pragma GCC diagnostic pop default: break; } } if (thread[History::FieldChatRoomInfo].toMap()["Joined"].toBool()) { writeInformationEvent(thread, type); // update backend updateRoomProperties(channel, QVariantMap{{"Joined", false}}, true); } } else // don't notify any other group member removal if we are leaving the group { Q_FOREACH (const Tp::ContactPtr& contact, groupMembersRemoved) { // inform about removed members other than us if (contact->id() != channel->groupSelfContact()->id()) { writeInformationEvent(thread, History::InformationTypeLeaving, contact->alias(), QString(), QString(), true); } QVariantMap participant; participant[History::FieldIdentifier] = contact->id(); participant[History::FieldAlias] = contact->alias(); removed << participant; } } } mDBus.notifyThreadParticipantsChanged(thread, added, removed, QList()); } } updateRoomParticipants(channel, !selfContactIsPending); } void HistoryDaemon::updateRoomParticipants(const Tp::TextChannelPtr channel, bool notify) { if (!channel) { return; } QVariantList participants; QStringList contactsAdded; ChannelInterfaceRolesInterface *roles_interface = channel->optionalInterface(); RolesMap roles; if (roles_interface) { if (!mRolesMap.contains(channel->objectPath())) { roles = roles_interface->getRoles(); mRolesMap[channel->objectPath()] = roles; } else { roles = mRolesMap[channel->objectPath()]; } } Q_FOREACH(const Tp::ContactPtr contact, channel->groupRemotePendingContacts(false)) { QVariantMap participant; contactsAdded << contact->id(); participant[History::FieldIdentifier] = contact->id(); participant[History::FieldAlias] = contact->alias(); participant[History::FieldParticipantState] = History::ParticipantStateRemotePending; participant[History::FieldParticipantRoles] = roles[contact->handle().at(0)]; participants << QVariant::fromValue(participant); } Q_FOREACH(const Tp::ContactPtr contact, channel->groupLocalPendingContacts(false)) { QVariantMap participant; contactsAdded << contact->id(); participant[History::FieldIdentifier] = contact->id(); participant[History::FieldAlias] = contact->alias(); participant[History::FieldParticipantState] = History::ParticipantStateLocalPending; participant[History::FieldParticipantRoles] = roles[contact->handle().at(0)]; participants << QVariant::fromValue(participant); } Q_FOREACH(const Tp::ContactPtr contact, channel->groupContacts(false)) { // do not include remote and local pending members if (contactsAdded.contains(contact->id())) { continue; } QVariantMap participant; participant[History::FieldIdentifier] = contact->id(); participant[History::FieldAlias] = contact->alias(); participant[History::FieldParticipantState] = History::ParticipantStateRegular; participant[History::FieldParticipantRoles] = roles[contact->handle().at(0)]; participants << QVariant::fromValue(participant); } QString accountId = channel->property(History::FieldAccountId).toString(); QString threadId = channel->targetId(); if (mBackend->updateRoomParticipants(accountId, threadId, History::EventTypeText, participants)) { if (notify) { QVariantMap updatedThread = getSingleThread(History::EventTypeText, accountId, threadId, QVariantMap()); mDBus.notifyThreadsModified(QList() << updatedThread); } } } void HistoryDaemon::updateRoomRoles(const Tp::TextChannelPtr &channel, const RolesMap &rolesMap, bool notify) { if (!channel) { return; } QVariantMap participantsRoles; Q_FOREACH(const Tp::ContactPtr contact, channel->groupRemotePendingContacts(false)) { participantsRoles[contact->id()] = rolesMap[contact->handle().at(0)]; } Q_FOREACH(const Tp::ContactPtr contact, channel->groupLocalPendingContacts(false)) { participantsRoles[contact->id()] = rolesMap[contact->handle().at(0)]; } Q_FOREACH(const Tp::ContactPtr contact, channel->groupContacts(false)) { if (!participantsRoles.contains(contact->id())) { participantsRoles[contact->id()] = rolesMap[contact->handle().at(0)]; } } // update participants roles QString accountId = channel->property(History::FieldAccountId).toString(); QString threadId = channel->targetId(); if (mBackend->updateRoomParticipantsRoles(accountId, threadId, History::EventTypeText, participantsRoles)) { if (notify) { QVariantMap updatedThread = getSingleThread(History::EventTypeText, accountId, threadId, QVariantMap()); mDBus.notifyThreadsModified(QList() << updatedThread); } } // update self roles in room properties uint selfRoles = rolesMap[channel->groupSelfContact()->handle().at(0)]; updateRoomProperties(channel, QVariantMap{{"SelfRoles", selfRoles}}); } void HistoryDaemon::onRoomPropertiesChanged(const QVariantMap &properties,const QStringList &invalidated) { QString accountId = sender()->property(History::FieldAccountId).toString(); QString threadId = sender()->property(History::FieldThreadId).toString(); History::EventType type = (History::EventType)sender()->property(History::FieldType).toInt(); // get thread before updating to see if there are changes to insert as information events QVariantMap thread = getSingleThread(type, accountId, threadId, QVariantMap()); if (!thread.empty()) { writeRoomChangesInformationEvents(thread, properties); } updateRoomProperties(accountId, threadId, type, properties, invalidated); } void HistoryDaemon::updateRoomProperties(const Tp::TextChannelPtr &channel, const QVariantMap &properties, bool notify) { QString accountId = channel->property(History::FieldAccountId).toString(); QString threadId = channel->targetId(); History::EventType type = History::EventTypeText; updateRoomProperties(accountId, threadId, type, properties, QStringList(), notify); } void HistoryDaemon::updateRoomProperties(const QString &accountId, const QString &threadId, History::EventType type, const QVariantMap &properties, const QStringList &invalidated, bool notify) { if (mBackend->updateRoomInfo(accountId, threadId, type, properties, invalidated)) { if (notify) { QVariantMap thread = getSingleThread(type, accountId, threadId, QVariantMap()); mDBus.notifyThreadsModified(QList() << thread); } } } void HistoryDaemon::onMessageReceived(const Tp::TextChannelPtr textChannel, const Tp::ReceivedMessage &message) { QString eventId; QString senderId; QString accountId = textChannel->property(History::FieldAccountId).toString(); QString threadId = textChannel->property(History::FieldThreadId).toString(); QVariantMap properties = propertiesFromChannel(textChannel); if (threadId.isNull()) { threadId = threadIdForProperties(accountId, History::EventTypeText, properties, matchFlagsForChannel(textChannel), true); } History::MessageStatus status = History::MessageStatusUnknown; if (!message.sender() || message.sender()->handle().at(0) == textChannel->connection()->selfHandle()) { senderId = "self"; status = History::MessageStatusDelivered; } else { senderId = message.sender()->id(); } if (message.messageToken().isEmpty()) { eventId = QDateTime::currentDateTime().toString("yyyy-MM-ddTHH:mm:ss.zzz"); } else { eventId = message.messageToken(); } // ignore delivery reports for now. // FIXME: maybe we should set the readTimestamp when a delivery report is received if (message.isRescued()) { return; } if (message.isDeliveryReport() && message.deliveryDetails().hasOriginalToken()) { // at this point we assume the delivery report is for a message that was already // sent and properly saved at our database, so we can safely get it here to update QVariantMap textEvent = getSingleEvent(History::EventTypeText, accountId, threadId, message.deliveryDetails().originalToken()); if (textEvent.isEmpty()) { qWarning() << "Cound not find the original event to update with delivery details."; return; } // FIXME: if this message is already read, don't allow reverting the status. // we need to check if this is the right place to do it. if (textEvent[History::FieldMessageStatus].toInt() == History::MessageStatusRead) { qWarning() << "Skipping delivery report as it is trying to revert the Read status of an existing message to the following status:" << message.deliveryDetails().status(); return; } textEvent[History::FieldMessageStatus] = (int) fromTelepathyDeliveryStatus(message.deliveryDetails().status()); if (!writeEvents(QList() << textEvent, properties)) { qWarning() << "Failed to save the new message status!"; } return; } int count = 1; QList attachments; History::MessageType type = History::MessageTypeText; QString subject; if (message.hasNonTextContent()) { type = History::MessageTypeMultiPart; subject = message.header()["subject"].variant().toString(); Q_FOREACH(const Tp::MessagePart &part, message.parts()) { // ignore the header part if (part["content-type"].variant().toString().isEmpty()) { continue; } QString attachmentFilePath = History::MmsStorageManager::instance()->saveAttachment(part, accountId, threadId, eventId); if (attachmentFilePath.isNull()) { qWarning() << "Failed to save attachment for attachment id:" << part["identifier"].variant().toString(); continue; } QVariantMap attachment; attachment[History::FieldAccountId] = accountId; attachment[History::FieldThreadId] = threadId; attachment[History::FieldEventId] = eventId; attachment[History::FieldAttachmentId] = part["identifier"].variant(); attachment[History::FieldContentType] = part["content-type"].variant(); attachment[History::FieldFilePath] = attachmentFilePath; attachment[History::FieldStatus] = (int) History::AttachmentDownloaded; attachments << attachment; } } QString text = message.text(); if (message.deliveryDetails().isError()) { status = fromTelepathyDeliveryStatus(message.deliveryDetails().status()); text = message.deliveryDetails().debugMessage(); } QVariantMap event; event[History::FieldType] = History::EventTypeText; event[History::FieldAccountId] = accountId; event[History::FieldThreadId] = threadId; event[History::FieldEventId] = eventId; event[History::FieldSenderId] = senderId; event[History::FieldTimestamp] = message.received().toString("yyyy-MM-ddTHH:mm:ss.zzz"); event[History::FieldSentTime] = message.sent().toString("yyyy-MM-ddTHH:mm:ss.zzz"); event[History::FieldNewEvent] = true; // message is always unread until it reaches HistoryDaemon::onMessageRead event[History::FieldMessage] = text; event[History::FieldMessageType] = (int)type; event[History::FieldMessageStatus] = (int)status; event[History::FieldReadTimestamp] = QDateTime::currentDateTime().toString("yyyy-MM-ddTHH:mm:ss.zzz"); event[History::FieldSubject] = subject; event[History::FieldAttachments] = QVariant::fromValue(attachments); writeEvents(QList() << event, properties); // if this messages supersedes another one, remove the original message if (!message.supersededToken().isEmpty()) { event[History::FieldEventId] = message.supersededToken(); removeEvents(QList() << event); } } QVariantMap HistoryDaemon::getSingleEventFromTextChannel(const Tp::TextChannelPtr textChannel, const QString &messageId) { QVariantMap properties = propertiesFromChannel(textChannel); QVariantMap thread = threadForProperties(textChannel->property(History::FieldAccountId).toString(), History::EventTypeText, properties, matchFlagsForChannel(textChannel), false); if (thread.isEmpty()) { qWarning() << "Cound not find the thread related to this eventId."; return QVariantMap(); } QVariantMap textEvent = getSingleEvent((int)History::EventTypeText, textChannel->property(History::FieldAccountId).toString(), thread[History::FieldThreadId].toString(), messageId); return textEvent; } void HistoryDaemon::onMessageSent(const Tp::TextChannelPtr textChannel, const Tp::Message &message, const QString &messageToken) { QVariantMap properties = propertiesFromChannel(textChannel); QList attachments; History::MessageType type = History::MessageTypeText; int count = 1; QString subject; QString eventId; if (messageToken.isEmpty()) { eventId = QDateTime::currentDateTime().toString("yyyy-MM-ddTHH:mm:ss.zzz"); } else { eventId = messageToken; } QVariantMap thread = threadForProperties(textChannel->property(History::FieldAccountId).toString(), History::EventTypeText, properties, matchFlagsForChannel(textChannel), true); if (message.hasNonTextContent()) { type = History::MessageTypeMultiPart; subject = message.header()["subject"].variant().toString(); Q_FOREACH(const Tp::MessagePart &part, message.parts()) { // ignore the header part if (part["content-type"].variant().toString().isEmpty()) { continue; } QString attachmentFilePath = History::MmsStorageManager::instance()->saveAttachment(part, thread[History::FieldAccountId].toString(), thread[History::FieldThreadId].toString(), eventId); if (attachmentFilePath.isNull()) { qWarning() << "Failed to save attachment for attachment id:" << part["identifier"].variant().toString(); continue; } QVariantMap attachment; attachment[History::FieldAccountId] = thread[History::FieldAccountId]; attachment[History::FieldThreadId] = thread[History::FieldThreadId]; attachment[History::FieldEventId] = eventId; attachment[History::FieldAttachmentId] = part["identifier"].variant(); attachment[History::FieldContentType] = part["content-type"].variant(); attachment[History::FieldFilePath] = attachmentFilePath; attachment[History::FieldStatus] = (int) History::AttachmentDownloaded; attachments << attachment; } } QVariantMap event; event[History::FieldType] = History::EventTypeText; event[History::FieldAccountId] = thread[History::FieldAccountId]; event[History::FieldThreadId] = thread[History::FieldThreadId]; event[History::FieldEventId] = eventId; event[History::FieldSenderId] = "self"; event[History::FieldTimestamp] = QDateTime::currentDateTime().toString("yyyy-MM-ddTHH:mm:ss.zzz"); event[History::FieldSentTime] = QDateTime::currentDateTime().toString("yyyy-MM-ddTHH:mm:ss.zzz"); event[History::FieldNewEvent] = false; // outgoing messages are never new (unseen) event[History::FieldMessage] = message.text(); event[History::FieldMessageType] = type; if (textChannel->deliveryReportingSupport() & Tp::DeliveryReportingSupportFlagReceiveSuccesses) { event[History::FieldMessageStatus] = (int)History::MessageStatusUnknown; } else { event[History::FieldMessageStatus] = (int)History::MessageStatusAccepted; } event[History::FieldReadTimestamp] = QDateTime::currentDateTime().toString("yyyy-MM-ddTHH:mm:ss.zzz"); event[History::FieldSubject] = ""; event[History::FieldAttachments] = QVariant::fromValue(attachments); writeEvents(QList() << event, properties); } History::MatchFlags HistoryDaemon::matchFlagsForChannel(const Tp::ChannelPtr &channel) { QString protocol = channel->connection()->protocolName(); if (mProtocolFlags.contains(protocol)) { return mProtocolFlags[protocol]; } // default to this value return History::MatchCaseSensitive; } QString HistoryDaemon::hashThread(const QVariantMap &thread) { QString hash = QString::number(thread[History::FieldType].toInt()); hash += "#-#" + thread[History::FieldAccountId].toString(); hash += "#-#" + thread[History::FieldThreadId].toString(); return hash; } QVariantMap HistoryDaemon::getInterfaceProperties(const Tp::AbstractInterface *interface) { QDBusInterface propsInterface(interface->service(), interface->path(), "org.freedesktop.DBus.Properties"); QDBusReply reply = propsInterface.call("GetAll", interface->interface()); if (!reply.isValid()) { qWarning() << "Failed to fetch channel properties for interface" << interface->interface() << reply.error().message(); } return reply.value(); } void HistoryDaemon::writeInformationEvent(const QVariantMap &thread, History::InformationType type, const QString &subject, const QString &sender, const QString &text, bool notify) { History::TextEvent historyEvent = History::TextEvent(thread[History::FieldAccountId].toString(), thread[History::FieldThreadId].toString(), QString(QCryptographicHash::hash(QByteArray( (QDateTime::currentDateTime().toString("yyyy-MM-ddTHH:mm:ss.zzz") + subject + text).toLatin1()), QCryptographicHash::Md5).toHex()), sender, QDateTime::currentDateTime(), QDateTime::currentDateTime(), false, text, History::MessageTypeInformation, History::MessageStatusUnknown, QDateTime::currentDateTime(), subject, type); writeEvents(QList() << historyEvent.properties(), thread, notify); } void HistoryDaemon::writeRoomChangesInformationEvents(const QVariantMap &thread, const QVariantMap &interfaceProperties) { if (!thread.isEmpty()) { // group subject QString storedSubject = thread[History::FieldChatRoomInfo].toMap()["Subject"].toString(); QString newSubject = interfaceProperties["Subject"].toString(); if (!newSubject.isEmpty() && storedSubject != newSubject) { //see if we have an actor. If actor is 'me', we have changed that subject QString actor = thread[History::FieldChatRoomInfo].toMap()["Actor"].toString(); if (actor == "me") { actor = "self"; } writeInformationEvent(thread, History::InformationTypeTitleChanged, newSubject, actor); } } } void HistoryDaemon::writeRolesInformationEvents(const QVariantMap &thread, const Tp::ChannelPtr &channel, const RolesMap &rolesMap) { if (thread.isEmpty()) { return; } if (!thread[History::FieldChatRoomInfo].toMap()["Joined"].toBool()) { return; } // list of identifiers for current channel admins QStringList adminIds; Q_FOREACH(const Tp::ContactPtr contact, channel->groupContacts(false)) { // see if admin role (ChannelAdminRole == 2) if (rolesMap[contact->handle().at(0)] & AdminRole) { adminIds << contact->id(); } } Q_FOREACH (QVariant participant, thread[History::FieldParticipants].toList()) { QString participantId = participant.toMap()[History::FieldIdentifier].toString(); if (adminIds.contains(participantId)) { // see if already was admin or not (ChannelAdminRole == 2) if (! (participant.toMap()[History::FieldParticipantRoles].toUInt() & AdminRole)) { writeInformationEvent(thread, History::InformationTypeAdminGranted, participantId); } } } //evaluate now self roles if (rolesMap[channel->groupSelfContact()->handle().at(0)] & AdminRole) { uint selfRoles = thread[History::FieldChatRoomInfo].toMap()["SelfRoles"].toUInt(); if (! (selfRoles & AdminRole)) { writeInformationEvent(thread, History::InformationTypeSelfAdminGranted); } } } History::MessageStatus HistoryDaemon::fromTelepathyDeliveryStatus(Tp::DeliveryStatus deliveryStatus) { History::MessageStatus status = History::MessageStatusUnknown; switch (deliveryStatus) { case Tp::DeliveryStatusAccepted: status = History::MessageStatusAccepted; break; case Tp::DeliveryStatusDeleted: status = History::MessageStatusDeleted; break; case Tp::DeliveryStatusDelivered: status = History::MessageStatusDelivered; break; case Tp::DeliveryStatusPermanentlyFailed: status = History::MessageStatusPermanentlyFailed; break; case Tp::DeliveryStatusRead: status = History::MessageStatusRead; break; case Tp::DeliveryStatusTemporarilyFailed: status = History::MessageStatusTemporarilyFailed; break; case Tp::DeliveryStatusUnknown: status = History::MessageStatusUnknown; break; case Tp::_DeliveryStatusPadding: status = History::_MessageStatusPadding; break; } return status; } History::ChatType HistoryDaemon::fromTelepathyHandleType(const Tp::HandleType &type) { History::ChatType chatType = History::ChatTypeNone; switch(type) { case Tp::HandleTypeContact: chatType = History::ChatTypeContact; break; case Tp::HandleTypeNone: chatType = History::ChatTypeNone; break; case Tp::HandleTypeRoom: chatType = History::ChatTypeRoom; break; case Tp::HandleTypeGroup: chatType = History::ChatTypeGroup; break; case Tp::HandleTypeList: chatType = History::ChatTypeList; break; case Tp::_HandleTypePadding: chatType = History::_ChatTypePadding; break; } return chatType; } history-service-0.5/daemon/historydaemon.h000066400000000000000000000134711455450246700210400ustar00rootroot00000000000000/* * Copyright (C) 2013-2017 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 HISTORYDAEMON_H #define HISTORYDAEMON_H #include #include #include #include "types.h" #include "textchannelobserver.h" #include "callchannelobserver.h" #include "emblemcountmanager.h" #include "historyservicedbus.h" #include "plugin.h" #include "rolesinterface.h" typedef QMap RolesMap; class HistoryDaemon : public QObject { Q_OBJECT public: ~HistoryDaemon(); static HistoryDaemon *instance(); QVariantMap propertiesFromChannel(const Tp::ChannelPtr &textChannel); QVariantMap threadForProperties(const QString &accountId, History::EventType type, const QVariantMap &properties, History::MatchFlags matchFlags = History::MatchCaseSensitive, bool create = true); QString threadIdForProperties(const QString &accountId, History::EventType type, const QVariantMap &properties, History::MatchFlags matchFlags = History::MatchCaseSensitive, bool create = true); QList participantsForThreads(const QList &threadIds); QString queryThreads(int type, const QVariantMap &sort, const QVariantMap &filter, const QVariantMap &properties); QString queryEvents(int type, const QVariantMap &sort, const QVariantMap &filter); QVariantMap getSingleThread(int type, const QString &accountId, const QString &threadId, const QVariantMap &properties); QVariantMap getSingleEvent(int type, const QString &accountId, const QString &threadId, const QString &eventId); QVariantMap getSingleEventFromTextChannel(const Tp::TextChannelPtr textChannel, const QString &messageId); bool writeEvents(const QList &events, const QVariantMap &properties, bool notify = true); bool removeEvents(const QList &events); bool removeEvents(int type, const QVariantMap &filter, int &removedCount); int getEventsCount(int type, const QVariantMap &filter); int getUnreadCount(History::EventType type); bool removeThreads(const QList &threads); void markThreadsAsRead(const QList &threads); private Q_SLOTS: void onObserverCreated(); void onCallEnded(const Tp::CallChannelPtr &channel, bool missed); void onMessageReceived(const Tp::TextChannelPtr textChannel, const Tp::ReceivedMessage &message); void onMessageSent(const Tp::TextChannelPtr textChannel, const Tp::Message &message, const QString &messageToken); void onTextChannelAvailable(const Tp::TextChannelPtr channel); void onTextChannelInvalidated(const Tp::TextChannelPtr channel); void onRoomPropertiesChanged(const QVariantMap &properties,const QStringList &invalidated); void onGroupMembersChanged(const Tp::Contacts &groupMembersAdded, const Tp::Contacts &groupLocalPendingMembersAdded, const Tp::Contacts &groupRemotePendingMembersAdded, const Tp::Contacts &groupMembersRemoved, const Tp::Channel::GroupMemberChangeDetails &details); void onRolesChanged(const HandleRolesMap &added, const HandleRolesMap &removed); protected: History::MatchFlags matchFlagsForChannel(const Tp::ChannelPtr &channel); void updateRoomParticipants(const Tp::TextChannelPtr channel, bool notify = true); void updateRoomRoles(const Tp::TextChannelPtr &channel, const RolesMap &rolesMap, bool notify = true); QString hashThread(const QVariantMap &thread); static QVariantMap getInterfaceProperties(const Tp::AbstractInterface *interface); void updateRoomProperties(const Tp::TextChannelPtr &channel, const QVariantMap &properties, bool notify = true); void updateRoomProperties(const QString &accountId, const QString &threadId, History::EventType type, const QVariantMap &properties, const QStringList &invalidated, bool notify = true); void writeInformationEvent(const QVariantMap &thread, History::InformationType type, const QString &subject = QString(), const QString &sender = QString("self"), const QString &text = QString(), bool notify = true); void writeRoomChangesInformationEvents(const QVariantMap &thread, const QVariantMap &interfaceProperties); void writeRolesInformationEvents(const QVariantMap &thread, const Tp::ChannelPtr &channel, const RolesMap &rolesMap); void writeRolesChangesInformationEvents(const QVariantMap &thread, const Tp::ChannelPtr &channel, const RolesMap &rolesMap); static History::MessageStatus fromTelepathyDeliveryStatus(Tp::DeliveryStatus deliveryStatus); static History::ChatType fromTelepathyHandleType(const Tp::HandleType &type); private: HistoryDaemon(QObject *parent = 0); CallChannelObserver mCallObserver; TextChannelObserver mTextObserver; QMap mProtocolFlags; History::PluginPtr mBackend; HistoryServiceDBus mDBus; EmblemCountManager* mEmblemCountManager; QMap mRolesMap; }; #endif history-service-0.5/daemon/historyservicedbus.cpp000066400000000000000000000176171455450246700224540ustar00rootroot00000000000000/* * Copyright (C) 2013-2016 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 "historydaemon.h" #include "historyservicedbus.h" #include "historyserviceadaptor.h" #include "types.h" Q_DECLARE_METATYPE(QList< QVariantMap >) HistoryServiceDBus::HistoryServiceDBus(QObject *parent) : QObject(parent), mAdaptor(0), mSignalsTimer(-1) { qDBusRegisterMetaType >(); } bool HistoryServiceDBus::connectToBus() { if (!mAdaptor) { mAdaptor = new HistoryServiceAdaptor(this); } if (!QDBusConnection::sessionBus().registerObject(History::DBusObjectPath, this)) { return false; } return QDBusConnection::sessionBus().registerService(History::DBusService); } void HistoryServiceDBus::notifyThreadsAdded(const QList &threads) { mThreadsAdded << threads; triggerSignals(); } void HistoryServiceDBus::notifyThreadsModified(const QList &threads) { mThreadsModified << threads; triggerSignals(); } void HistoryServiceDBus::notifyThreadsRemoved(const QList &threads) { mThreadsRemoved << threads; triggerSignals(); } void HistoryServiceDBus::notifyEventsAdded(const QList &events) { mEventsAdded << events; triggerSignals(); } void HistoryServiceDBus::notifyEventsModified(const QList &events) { mEventsModified << events; triggerSignals(); } void HistoryServiceDBus::notifyEventsRemoved(const QList &events) { mEventsRemoved << events; triggerSignals(); } void HistoryServiceDBus::notifyThreadParticipantsChanged(const QVariantMap &thread, const QList &added, const QList &removed, const QList &modified) { Q_EMIT ThreadParticipantsChanged(thread, added, removed, modified); } QVariantMap HistoryServiceDBus::ThreadForProperties(const QString &accountId, int type, const QVariantMap &properties, int matchFlags, bool create) { return HistoryDaemon::instance()->threadForProperties(accountId, (History::EventType) type, properties, (History::MatchFlags) matchFlags, create); } QList HistoryServiceDBus::ParticipantsForThreads(const QList &threadIds) { return HistoryDaemon::instance()->participantsForThreads(threadIds); } QVariantMap HistoryServiceDBus::ThreadForParticipants(const QString &accountId, int type, const QStringList &participants, int matchFlags, bool create) { QVariantMap properties; properties[History::FieldParticipants] = participants; return HistoryDaemon::instance()->threadForProperties(accountId, (History::EventType) type, properties, (History::MatchFlags) matchFlags, create); } bool HistoryServiceDBus::WriteEvents(const QList &events) { return HistoryDaemon::instance()->writeEvents(events, QVariantMap()); } bool HistoryServiceDBus::RemoveThreads(const QList &threads) { return HistoryDaemon::instance()->removeThreads(threads); } void HistoryServiceDBus::MarkThreadsAsRead(const QList &threads) { return HistoryDaemon::instance()->markThreadsAsRead(threads); } int HistoryServiceDBus::GetEventsCount(int type, const QVariantMap &filter) { return HistoryDaemon::instance()->getEventsCount(type, filter); } bool HistoryServiceDBus::RemoveEvents(const QList &events) { return HistoryDaemon::instance()->removeEvents(events); } int HistoryServiceDBus::RemoveEventsBy(int type, const QVariantMap &filter) { int removedCount = 0; bool ok = HistoryDaemon::instance()->removeEvents(type, filter, removedCount); if (!ok) { sendErrorReply(QDBusError::InternalError, "Issue while removing events"); } return removedCount; } QString HistoryServiceDBus::QueryThreads(int type, const QVariantMap &sort, const QVariantMap &filter, const QVariantMap &properties) { return HistoryDaemon::instance()->queryThreads(type, sort, filter, properties); } QString HistoryServiceDBus::QueryEvents(int type, const QVariantMap &sort, const QVariantMap &filter) { return HistoryDaemon::instance()->queryEvents(type, sort, filter); } QVariantMap HistoryServiceDBus::GetSingleThread(int type, const QString &accountId, const QString &threadId, const QVariantMap &properties) { return HistoryDaemon::instance()->getSingleThread(type, accountId, threadId, properties); } QVariantMap HistoryServiceDBus::GetSingleEvent(int type, const QString &accountId, const QString &threadId, const QString &eventId) { return HistoryDaemon::instance()->getSingleEvent(type, accountId, threadId, eventId); } void HistoryServiceDBus::timerEvent(QTimerEvent *event) { if (event->timerId() == mSignalsTimer) { killTimer(mSignalsTimer); mSignalsTimer = -1; processSignals(); } } void HistoryServiceDBus::filterDuplicatesAndAdd(QList &targetList, const QList newItems, const QStringList &propertiesToCompare) { Q_FOREACH (const QVariantMap &item, newItems) { Q_FOREACH(const QVariantMap &existing, targetList) { bool found = true; Q_FOREACH(const QString &prop, propertiesToCompare) { if (item[prop] != existing[prop]) { found = false; break; } } if (!found) { targetList << item; } } } } void HistoryServiceDBus::triggerSignals() { if (mSignalsTimer >= 0) { killTimer(mSignalsTimer); } mSignalsTimer = startTimer(100); } void HistoryServiceDBus::processSignals() { if (!mThreadsAdded.isEmpty()) { Q_EMIT ThreadsAdded(mThreadsAdded); mThreadsAdded.clear(); } if (!mThreadsModified.isEmpty()) { Q_EMIT ThreadsModified(mThreadsModified); mThreadsModified.clear(); } if (!mThreadsRemoved.isEmpty()) { Q_EMIT ThreadsRemoved(mThreadsRemoved); mThreadsRemoved.clear(); } if (!mEventsAdded.isEmpty()) { Q_EMIT EventsAdded(mEventsAdded); mEventsAdded.clear(); } if (!mEventsModified.isEmpty()) { Q_EMIT EventsModified(mEventsModified); mEventsModified.clear(); } if (!mEventsRemoved.isEmpty()) { Q_EMIT EventsRemoved(mEventsRemoved); mEventsRemoved.clear(); } } history-service-0.5/daemon/historyservicedbus.h000066400000000000000000000106671455450246700221170ustar00rootroot00000000000000/* * Copyright (C) 2013-2016 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 HISTORYSERVICEDBUS_H #define HISTORYSERVICEDBUS_H #include #include #include "types.h" class HistoryServiceAdaptor; class HistoryServiceDBus : public QObject, public QDBusContext { Q_OBJECT public: explicit HistoryServiceDBus(QObject *parent = 0); bool connectToBus(); void notifyThreadsAdded(const QList &threads); void notifyThreadsModified(const QList &threads); void notifyThreadsRemoved(const QList &threads); void notifyThreadParticipantsChanged(const QVariantMap &thread, const QList &added, const QList &removed, const QList &modified); void notifyEventsAdded(const QList &events); void notifyEventsModified(const QList &events); void notifyEventsRemoved(const QList &events); // functions exposed on DBUS QVariantMap ThreadForParticipants(const QString &accountId, int type, const QStringList &participants, int matchFlags, bool create); QVariantMap ThreadForProperties(const QString &accountId, int type, const QVariantMap &properties, int matchFlags, bool create); QList ParticipantsForThreads(const QList &threadIds); bool WriteEvents(const QList &events); bool RemoveThreads(const QList &threads); bool RemoveEvents(const QList &events); int RemoveEventsBy(int type, const QVariantMap &filter); void MarkThreadsAsRead(const QList &threads); int GetEventsCount(int type, const QVariantMap &filter); // views QString QueryThreads(int type, const QVariantMap &sort, const QVariantMap &filter, const QVariantMap &properties); QString QueryEvents(int type, const QVariantMap &sort, const QVariantMap &filter); QVariantMap GetSingleThread(int type, const QString &accountId, const QString &threadId, const QVariantMap &properties); QVariantMap GetSingleEvent(int type, const QString &accountId, const QString &threadId, const QString &eventId); Q_SIGNALS: // signals that will be relayed into the bus void ThreadsAdded(const QList &threads); void ThreadsModified(const QList &threads); void ThreadsRemoved(const QList &threads); void ThreadParticipantsChanged(const QVariantMap &thread, const QList &added, const QList &removed, const QList &modified); void EventsAdded(const QList &events); void EventsModified(const QList &events); void EventsRemoved(const QList &events); protected: void timerEvent(QTimerEvent *event) override; protected Q_SLOTS: void filterDuplicatesAndAdd(QList &targetList, const QList newItems, const QStringList &propertiesToCompare); void triggerSignals(); void processSignals(); private: HistoryServiceAdaptor *mAdaptor; QList mThreadsAdded; QList mThreadsModified; QList mThreadsRemoved; QList mEventsAdded; QList mEventsModified; QList mEventsRemoved; int mSignalsTimer; }; #endif // HISTORYSERVICEDBUS_H history-service-0.5/daemon/main.cpp000066400000000000000000000032411455450246700174240ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 "historydaemon.h" #include #include bool checkApplicationRunning() { QString lockPath = qgetenv("HISTORY_LOCK_FILE"); if (lockPath.isEmpty()) { lockPath = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation); QDir dir(lockPath); if (!dir.exists("history-service") && !dir.mkpath("history-service")) { qCritical() << "Failed to create dir"; // in case we fail to create the lock, better not even start the application return true; } dir.cd("history-service"); lockPath = dir.absoluteFilePath("history-daemon.lock"); } static QLockFile *lockFile = new QLockFile(lockPath); return !lockFile->tryLock(); } int main(int argc, char **argv) { QCoreApplication app(argc, argv); if (checkApplicationRunning()) { return 1; } HistoryDaemon::instance(); return app.exec(); } history-service-0.5/daemon/org.freedesktop.Telepathy.Client.HistoryDaemonObserver.service.in000066400000000000000000000002461455450246700325350ustar00rootroot00000000000000[D-BUS Service] Name=org.freedesktop.Telepathy.Client.HistoryDaemonObserver Exec=@CMAKE_INSTALL_FULL_LIBEXECDIR@/history-daemon SystemdService=history-daemon.service history-service-0.5/daemon/pluginmanager.cpp000066400000000000000000000033061455450246700213330ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 "pluginmanager.h" #include "config.h" #include "plugin.h" #include #include #include namespace History { PluginManager::PluginManager(QObject *parent) : QObject(parent) { loadPlugins(); } PluginManager::~PluginManager() { } PluginManager *PluginManager::instance() { static PluginManager *self = new PluginManager(); return self; } Plugins PluginManager::plugins() { return mPlugins; } void PluginManager::loadPlugins() { QString pluginPath = qgetenv("HISTORY_PLUGIN_PATH"); if (pluginPath.isEmpty()) { pluginPath = qgetenv("SNAP") + HISTORY_PLUGIN_PATH; } QDir dir(pluginPath); Q_FOREACH (QString fileName, dir.entryList(QStringList() << "*.so", QDir::Files)) { QPluginLoader loader(dir.absoluteFilePath(fileName)); Plugin *plugin = qobject_cast(loader.instance()); if (plugin) { mPlugins.append(PluginPtr(plugin)); } } } } history-service-0.5/daemon/pluginmanager.h000066400000000000000000000022521455450246700207770ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 HISTORY_PLUGINMANAGER_H #define HISTORY_PLUGINMANAGER_H #include #include "types.h" namespace History { class Plugin; class PluginManager : public QObject { Q_OBJECT public: ~PluginManager(); static PluginManager *instance(); Plugins plugins(); protected: void loadPlugins(); private: explicit PluginManager(QObject *parent = 0); Plugins mPlugins; }; } #endif // HISTORY_PLUGINMANAGER_H history-service-0.5/daemon/rolesinterface.cpp000066400000000000000000000056341455450246700215150ustar00rootroot00000000000000/* * Copyright (C) 2016 Canonical, Ltd. * * Authors: * Roberto Mier Escandon * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 ChannelInterfaceRolesInterface::ChannelInterfaceRolesInterface(const QString& busName, const QString& objectPath, QObject *parent) : Tp::AbstractInterface(busName, objectPath, staticInterfaceName(), QDBusConnection::sessionBus(), parent) { } ChannelInterfaceRolesInterface::ChannelInterfaceRolesInterface(const QDBusConnection& connection, const QString& busName, const QString& objectPath, QObject *parent) : Tp::AbstractInterface(busName, objectPath, staticInterfaceName(), connection, parent) { } ChannelInterfaceRolesInterface::ChannelInterfaceRolesInterface(Tp::DBusProxy *proxy) : Tp::AbstractInterface(proxy, staticInterfaceName()) { } ChannelInterfaceRolesInterface::ChannelInterfaceRolesInterface(const Tp::Client::ChannelInterface& mainInterface) : Tp::AbstractInterface(mainInterface.service(), mainInterface.path(), staticInterfaceName(), mainInterface.connection(), mainInterface.parent()) { } ChannelInterfaceRolesInterface::ChannelInterfaceRolesInterface(const Tp::Client::ChannelInterface& mainInterface, QObject *parent) : Tp::AbstractInterface(mainInterface.service(), mainInterface.path(), staticInterfaceName(), mainInterface.connection(), parent) { } void ChannelInterfaceRolesInterface::invalidate(Tp::DBusProxy *proxy, const QString &error, const QString &message) { Tp::AbstractInterface::invalidate(proxy, error, message); } HandleRolesMap ChannelInterfaceRolesInterface::getRoles() const { QDBusMessage msg = QDBusMessage::createMethodCall(service(), path(), TP_QT_IFACE_PROPERTIES, QLatin1String("Get")); msg << interface() << QLatin1String("Roles"); QDBusMessage result = connection().call(msg); return qdbus_cast(result.arguments().at(0).value().variant()); } bool ChannelInterfaceRolesInterface::getCanUpdateRoles() const { QDBusMessage msg = QDBusMessage::createMethodCall(service(), path(), TP_QT_IFACE_PROPERTIES, QLatin1String("Get")); msg << interface() << QLatin1String("CanUpdateRoles"); QDBusMessage result = connection().call(msg); return qdbus_cast(result.arguments().at(0).value().variant()); } history-service-0.5/daemon/rolesinterface.h000066400000000000000000000150471455450246700211610ustar00rootroot00000000000000/* * Copyright (C) 2016 Canonical, Ltd. * * Authors: * Roberto Mier Escandon * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 CHANNELINTERFACEROLESINTERFACE_H #define CHANNELINTERFACEROLESINTERFACE_H #include #include #include /** * \struct HandleRolesMap * \ingroup mapping * \headerfile TelepathyQt/types.h * * Convertible with * QMap, but needed to have a discrete type in the Qt type system. * * A map from channel-specific handles to their owners. */ struct HandleRolesMap : public QMap { inline HandleRolesMap() : QMap() {} inline HandleRolesMap(const QMap& a) : QMap(a) {} inline HandleRolesMap& operator=(const QMap& a) { *(static_cast*>(this)) = a; return *this; } }; Q_DECLARE_METATYPE(HandleRolesMap) class ChannelInterfaceRolesInterface : public Tp::AbstractInterface { Q_OBJECT public: /** * Returns the name of the interface "org.freedesktop.Telepathy.Channel.Interface.Roles", which this class * represents. * * \return The D-Bus interface name. */ static inline QLatin1String staticInterfaceName() { return QLatin1String("org.freedesktop.Telepathy.Channel.Interface.Roles"); } /** * Creates a ChannelInterfaceRolesInterface associated with the given object on the session bus. * * \param busName Name of the service the object is on. * \param objectPath Path to the object on the service. * \param parent Passed to the parent class constructor. */ ChannelInterfaceRolesInterface( const QString& busName, const QString& objectPath, QObject* parent = 0 ); /** * Creates a ChannelInterfaceRolesInterface associated with the given object on the given bus. * * \param connection The bus via which the object can be reached. * \param busName Name of the service the object is on. * \param objectPath Path to the object on the service. * \param parent Passed to the parent class constructor. */ ChannelInterfaceRolesInterface( const QDBusConnection& connection, const QString& busName, const QString& objectPath, QObject* parent = 0 ); /** * Creates a ChannelInterfaceRolesInterface associated with the same object as the given proxy. * * \param proxy The proxy to use. It will also be the QObject::parent() * for this object. */ ChannelInterfaceRolesInterface(Tp::DBusProxy *proxy); /** * Creates a ChannelInterfaceRolesInterface associated with the same object as the given proxy. * Additionally, the created proxy will have the same parent as the given * proxy. * * \param mainInterface The proxy to use. */ explicit ChannelInterfaceRolesInterface(const Tp::Client::ChannelInterface& mainInterface); /** * Creates a ChannelInterfaceRolesInterface associated with the same object as the given proxy. * However, a different parent object can be specified. * * \param mainInterface The proxy to use. * \param parent Passed to the parent class constructor. */ ChannelInterfaceRolesInterface(const Tp::Client::ChannelInterface& mainInterface, QObject* parent); /** * Asynchronous getter for the remote object property \c Roles of type \c HandleRolesMap. * * \return A pending variant which will emit finished when the property has been * retrieved. */ inline Tp::PendingVariant *requestPropertyRoles() const { return internalRequestProperty(QLatin1String("Roles")); } /** * Asynchronous getter for the remote object property \c CanUpdateRoles of type \c bool. * * \return A pending variant which will emit finished when the property has been * retrieved. */ inline Tp::PendingVariant *requestPropertyCanUpdateRoles() const { return internalRequestProperty(QLatin1String("CanUpdateRoles")); } /** * Request all of the DBus properties on the interface. * * \return A pending variant map which will emit finished when the properties have * been retrieved. */ Tp::PendingVariantMap *requestAllProperties() const { return internalRequestAllProperties(); } /** * Synchronous version to get Roles property */ HandleRolesMap getRoles() const; /** * Synchronous version to get CanUpdateRoles property */ bool getCanUpdateRoles() const; public Q_SLOTS: /** * Begins a call to the D-Bus method \c UpdateRoles on the remote object. * * Update the roles in the server * */ inline QDBusPendingReply<> UpdateRoles(const HandleRolesMap &contactRoles, int timeout = -1) { if (!invalidationReason().isEmpty()) { return QDBusPendingReply<>(QDBusMessage::createError( invalidationReason(), invalidationMessage() )); } QDBusMessage callMessage = QDBusMessage::createMethodCall(this->service(), this->path(), this->staticInterfaceName(), QLatin1String("UpdateRoles")); callMessage << QVariant::fromValue(contactRoles); return this->connection().asyncCall(callMessage, timeout); } Q_SIGNALS: /** * Represents the signal \c RolesChanged on the remote object. * * Emitted when the state the roles of the channel has changed. * * \param added * * map of handles and related roles added * * \param removed * * map of handles and related roles removed */ void RolesChanged(const HandleRolesMap &added, const HandleRolesMap& removed); protected: virtual void invalidate(Tp::DBusProxy *, const QString &, const QString &); }; #endif // CHANNELINTERFACEROLESINTERFACE_H history-service-0.5/daemon/textchannelobserver.cpp000066400000000000000000000053021455450246700225650ustar00rootroot00000000000000/* * Copyright (C) 2012-2016 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 "textchannelobserver.h" #include #include TextChannelObserver::TextChannelObserver(QObject *parent) : QObject(parent) { } void TextChannelObserver::onTextChannelAvailable(Tp::TextChannelPtr textChannel) { connect(textChannel.data(), SIGNAL(invalidated(Tp::DBusProxy*,const QString&, const QString&)), SLOT(onTextChannelInvalidated())); connect(textChannel.data(), SIGNAL(messageReceived(const Tp::ReceivedMessage&)), SLOT(onMessageReceived(const Tp::ReceivedMessage&))); connect(textChannel.data(), SIGNAL(messageSent(Tp::Message,Tp::MessageSendingFlags,QString)), SLOT(onMessageSent(Tp::Message,Tp::MessageSendingFlags,QString))); Q_EMIT channelAvailable(textChannel); // process the messages that are already pending in the channel Q_FOREACH(const Tp::ReceivedMessage &message, textChannel->messageQueue()) { Q_EMIT messageReceived(textChannel, message); } mChannels.append(textChannel); } void TextChannelObserver::onTextChannelInvalidated() { Tp::TextChannelPtr textChannel(qobject_cast(sender())); mChannels.removeAll(textChannel); Q_EMIT textChannelInvalidated(textChannel); } void TextChannelObserver::onMessageReceived(const Tp::ReceivedMessage &message) { Tp::TextChannelPtr textChannel(qobject_cast(sender())); if (textChannel.isNull()) { return; } Q_EMIT messageReceived(textChannel, message); } void TextChannelObserver::onMessageSent(const Tp::Message &message, Tp::MessageSendingFlags /* flags */, const QString &sentMessageToken) { Tp::TextChannelPtr textChannel(qobject_cast(sender())); if (textChannel.isNull()) { return; } if (message.parts()[0]["skip-storage"].variant().toBool()) { return; } Q_EMIT messageSent(textChannel, message, sentMessageToken); } history-service-0.5/daemon/textchannelobserver.h000066400000000000000000000036371455450246700222430ustar00rootroot00000000000000/* * Copyright (C) 2012-2016 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 TEXTCHANNELOBSERVER_H #define TEXTCHANNELOBSERVER_H #include #include #include class TextChannelObserver : public QObject { Q_OBJECT public: explicit TextChannelObserver(QObject *parent = 0); public Q_SLOTS: void onTextChannelAvailable(Tp::TextChannelPtr textChannel); Q_SIGNALS: void channelAvailable(const Tp::TextChannelPtr textChannel); void textChannelInvalidated(const Tp::TextChannelPtr textChannel); void messageReceived(const Tp::TextChannelPtr textChannel, const Tp::ReceivedMessage &message); void messageSent(const Tp::TextChannelPtr textChannel, const Tp::Message &message, const QString &messageToken); protected: void showNotificationForMessage(const Tp::ReceivedMessage &message); Tp::TextChannelPtr channelFromPath(const QString &path); protected Q_SLOTS: void onTextChannelInvalidated(); void onMessageReceived(const Tp::ReceivedMessage &message); void onMessageSent(const Tp::Message &message, Tp::MessageSendingFlags flags, const QString &sentMessageToken); private: QList mChannels; }; #endif // TEXTCHANNELOBSERVER_H history-service-0.5/debian/000077500000000000000000000000001455450246700157535ustar00rootroot00000000000000history-service-0.5/debian/Jenkinsfile000066400000000000000000000001011455450246700201270ustar00rootroot00000000000000@Library('ubports-build-tools') _ buildAndProvideDebianPackage() history-service-0.5/debian/changelog000066400000000000000000000553541455450246700176410ustar00rootroot00000000000000history-service (0.5) unstable; urgency=medium * Upstream-provided Debian package for history-service. See upstream ChangeLog for recent changes. -- UBports developers Thu, 25 Jan 2024 16:58:47 +0100 history-service (0.4) unstable; urgency=medium [ Lionel Duboeuf ] * Manage ofono account and database mismatch * Be able to disable database Triggers for database maintenance * Remove attachments management * Store attachments with their file name * Add MMS tests [ UBports developers ] * Upstream-provided Debian package for history-service. See upstream ChangeLog for recent changes. -- UBports developers Fri, 13 Jan 2023 10:30:56 +0100 history-service (0.3+ubports6) UNRELEASED; urgency=medium * Rename qtdeclarative5-ubuntu-history0.1 to qml-module-lomiri-history0.1 [jEzEk] * Use status from delivery details if received message is an error message * Use debug message from delivery details as text if received message is an error message -- Guido Berhoerster Fri, 28 Jan 2022 11:36:59 +0100 history-service (0.3~0ubports5) focal; urgency=medium * Port to focal * Update to dh version 12 * Specify native package format * Remove dependency of qt5-default metapackage -- Guido Berhoerster Mon, 06 Dec 2021 13:39:59 +0100 history-service (0.3~0ubports4) xenial; urgency=medium * Revert use of sent date instead of the received one -- Lionel Duboeuf Tue, 09 Jul 2020 20:44:04 +0200 history-service (0.3~0ubports3) xenial; urgency=medium * Allow querying history db with NotEquals match flag -- Lionel Duboeuf Tue, 09 Jul 2020 20:44:04 +0200 history-service (0.3~0ubports2) xenial; urgency=medium * Add MessageStatusDraft Enum -- Lionel Duboeuf Tue, 31 Mar 2020 20:44:04 +0200 history-service (0.3~0ubports1) xenial; urgency=medium * debian/control: Allow building with crossbuilder * Update to new QtPim -- Alberto Mardegan Tue, 18 Sep 2018 20:44:04 +0300 history-service (0.2+0ubports2) xenial; urgency=medium * No change rebuild against new QtPim. -- Alberto Mardegan Wed, 25 Jul 2018 21:46:57 +0300 history-service (0.2+0ubports1) xenial; urgency=medium * Adapt to support VOIP accounts. * Improve the notifications of participants changing * Only start saving information events about contacts joining and leaving after the self contact is in the local list of participants. * Improve Roles management performance by caching the retrieved data. * Mark entire conversations as read. * Allow pass multiple fields on sort clause. * Reduce the dbus traffic when marking messages and threads as read. * Use a QLockFile to ensure there will be only one instance of the daemon per user. As we now delay the registration on dbus, sometimes we ended up having two instances of the daeon running (because of dbus activation). This change makes sure that won't happen. * Do not load the participants from threads automatically. If the client really needs it, it can use the newly added API to fetch the participants. * Make it possible to debug sqlite commands. -- Marius Gripsgard Tue, 06 Mar 2018 08:18:08 +0100 history-service (0.2+ubports) xenial; urgency=medium * Imported to UBports -- UBports auto importer Thu, 11 Jan 2018 10:22:35 +0000 history-service (0.1+17.04.20161130-0ubuntu1) zesty; urgency=medium [ Gustavo Pichorim Boiko ] * Improve group chat support. -- Tiago Salem Herrmann Wed, 30 Nov 2016 15:14:35 +0000 history-service (0.1+17.04.20161124.1-0ubuntu1) zesty; urgency=medium * Append "SNAP" prefix into the path to the plugins dir if none is set. -- Renato Araujo Oliveira Filho Thu, 24 Nov 2016 18:54:43 +0000 history-service (0.1+16.10.20160909.1-0ubuntu1) yakkety; urgency=medium * Temporarily disable DaemonTest until we get QtDBus fixed. -- Gustavo Pichorim Boiko Fri, 09 Sep 2016 14:53:42 +0000 history-service (0.1+16.04.20160425-0ubuntu1) xenial; urgency=medium * Make sure Telepathy types are always initialized. (LP: #1563050) -- Gustavo Pichorim Boiko Mon, 25 Apr 2016 21:03:28 +0000 history-service (0.1+16.04.20160104-0ubuntu1) xenial; urgency=medium [ Tiago Salem Herrmann ] * Do proper dbus demarshalling of detailProperties. (LP: #1510655) -- Bill Filler Mon, 04 Jan 2016 20:22:09 +0000 history-service (0.1+16.04.20151207-0ubuntu1) xenial; urgency=medium [ CI Train Bot ] * New rebuild forced. [ Tiago Salem Herrmann ] * Allow removing empty threads (LP: #1517602) * Fix build with new qt on xenial-proposed. * Mark messages as read on onMessageRead(). (LP: #1373059) -- Tiago Salem Herrmann Mon, 07 Dec 2015 17:55:02 +0000 history-service (0.1+16.04.20151103-0ubuntu1) xenial; urgency=medium [ Tiago Salem Herrmann ] * Enable cross compiling. -- Gustavo Pichorim Boiko Tue, 03 Nov 2015 14:26:16 +0000 history-service (0.1+15.10.20151020.1-0ubuntu1) wily; urgency=medium [ Gustavo Pichorim Boiko ] * Make it possible to use the custom functions defined by the sqlite plugin in the schema files themselves. * Move the classes from historyprivate static library to libhistoryservice itself and just keep their header files private. * Optimize the thread matching by avoiding some phone number formatting and validating. (LP: #1488988) * Request contact information for all known participants on history- daemon initialization, and use this cached information on the models. * Store threads' timestamps in UTC in the cache, and convert only when returning to the client, so that it has the most up-to-date local time. [ Tiago Salem Herrmann ] * Initial cache implementation for grouped conversations. * Set max count hint Don't use fallback to tel to avoid queries to the address-book-service with null filters -- CI Train Bot Tue, 20 Oct 2015 20:22:05 +0000 history-service (0.1+15.10.20150828.1-0ubuntu1) wily; urgency=medium [ Gustavo Pichorim Boiko ] * Do not override the build type. Also remove a block of unused code from debian/rules. * Fix flags. * Prevent the history-daemon from crashing in the event of an ended call not having any remote member. (LP: #1458990) [ Tiago Salem Herrmann ] * Correctly set mCanFetchMore in the HistoryGroupedThreadsModel. * Generate eventId when there is none Set status to "Accepted" if the channel does not support delivery reports * Manually add self id to participants list if needed. (LP: #1486187) -- CI Train Bot Fri, 28 Aug 2015 10:16:13 +0000 history-service (0.1+15.10.20150819.2-0ubuntu1) wily; urgency=medium [ Gustavo Pichorim Boiko ] * Fix tests on vivid. [ Tiago Salem Herrmann ] * Wait telepathy to be ready before querying address-book-service. (LP: #1485005) -- CI Train Bot Wed, 19 Aug 2015 22:29:24 +0000 history-service (0.1+15.10.20150810-0ubuntu1) wily; urgency=medium [ Tiago Salem Herrmann ] * Use libphonenumber for phone number validation, normalization and comparison. (LP: #1471545, #1473028) -- CI Train Bot Mon, 10 Aug 2015 21:06:23 +0000 history-service (0.1+15.10.20150701-0ubuntu2~gcc5.1) wily; urgency=medium * No-change test rebuild for g++5 ABI transition -- Steve Langasek Wed, 15 Jul 2015 08:01:07 +0000 history-service (0.1+15.10.20150701-0ubuntu1) wily; urgency=medium [ Tiago Salem Herrmann ] * Update to telepathy-qt 0.9.6.1. -- CI Train Bot Wed, 01 Jul 2015 14:15:57 +0000 history-service (0.1+15.04.20150428.1-0ubuntu1) vivid; urgency=medium [ Gustavo Pichorim Boiko ] * Fix a crash that was happening when matching new contacts. Also add a unit test to make sure it doesn't happen again. (LP: #1449662) -- CI Train Bot Tue, 28 Apr 2015 21:42:14 +0000 history-service (0.1+15.04.20150409-0ubuntu1) vivid; urgency=medium [ Gustavo Pichorim Boiko ] * Change the tests to use the same infrastructure as the one used in telephony-service, and add some tests for the contact matching changes. * Improve contact matching by accepting other addressable fields than just phone numbers. -- CI Train Bot Thu, 09 Apr 2015 19:06:12 +0000 history-service (0.1+15.04.20150210-0ubuntu1) vivid; urgency=medium [ Tiago Salem Herrmann ] * Add support for writing events from qml. Iterate over attachments when qdbus_cast fails. (LP: #1417353) -- Ubuntu daily release Tue, 10 Feb 2015 19:22:48 +0000 history-service (0.1+15.04.20150206.1-0ubuntu1) vivid; urgency=medium [ Tiago Salem Herrmann ] * Add tools to populate the history database. -- Ubuntu daily release Fri, 06 Feb 2015 15:26:43 +0000 history-service (0.1+15.04.20150128-0ubuntu1) vivid; urgency=low [ Gustavo Pichorim Boiko ] * Store the number that was used to call a given voice event. (LP: #1410500) * Use QSqlQuery::bindValue() to pass filter arguments to the query to prevent errors. -- Ubuntu daily release Wed, 28 Jan 2015 23:15:37 +0000 history-service (0.1+15.04.20150125-0ubuntu1) vivid; urgency=low [ Gustavo Pichorim Boiko ] * Make sure string filters are correctly escaped. -- Ubuntu daily release Sun, 25 Jan 2015 13:27:20 +0000 history-service (0.1+15.04.20150120-0ubuntu1) vivid; urgency=low [ Ubuntu daily release ] * New rebuild forced [ Gustavo Pichorim Boiko ] * Convert the filter to a string in the plugin itself and add support for the MatchContains match flag. (LP: #1376793) -- Ubuntu daily release Tue, 20 Jan 2015 21:03:27 +0000 history-service (0.1+15.04.20150106-0ubuntu1) vivid; urgency=low [ Gustavo Pichorim Boiko ] * Make it possible to delete multiple entries from QML. (LP: #1404286) -- Ubuntu daily release Tue, 06 Jan 2015 17:47:46 +0000 history-service (0.1+15.04.20141110-0ubuntu1) vivid; urgency=low [ Gustavo Pichorim Boiko ] * Save timestamps in UTC. (LP: #1379018) * Remove the tools and their dependencies: they are obsolete and not useful anymore. -- Ubuntu daily release Mon, 10 Nov 2014 13:23:06 +0000 history-service (0.1+14.10.20141003-0ubuntu1) 14.09; urgency=low [ Ubuntu daily release ] * New rebuild forced [ Tiago Salem Herrmann ] * Add support for a new message Type: MessageTypeInformation (LP: #1356950) -- Ubuntu daily release Fri, 03 Oct 2014 15:48:10 +0000 history-service (0.1+14.10.20140912-0ubuntu1) utopic; urgency=low [ Gustavo Pichorim Boiko ] * Add support for matching contact info in the data models. (LP: #1362387) * Keep the models sorted all the time, even when new events arrive. Also replace the thread grouping proxy model by a real model that groups threads. * If the models are used from QML, update the query right after the component setup is finished. [ Tiago Salem Herrmann ] * Create temporary tables in memory * Skip messages with the "skip-storage" flag set (LP: #1340255) -- Ubuntu daily release Fri, 12 Sep 2014 21:28:40 +0000 history-service (0.1+14.10.20140821.1-0ubuntu1) utopic; urgency=low [ Gustavo Pichorim Boiko ] * Add a model that groups events that have happened in a contiguous way. * Optimize the history-service by delaying the event and thread views on the model until the properties are all set. -- Ubuntu daily release Thu, 21 Aug 2014 19:03:32 +0000 history-service (0.1+14.10.20140813-0ubuntu1) utopic; urgency=low [ Ubuntu daily release ] * New rebuild forced [ Tiago Salem Herrmann ] * Export canFetchMore to qml [ Renato Araujo Oliveira Filho ] * Added "count" property for sortproxymodel; Added "get" function for sortproxymodel; -- Ubuntu daily release Wed, 13 Aug 2014 22:21:25 +0000 history-service (0.1+14.10.20140730-0ubuntu1) utopic; urgency=low [ Tiago Salem Herrmann ] * Fix unreadCount update. -- Ubuntu daily release Wed, 30 Jul 2014 17:52:20 +0000 history-service (0.1+14.10.20140725.1-0ubuntu1) utopic; urgency=low [ Tiago Salem Herrmann ] * fix attachments on thread queries. don't query the database for null filters. Apps should use an empty filter if they want to query the whole database. avoid performing queries when the filter is null. -- Ubuntu daily release Fri, 25 Jul 2014 22:18:44 +0000 history-service (0.1+14.10.20140724-0ubuntu1) utopic; urgency=low [ Tiago Salem Herrmann ] * Multiple fixes to HistoryThreadGroupingProxyModel: Update grouping also on onDataChanged(). use rowsAboutToBeRemoved() instead of rowsRemoved() to avoid crashes. Do not emit dataChanged for removed indexes. Set participants on groupForEntry() so phone comparison works . -- Ubuntu daily release Thu, 24 Jul 2014 02:16:30 +0000 history-service (0.1+14.10.20140717.1-0ubuntu1) utopic; urgency=low [ Gustavo Pichorim Boiko ] * Add a proxy model to allow grouping different threads into one. -- Ubuntu daily release Thu, 17 Jul 2014 18:15:11 +0000 history-service (0.1+14.10.20140701-0ubuntu1) utopic; urgency=low [ Tiago Salem Herrmann ] * Implement MMS sending support. -- Ubuntu daily release Tue, 01 Jul 2014 16:24:07 +0000 history-service (0.1+14.10.20140627-0ubuntu1) utopic; urgency=low [ Renato Araujo Oliveira Filho ] * Export HistoryEventModel.[canFetchMore | fetchMore] to QML -- Ubuntu daily release Fri, 27 Jun 2014 16:02:33 +0000 history-service (0.1+14.10.20140619-0ubuntu1) utopic; urgency=low [ Ken VanDine ] * Added count and at functions to HistoryEventModel to simplify getting single events from the model. [ Renato Araujo Oliveira Filho ] * Export property count and get to QML. -- Ubuntu daily release Thu, 19 Jun 2014 17:47:44 +0000 history-service (0.1+14.10.20140522-0ubuntu1) utopic; urgency=low [ Tiago Salem Herrmann ] * add support for removing event attachments. fix dbus demarshaling for attachments so they don't get deleted when the event is marked as read. . -- Ubuntu daily release Thu, 22 May 2014 21:44:24 +0000 history-service (0.1+14.04.20140407-0ubuntu1) trusty; urgency=low [ Tiago Salem Herrmann ] * Change observer filter to only receive non flash sms's. -- Ubuntu daily release Mon, 07 Apr 2014 23:02:58 +0000 history-service (0.1+14.04.20140326-0ubuntu1) trusty; urgency=low [ Gustavo Pichorim Boiko ] * Delay the loading of model data until after the model properties are set. -- Ubuntu daily release Wed, 26 Mar 2014 19:51:27 +0000 history-service (0.1+14.04.20140319-0ubuntu1) trusty; urgency=low [ Gustavo Pichorim Boiko ] * Make sure to only propagate events and threads of the correct type on views, even when the filter is null. -- Ubuntu daily release Wed, 19 Mar 2014 02:39:59 +0000 history-service (0.1+14.04.20140305-0ubuntu1) trusty; urgency=low * New rebuild forced -- Ubuntu daily release Wed, 05 Mar 2014 13:13:10 +0000 history-service (0.1+14.04.20140228-0ubuntu1) trusty; urgency=low [ Tiago Salem Herrmann ] * Fix broken test with qt5.2. (LP: #1285007) -- Ubuntu daily release Fri, 28 Feb 2014 23:22:36 +0000 history-service (0.1+14.04.20140217-0ubuntu1) trusty; urgency=low [ Tiago Salem Herrmann ] * History needs to also observe channels with TargetHandleType = 0 for group chat. Don't iterate if the amount of participants is different . -- Ubuntu daily release Mon, 17 Feb 2014 13:40:07 +0000 history-service (0.1+14.04.20140210-0ubuntu1) trusty; urgency=low [ Tiago Salem Herrmann ] * Check if the model is already empty before removing items in order to avoid an assert() in qt 5.2. (LP: #1275835) -- Ubuntu daily release Mon, 10 Feb 2014 16:02:49 +0000 history-service (0.1+14.04.20140130-0ubuntu1) trusty; urgency=low [ CI bot ] * Release history-service trunk in CITrain [ Gustavo Pichorim Boiko ] * Do not create threads if they are not already there when a delivery report arrives. -- Ubuntu daily release Thu, 30 Jan 2014 17:04:06 +0000 history-service (0.1+14.04.20140110-0ubuntu1) trusty; urgency=low [ Gustavo Pichorim Boiko ] * Rename the messageFlags field to messageStatus to reflect its real purpose. * Add tests for the Telepathy related code in history-service. * Handle the delivery reports and use the data to update the message status. [ Tiago Salem Herrmann ] * Declare delivery report enums to be used from QML. Update all existing text message status to "accepted". [ Ubuntu daily release ] * Automatic snapshot from revision 125 -- Ubuntu daily release Fri, 10 Jan 2014 04:41:34 +0000 history-service (0.1+14.04.20131209-0ubuntu1) trusty; urgency=low [ Gustavo Pichorim Boiko ] * Update the existing unit tests to improve the code coverage a bit. * Pass filters in a way that they can be recreated at the service end. * Add a pkgconfig file. * Fix calling History::Filter::toString() when the value is a boolean or a number. * Add tests for the sqlite plugin. * Add tests for the History::Manager class. * Add an initial set of tests for the thread and event views. [ Tiago Salem Herrmann ] * Accept non numeric id's. [ Ubuntu daily release ] * Automatic snapshot from revision 120 -- Ubuntu daily release Mon, 09 Dec 2013 04:18:48 +0000 history-service (0.1+13.10.20131016-0ubuntu1) saucy; urgency=low [ Gustavo Pichorim Boiko ] * Fix the duration saving on new call entries. They were being saved as QTime, but they should be actually saved as just the duration in seconds. [ Ubuntu daily release ] * Automatic snapshot from revision 111 -- Ubuntu daily release Wed, 16 Oct 2013 04:15:07 +0000 history-service (0.1+13.10.20131011-0ubuntu1) saucy; urgency=low [ Gustavo Pichorim Boiko ] * Fix passing the call duration via dbus. [ Ubuntu daily release ] * Automatic snapshot from revision 109 -- Ubuntu daily release Fri, 11 Oct 2013 08:47:23 +0000 history-service (0.1+13.10.20131008.1-0ubuntu1) saucy; urgency=low [ Gustavo Pichorim Boiko ] * Make sure the ThreadsModified signal is emitted with the threads up- to-date. (LP: #1234611) [ Ubuntu daily release ] * Automatic snapshot from revision 107 -- Ubuntu daily release Tue, 08 Oct 2013 04:18:37 +0000 history-service (0.1+13.10.20131001.4-0ubuntu1) saucy; urgency=low [ Gustavo Pichorim Boiko ] * Make sure the events emitted on signals contain the participants field. [ Ubuntu daily release ] * Automatic snapshot from revision 105 -- Ubuntu daily release Tue, 01 Oct 2013 21:20:42 +0000 history-service (0.1+13.10.20130927-0ubuntu1) saucy; urgency=low [ Gustavo Pichorim Boiko ] * Cache the threads to optimize the performance a bit when displaying events. * Add a participants field on event to avoid having to query threads for each and every event just to discover the participants. [ Ubuntu daily release ] * Automatic snapshot from revision 103 -- Ubuntu daily release Fri, 27 Sep 2013 05:41:17 +0000 history-service (0.1+13.10.20130925-0ubuntu1) saucy; urgency=low [ Gustavo Pichorim Boiko ] * Make it possible to modify events. * Set the Recover property on the telepathy client file to make sure that if the history daemon crashes it will get restarted by telepathy mission control. (LP: #1228446) * Clear the inconsistencies between thread_participants and threads on existing databases to make sure the service continues to work properly. (LP: #1228446) [ Ubuntu daily release ] * Automatic snapshot from revision 100 -- Ubuntu daily release Wed, 25 Sep 2013 06:35:24 +0000 history-service (0.1+13.10.20130919.3-0ubuntu1) saucy; urgency=low [ Gustavo Pichorim Boiko ] * Do not use shared pointers for events, threads, filters and sort. They were causing too much trouble for little benefit. [ Ubuntu daily release ] * Automatic snapshot from revision 96 -- Ubuntu daily release Thu, 19 Sep 2013 15:47:09 +0000 history-service (0.1+13.10.20130918.1-0ubuntu1) saucy; urgency=low [ Gustavo Pichorim Boiko ] * Use dbus for all the communication between the history service and its clients. Clients no longer access the sqlite database directly. [ Ubuntu daily release ] * Automatic snapshot from revision 94 -- Ubuntu daily release Wed, 18 Sep 2013 07:56:11 +0000 history-service (0.1+13.10.20130917-0ubuntu1) saucy; urgency=low [ Gustavo Pichorim Boiko ] * Add a missing build dependency on sqlite3 (required to generate the schema file). * Remove the Reader and Writer classes. All methods are implemented in the Plugin class directly to make it simpler. [ Tiago Salem Herrmann ] * Remove data from thread_participants when deleting threads. [ Ubuntu daily release ] * Automatic snapshot from revision 92 -- Ubuntu daily release Tue, 17 Sep 2013 12:57:10 +0000 history-service (0.1+13.10.20130827-0ubuntu1) saucy; urgency=low * Automatic snapshot from revision 87 -- Ubuntu daily release Tue, 27 Aug 2013 02:41:38 +0000 history-service (0.1+13.10.20130826.1-0ubuntu1) saucy; urgency=low [ Gustavo Pichorim Boiko ] * Initial release. [ Timo Jyrinki ] * Automatic snapshot from revision 85 (bootstrap) [ Ubuntu daily release ] * Automatic snapshot from revision 86 -- Ubuntu daily release Mon, 26 Aug 2013 19:14:36 +0000 history-service-0.5/debian/control000066400000000000000000000061731455450246700173650ustar00rootroot00000000000000Source: history-service Section: libs Priority: optional Maintainer: UBports developers Build-Depends: cmake, cmake-extras, dbus-test-runner, debhelper-compat (= 12), dh-translations, dconf-cli, gnome-keyring, libphonenumber-dev, libqt5sql5-sqlite, libsqlite3-dev, libtelepathy-qt5-dev, pkg-config, python:any, qtbase5-dev (>= 5.0), qtdeclarative5-dev (>= 5.0), # version 5.0~git... is not greater or equal 5.0, so leave it as 5 qtpim5-dev (>= 5), sqlite3, systemd, telepathy-mission-control-5, xvfb , Standards-Version: 3.9.4 Homepage: https://gitlab.com/ubports/core/history-service/ Vcs-Git: https://gitlab.com/ubports/core/history-service.git Vcs-Browser: https://gitlab.com/ubports/core/history-service/ Package: history-service Architecture: any Multi-Arch: foreign Depends: ${misc:Depends}, ${shlibs:Depends}, libqt5sql5-sqlite, Description: History service to store messages and calls A service to record the messages that are sent and received and the calls that are made in the Ubuntu Touch platform. . This package contains the history service daemon that watches for Telepathy events and stores. Package: history-service-tools Architecture: any Multi-Arch: foreign Section: utils Depends: history-service (= ${binary:Version}), ${misc:Depends}, ${shlibs:Depends}, Description: History service to store messages and calls - tools A service to record the messages that are sent and received and the calls that are made in the Ubuntu Touch platform. . This package contains some tools to be used with the history service. Package: libhistoryservice0 Architecture: any Multi-Arch: same Depends: ${misc:Depends}, ${shlibs:Depends}, Description: History service to store messages and calls - client library A service to record the messages that are sent and received and the calls that are made in the Ubuntu Touch platform. . This package contains the client library to access the data stored by the history service. Package: qml-module-lomiri-history0.1 Architecture: any Multi-Arch: same Depends: ${misc:Depends}, ${shlibs:Depends}, Description: History service to store messages and calls - QML plugin A service to record the messages that are sent and received and the calls that are made in the Ubuntu Touch platform. . This package contains a QML plugin that provides data models to access the data stored by the history service. Package: libhistoryservice-dev Architecture: any Multi-Arch: same Section: libdevel Depends: libhistoryservice0 (= ${binary:Version}), ${misc:Depends}, Description: History service to store messages and calls - development files A service to record the messages that are sent and received and the calls that are made in the Ubuntu Touch platform. . This package contains the development files for the history service client library. history-service-0.5/debian/copyright000066400000000000000000000034631455450246700177140ustar00rootroot00000000000000Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: history-service Source: https://launchpad.net/history-service Files: * Copyright: 2012, 2013 Canonical Ltd. License: GPL-3 Files: daemon/tests/mock/* Copyright: 2013 Canonical Ltd. License: LGPL-3 License: GPL-3 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, 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 . . On Debian systems, the full text of the GNU General Public License version 3 can be found in the file /usr/share/common-licenses/GPL-3. License: LGPL-3 This package is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, version 3 of the License. . This package 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 Lesser General Public License for more details. . You should have received a copy of the GNU Lesser General Public License along with this package; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA . On Debian systems, the complete text of the GNU Lesser General Public License can be found in `/usr/share/common-licenses/LGPL-3'. history-service-0.5/debian/history-service-tools.install000066400000000000000000000000771455450246700236440ustar00rootroot00000000000000usr/bin/history-makevoiceevents usr/bin/history-maketextevents history-service-0.5/debian/history-service.install000066400000000000000000000003361455450246700225040ustar00rootroot00000000000000usr/libexec/history-daemon usr/share/dbus-1/services/*HistoryDaemon*.service usr/share/dbus-1/services/*HistoryService*.service usr/share/telepathy/clients/HistoryDaemon*.client usr/lib/systemd/user/history-daemon.service history-service-0.5/debian/libhistoryservice-dev.install000066400000000000000000000001441455450246700236670ustar00rootroot00000000000000usr/include/history-service/* usr/lib/*/libhistoryservice.so usr/lib/*/pkgconfig/history-service.pc history-service-0.5/debian/libhistoryservice0.install000066400000000000000000000001361455450246700231740ustar00rootroot00000000000000usr/lib/*/history-service/plugins/libsqlitehistoryplugin.so usr/lib/*/libhistoryservice.so.0* history-service-0.5/debian/qml-module-lomiri-history0.1.install000066400000000000000000000001331455450246700246230ustar00rootroot00000000000000usr/lib/*/qt5/qml/Lomiri/History/libhistory-qml.so usr/lib/*/qt5/qml/Lomiri/History/qmldir history-service-0.5/debian/rules000077500000000000000000000005601455450246700170340ustar00rootroot00000000000000#!/usr/bin/make -f # -*- makefile -*- # Uncomment this to turn on verbose mode. #export DH_VERBOSE=1 export XDG_DATA_HOME=/tmp override_dh_missing: dh_missing --fail-missing %: dh $@ --parallel --with translations override_dh_auto_test: ifeq ($(DEB_BUILD_GNU_TYPE),$(DEB_HOST_GNU_TYPE)) CTEST_OUTPUT_ON_FAILURE=1 make -j1 -C obj-$(DEB_HOST_GNU_TYPE) test endif history-service-0.5/debian/source/000077500000000000000000000000001455450246700172535ustar00rootroot00000000000000history-service-0.5/debian/source/format000066400000000000000000000000141455450246700204610ustar00rootroot000000000000003.0 (native)history-service-0.5/plugins/000077500000000000000000000000001455450246700162125ustar00rootroot00000000000000history-service-0.5/plugins/CMakeLists.txt000066400000000000000000000000311455450246700207440ustar00rootroot00000000000000add_subdirectory(sqlite) history-service-0.5/plugins/sqlite/000077500000000000000000000000001455450246700175135ustar00rootroot00000000000000history-service-0.5/plugins/sqlite/CMakeLists.txt000066400000000000000000000024331455450246700222550ustar00rootroot00000000000000# SQLite Plugin set(plugin_SRCS sqlitedatabase.cpp sqlitehistoryeventview.cpp sqlitehistorythreadview.cpp sqlitehistoryplugin.cpp ) set (plugin_HDRS sqlitedatabase.h sqlitehistoryeventview.h sqlitehistorythreadview.h sqlitehistoryplugin.h ) include_directories( ${CMAKE_SOURCE_DIR}/src ${SQLITE3_INCLUDE_DIRS} ) qt5_add_resources(plugin_RES sqlitehistoryplugin.qrc) add_library(sqlitehistoryplugin SHARED ${plugin_SRCS} ${plugin_HDRS} ${plugin_RES}) qt5_use_modules(sqlitehistoryplugin Core DBus Sql) # update the .qrc file automatically when there are new schema files file(GLOB QRC_RESOURCE_FILES ${CMAKE_CURRENT_SOURCE_DIR}/schema/*.sql ${CMAKE_CURRENT_SOURCE_DIR}/schema/*.info) set(QRC_FILE ${CMAKE_CURRENT_SOURCE_DIR}/sqlitehistoryplugin.qrc) add_custom_command( OUTPUT ${QRC_FILE} COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/update_qrc.sh ${QRC_FILE} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} DEPENDS ${QRC_RESOURCE_FILES} ) add_custom_target(qrc_update DEPENDS ${QRC_FILE} schema_update) add_dependencies(sqlitehistoryplugin schema_update qrc_update) target_link_libraries(sqlitehistoryplugin historyservice ${SQLITE3_LIBRARIES}) install(TARGETS sqlitehistoryplugin DESTINATION ${HISTORY_PLUGIN_PATH}) add_subdirectory(schema) history-service-0.5/plugins/sqlite/schema/000077500000000000000000000000001455450246700207535ustar00rootroot00000000000000history-service-0.5/plugins/sqlite/schema/CMakeLists.txt000066400000000000000000000015731455450246700235210ustar00rootroot00000000000000file(GLOB SCHEMA_FILES ${CMAKE_CURRENT_SOURCE_DIR}/v*.sql) set(SCHEMA_FILE ${CMAKE_CURRENT_SOURCE_DIR}/schema.sql) set(VERSION_FILE ${CMAKE_CURRENT_SOURCE_DIR}/version.info) configure_file(update_schema.sh.in ${CMAKE_CURRENT_BINARY_DIR}/update_schema.sh @ONLY) add_custom_command( OUTPUT ${SCHEMA_FILE} ${VERSION_FILE} COMMAND ${CMAKE_CURRENT_BINARY_DIR}/update_schema.sh ${CMAKE_CURRENT_SOURCE_DIR} ${SCHEMA_FILE} ${VERSION_FILE} WORKING DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} DEPENDS ${SCHEMA_FILES} generate_schema ) include_directories(${CMAKE_CURRENT_SOURCE_DIR}/..) add_executable(generate_schema generate_schema.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../sqlitedatabase.cpp) qt5_use_modules(generate_schema Core DBus Sql) target_link_libraries(generate_schema historyservice ${SQLITE3_LIBRARIES}) add_custom_target(schema_update DEPENDS ${SCHEMA_FILE} ${VERSION_FILE}) history-service-0.5/plugins/sqlite/schema/generate_schema.cpp000066400000000000000000000037711455450246700246010ustar00rootroot00000000000000/* * Copyright (C) 2015 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 #include "sqlitedatabase.h" #include #include int main(int argc, char** argv) { QCoreApplication app(argc, argv); // force using a memory database setenv("HISTORY_SQLITE_DBPATH", ":memory:", 1); QStringList schemaFiles = app.arguments(); // take argv[0] out of the list schemaFiles.takeFirst(); // and store the target file too QString targetFile = schemaFiles.takeLast(); qDebug() << "**************************************************************"; qDebug() << "Starting the schema file generation process"; qDebug() << "Note: the failures seen below are known, expected and harmless"; qDebug() << "in this context."; QStringList statements; Q_FOREACH(const QString &file, schemaFiles) { statements << SQLiteDatabase::instance()->parseSchemaFile(file); } if (SQLiteDatabase::instance()->runMultipleStatements(statements)) { QFile file(targetFile); if (!file.open(QIODevice::WriteOnly)) { return 1; } file.write(SQLiteDatabase::instance()->dumpSchema().toUtf8().data()); file.close(); qDebug() << "**************************************************************"; return 0; } return 1; } history-service-0.5/plugins/sqlite/schema/update_schema.sh.in000077500000000000000000000010321455450246700245150ustar00rootroot00000000000000#!/bin/sh if [ $# -lt 3 ]; then echo "Usage: $0 " fi SOURCE_DIR=$1 TARGET_FILE=$2 VERSION_FILE=$3 VERSION="1" LATEST_VERSION="1" SCHEMA_FILES="" SCHEMA_FILE="$SOURCE_DIR/v${VERSION}.sql" while [ -e $SCHEMA_FILE ]; do SCHEMA_FILES="$SCHEMA_FILES $SCHEMA_FILE" LATEST_VERSION=$VERSION VERSION=$(($VERSION+1)) SCHEMA_FILE="$SOURCE_DIR/v${VERSION}.sql" done @CMAKE_CURRENT_BINARY_DIR@/generate_schema $SCHEMA_FILES $TARGET_FILE echo $LATEST_VERSION > $VERSION_FILE history-service-0.5/plugins/sqlite/schema/v1.sql000066400000000000000000000162651455450246700220340ustar00rootroot00000000000000CREATE TABLE schema_version ( version int ); CREATE TABLE threads ( accountId varchar(255), threadId varchar(255), type tinyint, lastEventId varchar(255), lastEventTimestamp datetime, count int, unreadCount int ); CREATE TABLE thread_participants ( accountId varchar(255), threadId varchar(255), type tinyint, participantId varchar(255) ); CREATE TABLE voice_events ( accountId varchar(255), threadId varchar(255), eventId varchar(255), senderId varchar(255), timestamp datetime, newEvent bool, duration int, missed bool ); CREATE TABLE text_events ( accountId varchar(255), threadId varchar(255), eventId varchar(255), senderId varchar(255), timestamp datetime, newEvent bool, message varchar(512), messageType tinyint, messageFlags tinyint, readTimestamp datetime ); CREATE TRIGGER voice_events_insert_trigger AFTER INSERT ON voice_events FOR EACH ROW BEGIN UPDATE threads SET count=(SELECT count(eventId) FROM voice_events WHERE accountId=new.accountId AND threadId=new.threadId) WHERE accountId=new.accountId AND threadId=new.threadId AND type=1; UPDATE threads SET unreadCount=(SELECT count(eventId) FROM voice_events WHERE accountId=new.accountId AND threadId=new.threadId AND newEvent='true') WHERE accountId=new.accountId AND threadId=new.threadId AND type=1; UPDATE threads SET lastEventId=(SELECT eventId FROM voice_events WHERE accountId=new.accountId AND threadId=new.threadId ORDER BY timestamp DESC LIMIT 1) WHERE accountId=new.accountId AND threadId=new.threadId AND type=1; UPDATE threads SET lastEventTimestamp=(SELECT timestamp FROM voice_events WHERE accountId=new.accountId AND threadId=new.threadId ORDER BY timestamp DESC LIMIT 1) WHERE accountId=new.accountId AND threadId=new.threadId AND type=1; END; CREATE TRIGGER voice_events_update_trigger AFTER UPDATE ON voice_events FOR EACH ROW BEGIN UPDATE threads SET count=(SELECT count(eventId) FROM voice_events WHERE accountId=new.accountId AND threadId=new.threadId) WHERE accountId=new.accountId AND threadId=new.threadId AND type=1; UPDATE threads SET unreadCount=(SELECT count(eventId) FROM voice_events WHERE accountId=new.accountId AND threadId=new.threadId AND newEvent='true') WHERE accountId=new.accountId AND threadId=new.threadId AND type=1; UPDATE threads SET lastEventId=(SELECT eventId FROM voice_events WHERE accountId=new.accountId AND threadId=new.threadId ORDER BY timestamp DESC LIMIT 1) WHERE accountId=new.accountId AND threadId=new.threadId AND type=1; UPDATE threads SET lastEventTimestamp=(SELECT timestamp FROM voice_events WHERE accountId=new.accountId AND threadId=new.threadId ORDER BY timestamp DESC LIMIT 1) WHERE accountId=new.accountId AND threadId=new.threadId AND type=1; END; CREATE TRIGGER voice_events_delete_trigger AFTER DELETE ON voice_events FOR EACH ROW BEGIN UPDATE threads SET count=(SELECT count(eventId) FROM voice_events WHERE accountId=new.accountId AND threadId=new.threadId) WHERE accountId=new.accountId AND threadId=new.threadId AND type=1; UPDATE threads SET unreadCount=(SELECT count(eventId) FROM voice_events WHERE accountId=new.accountId AND threadId=new.threadId AND newEvent='true') WHERE accountId=new.accountId AND threadId=new.threadId AND type=1; UPDATE threads SET lastEventId=(SELECT eventId FROM voice_events WHERE accountId=new.accountId AND threadId=new.threadId ORDER BY timestamp DESC LIMIT 1) WHERE accountId=new.accountId AND threadId=new.threadId AND type=1; UPDATE threads SET lastEventTimestamp=(SELECT timestamp FROM voice_events WHERE accountId=new.accountId AND threadId=new.threadId ORDER BY timestamp DESC LIMIT 1) WHERE accountId=new.accountId AND threadId=new.threadId AND type=1; END; CREATE TRIGGER text_events_insert_trigger AFTER INSERT ON text_events FOR EACH ROW BEGIN UPDATE threads SET count=(SELECT count(eventId) FROM text_events WHERE accountId=new.accountId AND threadId=new.threadId) WHERE accountId=new.accountId AND threadId=new.threadId AND type=0; UPDATE threads SET unreadCount=(SELECT count(eventId) FROM text_events WHERE accountId=new.accountId AND threadId=new.threadId AND newEvent='true') WHERE accountId=new.accountId AND threadId=new.threadId AND type=0; UPDATE threads SET lastEventId=(SELECT eventId FROM text_events WHERE accountId=new.accountId AND threadId=new.threadId ORDER BY timestamp DESC LIMIT 1) WHERE accountId=new.accountId AND threadId=new.threadId AND type=0; UPDATE threads SET lastEventTimestamp=(SELECT timestamp FROM text_events WHERE accountId=new.accountId AND threadId=new.threadId ORDER BY timestamp DESC LIMIT 1) WHERE accountId=new.accountId AND threadId=new.threadId AND type=0; END; CREATE TRIGGER text_events_update_trigger AFTER UPDATE ON text_events FOR EACH ROW BEGIN UPDATE threads SET count=(SELECT count(eventId) FROM text_events WHERE accountId=new.accountId AND threadId=new.threadId) WHERE accountId=new.accountId AND threadId=new.threadId AND type=0; UPDATE threads SET unreadCount=(SELECT count(eventId) FROM text_events WHERE accountId=new.accountId AND threadId=new.threadId AND newEvent='true') WHERE accountId=new.accountId AND threadId=new.threadId AND type=0; UPDATE threads SET lastEventId=(SELECT eventId FROM text_events WHERE accountId=new.accountId AND threadId=new.threadId ORDER BY timestamp DESC LIMIT 1) WHERE accountId=new.accountId AND threadId=new.threadId AND type=0; UPDATE threads SET lastEventTimestamp=(SELECT timestamp FROM text_events WHERE accountId=new.accountId AND threadId=new.threadId ORDER BY timestamp DESC LIMIT 1) WHERE accountId=new.accountId AND threadId=new.threadId AND type=0; END; CREATE TRIGGER text_events_delete_trigger AFTER DELETE ON text_events FOR EACH ROW BEGIN UPDATE threads SET count=(SELECT count(eventId) FROM text_events WHERE accountId=new.accountId AND threadId=new.threadId) WHERE accountId=new.accountId AND threadId=new.threadId AND type=0; UPDATE threads SET unreadCount=(SELECT count(eventId) FROM text_events WHERE accountId=new.accountId AND threadId=new.threadId AND newEvent='true') WHERE accountId=new.accountId AND threadId=new.threadId AND type=0; UPDATE threads SET lastEventId=(SELECT eventId FROM text_events WHERE accountId=new.accountId AND threadId=new.threadId ORDER BY timestamp DESC LIMIT 1) WHERE accountId=new.accountId AND threadId=new.threadId AND type=0; UPDATE threads SET lastEventTimestamp=(SELECT timestamp FROM text_events WHERE accountId=new.accountId AND threadId=new.threadId ORDER BY timestamp DESC LIMIT 1) WHERE accountId=new.accountId AND threadId=new.threadId AND type=0; END; history-service-0.5/plugins/sqlite/schema/v10.sql000066400000000000000000000000431455450246700220770ustar00rootroot00000000000000SELECT * FROM text_events LIMIT 1; history-service-0.5/plugins/sqlite/schema/v11.sql000066400000000000000000000007521455450246700221070ustar00rootroot00000000000000ALTER TABLE voice_events ADD COLUMN remoteParticipant varchar(255); UPDATE voice_events SET remoteParticipant=(SELECT participantId FROM thread_participants WHERE thread_participants.accountId = voice_events.accountId AND thread_participants.threadId = voice_events.threadId AND thread_participants.type = 1 LIMIT 1); history-service-0.5/plugins/sqlite/schema/v12.sql000066400000000000000000000002331455450246700221020ustar00rootroot00000000000000ALTER TABLE thread_participants ADD COLUMN normalizedId varchar(255); UPDATE thread_participants SET normalizedId = normalizeId(accountId, participantId); history-service-0.5/plugins/sqlite/schema/v13.sql000066400000000000000000000027221455450246700221100ustar00rootroot00000000000000ALTER TABLE threads ADD COLUMN chatType tinyint; ALTER TABLE thread_participants ADD COLUMN alias varchar(255); ALTER TABLE thread_participants ADD COLUMN state tinyint; CREATE TABLE chat_room_info ( accountId varchar(255), type tinyint, threadId varchar(255), roomName varchar(255), server varchar(255), creator varchar(255), creationTimestamp datetime, anonymous boolean, inviteOnly boolean, participantLimit integer, moderated boolean, title varchar(1024), description varchar(1024), persistent boolean, private boolean, passwordProtected boolean, password varchar(512), passwordHint varchar(512), canUpdateConfiguration boolean, subject varchar(1024), actor varchar(512), timestamp datetime ); UPDATE threads SET chatType = 0; UPDATE threads SET chatType=1 WHERE (SELECT COUNT(participantId) from thread_participants WHERE thread_participants.threadId=threads.threadId and thread_participants.accountId=threads.accountId AND thread_participants.type=threads.type)=1; UPDATE thread_participants SET state = 0; DROP TRIGGER threads_delete_trigger; CREATE TRIGGER threads_delete_trigger AFTER DELETE ON threads FOR EACH ROW BEGIN DELETE FROM thread_participants WHERE accountId=old.accountId AND threadId=old.threadId AND type=old.type; DELETE FROM chat_room_info WHERE accountId=old.accountId AND threadId=old.threadId AND type=old.type; END; history-service-0.5/plugins/sqlite/schema/v14.sql000066400000000000000000000074061455450246700221150ustar00rootroot00000000000000DROP TRIGGER text_events_insert_trigger; CREATE TRIGGER text_events_insert_trigger AFTER INSERT ON text_events FOR EACH ROW WHEN new.messageType!=2 BEGIN UPDATE threads SET count=(SELECT count(eventId) FROM text_events WHERE accountId=new.accountId AND threadId=new.threadId AND messageType!=2) WHERE accountId=new.accountId AND threadId=new.threadId AND type=0; UPDATE threads SET unreadCount=(SELECT count(eventId) FROM text_events WHERE accountId=new.accountId AND threadId=new.threadId AND newEvent='1' AND messageType!=2) WHERE accountId=new.accountId AND threadId=new.threadId AND type=0; UPDATE threads SET lastEventId=(SELECT eventId FROM text_events WHERE accountId=new.accountId AND threadId=new.threadId AND messageType!=2 ORDER BY timestamp DESC LIMIT 1) WHERE accountId=new.accountId AND threadId=new.threadId AND type=0; UPDATE threads SET lastEventTimestamp=(SELECT timestamp FROM text_events WHERE accountId=new.accountId AND threadId=new.threadId AND messageType!=2 ORDER BY timestamp DESC LIMIT 1) WHERE accountId=new.accountId AND threadId=new.threadId AND type=0; END; DROP TRIGGER text_events_update_trigger; CREATE TRIGGER text_events_update_trigger AFTER UPDATE ON text_events FOR EACH ROW WHEN new.messageType!=2 BEGIN UPDATE threads SET count=(SELECT count(eventId) FROM text_events WHERE accountId=new.accountId AND threadId=new.threadId AND messageType!=2) WHERE accountId=new.accountId AND threadId=new.threadId AND type=0; UPDATE threads SET unreadCount=(SELECT count(eventId) FROM text_events WHERE accountId=new.accountId AND threadId=new.threadId AND newEvent='1' AND messageType!=2) WHERE accountId=new.accountId AND threadId=new.threadId AND type=0; UPDATE threads SET lastEventId=(SELECT eventId FROM text_events WHERE accountId=new.accountId AND threadId=new.threadId AND messageType!=2 ORDER BY timestamp DESC LIMIT 1) WHERE accountId=new.accountId AND threadId=new.threadId AND type=0; UPDATE threads SET lastEventTimestamp=(SELECT timestamp FROM text_events WHERE accountId=new.accountId AND threadId=new.threadId AND messageType!=2 ORDER BY timestamp DESC LIMIT 1) WHERE accountId=new.accountId AND threadId=new.threadId AND type=0; END; DROP TRIGGER text_events_delete_trigger; CREATE TRIGGER text_events_delete_trigger AFTER DELETE ON text_events FOR EACH ROW WHEN old.messageType!=2 BEGIN UPDATE threads SET count=(SELECT count(eventId) FROM text_events WHERE accountId=old.accountId AND threadId=old.threadId AND messageType!=2) WHERE accountId=old.accountId AND threadId=old.threadId AND type=0; UPDATE threads SET unreadCount=(SELECT count(eventId) FROM text_events WHERE accountId=old.accountId AND threadId=old.threadId AND newEvent='1' AND messageType!=2) WHERE accountId=old.accountId AND threadId=old.threadId AND type=0; UPDATE threads SET lastEventId=(SELECT eventId FROM text_events WHERE accountId=old.accountId AND threadId=old.threadId AND messageType!=2 ORDER BY timestamp DESC LIMIT 1) WHERE accountId=old.accountId AND threadId=old.threadId AND type=0; UPDATE threads SET lastEventTimestamp=(SELECT timestamp FROM text_events WHERE accountId=old.accountId AND threadId=old.threadId AND messageType!=2 ORDER BY timestamp DESC LIMIT 1) WHERE accountId=old.accountId AND threadId=old.threadId AND type=0; DELETE from text_event_attachments WHERE accountId=old.accountId AND threadId=old.threadId AND eventId=old.eventId; END; history-service-0.5/plugins/sqlite/schema/v15.sql000066400000000000000000000000721455450246700221060ustar00rootroot00000000000000ALTER TABLE thread_participants ADD COLUMN roles tinyint; history-service-0.5/plugins/sqlite/schema/v16.sql000066400000000000000000000001571455450246700221130ustar00rootroot00000000000000ALTER TABLE chat_room_info ADD COLUMN joined boolean; ALTER TABLE chat_room_info ADD COLUMN selfRoles integer; history-service-0.5/plugins/sqlite/schema/v17.sql000066400000000000000000000001501455450246700221050ustar00rootroot00000000000000ALTER TABLE text_events ADD COLUMN informationType integer; UPDATE text_events SET informationType = 1; history-service-0.5/plugins/sqlite/schema/v18.sql000066400000000000000000000006761455450246700221230ustar00rootroot00000000000000CREATE TRIGGER IF NOT EXISTS text_threads_delete_trigger AFTER DELETE ON threads FOR EACH ROW WHEN old.type=0 BEGIN DELETE FROM text_events WHERE accountId=old.accountId AND threadId=old.threadId; END; CREATE TRIGGER IF NOT EXISTS voice_threads_delete_trigger AFTER DELETE ON threads FOR EACH ROW WHEN old.type=1 BEGIN DELETE FROM voice_events WHERE accountId=old.accountId AND threadId=old.threadId; END; history-service-0.5/plugins/sqlite/schema/v19.sql000066400000000000000000000000661455450246700221150ustar00rootroot00000000000000ALTER TABLE text_events ADD COLUMN sentTime datetime; history-service-0.5/plugins/sqlite/schema/v2.sql000066400000000000000000000042721455450246700220300ustar00rootroot00000000000000DROP TRIGGER voice_events_delete_trigger; CREATE TRIGGER voice_events_delete_trigger AFTER DELETE ON voice_events FOR EACH ROW BEGIN UPDATE threads SET count=(SELECT count(eventId) FROM voice_events WHERE accountId=old.accountId AND threadId=old.threadId) WHERE accountId=old.accountId AND threadId=old.threadId AND type=1; UPDATE threads SET unreadCount=(SELECT count(eventId) FROM voice_events WHERE accountId=old.accountId AND threadId=old.threadId AND newEvent='true') WHERE accountId=old.accountId AND threadId=old.threadId AND type=1; UPDATE threads SET lastEventId=(SELECT eventId FROM voice_events WHERE accountId=old.accountId AND threadId=old.threadId ORDER BY timestamp DESC LIMIT 1) WHERE accountId=old.accountId AND threadId=old.threadId AND type=1; UPDATE threads SET lastEventTimestamp=(SELECT timestamp FROM voice_events WHERE accountId=old.accountId AND threadId=old.threadId ORDER BY timestamp DESC LIMIT 1) WHERE accountId=old.accountId AND threadId=old.threadId AND type=1; END; DROP TRIGGER text_events_delete_trigger; CREATE TRIGGER text_events_delete_trigger AFTER DELETE ON text_events FOR EACH ROW BEGIN UPDATE threads SET count=(SELECT count(eventId) FROM text_events WHERE accountId=old.accountId AND threadId=old.threadId) WHERE accountId=old.accountId AND threadId=old.threadId AND type=0; UPDATE threads SET unreadCount=(SELECT count(eventId) FROM text_events WHERE accountId=old.accountId AND threadId=old.threadId AND newEvent='true') WHERE accountId=old.accountId AND threadId=old.threadId AND type=0; UPDATE threads SET lastEventId=(SELECT eventId FROM text_events WHERE accountId=old.accountId AND threadId=old.threadId ORDER BY timestamp DESC LIMIT 1) WHERE accountId=old.accountId AND threadId=old.threadId AND type=0; UPDATE threads SET lastEventTimestamp=(SELECT timestamp FROM text_events WHERE accountId=old.accountId AND threadId=old.threadId ORDER BY timestamp DESC LIMIT 1) WHERE accountId=old.accountId AND threadId=old.threadId AND type=0; END; history-service-0.5/plugins/sqlite/schema/v20.sql000066400000000000000000000000711455450246700221010ustar00rootroot00000000000000UPDATE voice_events SET newEvent = 0 WHERE newEvent = 1; history-service-0.5/plugins/sqlite/schema/v21.sql000066400000000000000000000216161455450246700221120ustar00rootroot00000000000000-- Allows to disable triggers when needed CREATE TABLE IF NOT EXISTS "disabled_triggers" ( "name" TEXT NOT NULL ); -- Drop all triggers DROP TRIGGER text_events_delete_trigger; DROP TRIGGER text_events_insert_trigger; DROP TRIGGER text_events_update_trigger; DROP TRIGGER text_threads_delete_trigger; DROP TRIGGER threads_delete_trigger; DROP TRIGGER voice_events_delete_trigger; DROP TRIGGER voice_events_insert_trigger; DROP TRIGGER voice_events_update_trigger; DROP TRIGGER voice_threads_delete_trigger; -- Recreate all triggers with new condition CREATE TRIGGER voice_events_insert_trigger AFTER INSERT ON voice_events FOR EACH ROW WHEN NOT exists(SELECT name FROM disabled_triggers WHERE name='voice_events_insert_trigger') BEGIN UPDATE threads SET count=(SELECT count(eventId) FROM voice_events WHERE accountId=new.accountId AND threadId=new.threadId) WHERE accountId=new.accountId AND threadId=new.threadId AND type=1; UPDATE threads SET unreadCount=(SELECT count(eventId) FROM voice_events WHERE accountId=new.accountId AND threadId=new.threadId AND newEvent='1') WHERE accountId=new.accountId AND threadId=new.threadId AND type=1; UPDATE threads SET lastEventId=(SELECT eventId FROM voice_events WHERE accountId=new.accountId AND threadId=new.threadId ORDER BY timestamp DESC LIMIT 1) WHERE accountId=new.accountId AND threadId=new.threadId AND type=1; UPDATE threads SET lastEventTimestamp=(SELECT timestamp FROM voice_events WHERE accountId=new.accountId AND threadId=new.threadId ORDER BY timestamp DESC LIMIT 1) WHERE accountId=new.accountId AND threadId=new.threadId AND type=1; END; CREATE TRIGGER voice_events_update_trigger AFTER UPDATE ON voice_events FOR EACH ROW WHEN NOT exists(SELECT name FROM disabled_triggers WHERE name='voice_events_update_trigger') BEGIN UPDATE threads SET count=(SELECT count(eventId) FROM voice_events WHERE accountId=new.accountId AND threadId=new.threadId) WHERE accountId=new.accountId AND threadId=new.threadId AND type=1; UPDATE threads SET unreadCount=(SELECT count(eventId) FROM voice_events WHERE accountId=new.accountId AND threadId=new.threadId AND newEvent='1') WHERE accountId=new.accountId AND threadId=new.threadId AND type=1; UPDATE threads SET lastEventId=(SELECT eventId FROM voice_events WHERE accountId=new.accountId AND threadId=new.threadId ORDER BY timestamp DESC LIMIT 1) WHERE accountId=new.accountId AND threadId=new.threadId AND type=1; UPDATE threads SET lastEventTimestamp=(SELECT timestamp FROM voice_events WHERE accountId=new.accountId AND threadId=new.threadId ORDER BY timestamp DESC LIMIT 1) WHERE accountId=new.accountId AND threadId=new.threadId AND type=1; END; CREATE TRIGGER voice_events_delete_trigger AFTER DELETE ON voice_events FOR EACH ROW WHEN NOT exists(SELECT name FROM disabled_triggers WHERE name='voice_events_delete_trigger') BEGIN UPDATE threads SET count=(SELECT count(eventId) FROM voice_events WHERE accountId=old.accountId AND threadId=old.threadId) WHERE accountId=old.accountId AND threadId=old.threadId AND type=1; UPDATE threads SET unreadCount=(SELECT count(eventId) FROM voice_events WHERE accountId=old.accountId AND threadId=old.threadId AND newEvent='1') WHERE accountId=old.accountId AND threadId=old.threadId AND type=1; UPDATE threads SET lastEventId=(SELECT eventId FROM voice_events WHERE accountId=old.accountId AND threadId=old.threadId ORDER BY timestamp DESC LIMIT 1) WHERE accountId=old.accountId AND threadId=old.threadId AND type=1; UPDATE threads SET lastEventTimestamp=(SELECT timestamp FROM voice_events WHERE accountId=old.accountId AND threadId=old.threadId ORDER BY timestamp DESC LIMIT 1) WHERE accountId=old.accountId AND threadId=old.threadId AND type=1; END; CREATE TRIGGER threads_delete_trigger AFTER DELETE ON threads FOR EACH ROW WHEN NOT exists(SELECT name FROM disabled_triggers WHERE name='threads_delete_trigger') BEGIN DELETE FROM thread_participants WHERE accountId=old.accountId AND threadId=old.threadId AND type=old.type; DELETE FROM chat_room_info WHERE accountId=old.accountId AND threadId=old.threadId AND type=old.type; END; CREATE TRIGGER text_events_insert_trigger AFTER INSERT ON text_events FOR EACH ROW WHEN new.messageType!=2 AND NOT exists(SELECT name FROM disabled_triggers WHERE name='text_events_insert_trigger') BEGIN UPDATE threads SET count=(SELECT count(eventId) FROM text_events WHERE accountId=new.accountId AND threadId=new.threadId AND messageType!=2) WHERE accountId=new.accountId AND threadId=new.threadId AND type=0; UPDATE threads SET unreadCount=(SELECT count(eventId) FROM text_events WHERE accountId=new.accountId AND threadId=new.threadId AND newEvent='1' AND messageType!=2) WHERE accountId=new.accountId AND threadId=new.threadId AND type=0; UPDATE threads SET lastEventId=(SELECT eventId FROM text_events WHERE accountId=new.accountId AND threadId=new.threadId AND messageType!=2 ORDER BY timestamp DESC LIMIT 1) WHERE accountId=new.accountId AND threadId=new.threadId AND type=0; UPDATE threads SET lastEventTimestamp=(SELECT timestamp FROM text_events WHERE accountId=new.accountId AND threadId=new.threadId AND messageType!=2 ORDER BY timestamp DESC LIMIT 1) WHERE accountId=new.accountId AND threadId=new.threadId AND type=0; END; CREATE TRIGGER text_events_update_trigger AFTER UPDATE ON text_events FOR EACH ROW WHEN new.messageType!=2 AND NOT exists(SELECT name FROM disabled_triggers WHERE name='text_events_update_trigger') BEGIN UPDATE threads SET count=(SELECT count(eventId) FROM text_events WHERE accountId=new.accountId AND threadId=new.threadId AND messageType!=2) WHERE accountId=new.accountId AND threadId=new.threadId AND type=0; UPDATE threads SET unreadCount=(SELECT count(eventId) FROM text_events WHERE accountId=new.accountId AND threadId=new.threadId AND newEvent='1' AND messageType!=2) WHERE accountId=new.accountId AND threadId=new.threadId AND type=0; UPDATE threads SET lastEventId=(SELECT eventId FROM text_events WHERE accountId=new.accountId AND threadId=new.threadId AND messageType!=2 ORDER BY timestamp DESC LIMIT 1) WHERE accountId=new.accountId AND threadId=new.threadId AND type=0; UPDATE threads SET lastEventTimestamp=(SELECT timestamp FROM text_events WHERE accountId=new.accountId AND threadId=new.threadId AND messageType!=2 ORDER BY timestamp DESC LIMIT 1) WHERE accountId=new.accountId AND threadId=new.threadId AND type=0; END; CREATE TRIGGER text_events_delete_trigger AFTER DELETE ON text_events FOR EACH ROW WHEN old.messageType!=2 AND NOT exists(SELECT name FROM disabled_triggers WHERE name='text_events_delete_trigger') BEGIN UPDATE threads SET count=(SELECT count(eventId) FROM text_events WHERE accountId=old.accountId AND threadId=old.threadId AND messageType!=2) WHERE accountId=old.accountId AND threadId=old.threadId AND type=0; UPDATE threads SET unreadCount=(SELECT count(eventId) FROM text_events WHERE accountId=old.accountId AND threadId=old.threadId AND newEvent='1' AND messageType!=2) WHERE accountId=old.accountId AND threadId=old.threadId AND type=0; UPDATE threads SET lastEventId=(SELECT eventId FROM text_events WHERE accountId=old.accountId AND threadId=old.threadId AND messageType!=2 ORDER BY timestamp DESC LIMIT 1) WHERE accountId=old.accountId AND threadId=old.threadId AND type=0; UPDATE threads SET lastEventTimestamp=(SELECT timestamp FROM text_events WHERE accountId=old.accountId AND threadId=old.threadId AND messageType!=2 ORDER BY timestamp DESC LIMIT 1) WHERE accountId=old.accountId AND threadId=old.threadId AND type=0; DELETE from text_event_attachments WHERE accountId=old.accountId AND threadId=old.threadId AND eventId=old.eventId; END; CREATE TRIGGER text_threads_delete_trigger AFTER DELETE ON threads FOR EACH ROW WHEN old.type=0 AND NOT exists(SELECT name FROM disabled_triggers WHERE name='text_threads_delete_trigger') BEGIN DELETE FROM text_events WHERE accountId=old.accountId AND threadId=old.threadId; END; CREATE TRIGGER voice_threads_delete_trigger AFTER DELETE ON threads FOR EACH ROW WHEN old.type=1 AND NOT exists(SELECT name FROM disabled_triggers WHERE name='voice_threads_delete_trigger') BEGIN DELETE FROM voice_events WHERE accountId=old.accountId AND threadId=old.threadId; END; history-service-0.5/plugins/sqlite/schema/v3.sql000066400000000000000000000030271455450246700220260ustar00rootroot00000000000000ALTER TABLE text_events ADD COLUMN subject varchar(256); CREATE TABLE IF NOT EXISTS text_event_attachments ( accountId varchar(255), threadId varchar(255), eventId varchar(255), attachmentId varchar(255), contentType varchar(255), filePath varchar(255), status tinyint ); DROP TRIGGER text_events_delete_trigger; CREATE TRIGGER text_events_delete_trigger AFTER DELETE ON text_events FOR EACH ROW BEGIN UPDATE threads SET count=(SELECT count(eventId) FROM text_events WHERE accountId=old.accountId AND threadId=old.threadId) WHERE accountId=old.accountId AND threadId=old.threadId AND type=0; UPDATE threads SET unreadCount=(SELECT count(eventId) FROM text_events WHERE accountId=old.accountId AND threadId=old.threadId AND newEvent='true') WHERE accountId=old.accountId AND threadId=old.threadId AND type=0; UPDATE threads SET lastEventId=(SELECT eventId FROM text_events WHERE accountId=old.accountId AND threadId=old.threadId ORDER BY timestamp DESC LIMIT 1) WHERE accountId=old.accountId AND threadId=old.threadId AND type=0; UPDATE threads SET lastEventTimestamp=(SELECT timestamp FROM text_events WHERE accountId=old.accountId AND threadId=old.threadId ORDER BY timestamp DESC LIMIT 1) WHERE accountId=old.accountId AND threadId=old.threadId AND type=0; DELETE from text_event_attachments WHERE accountId=old.accountId AND threadId=old.threadId AND eventId=old.eventId; END; history-service-0.5/plugins/sqlite/schema/v4.sql000066400000000000000000000003361455450246700220270ustar00rootroot00000000000000CREATE TRIGGER threads_delete_trigger AFTER DELETE ON threads FOR EACH ROW BEGIN DELETE FROM thread_participants WHERE accountId=old.accountId AND threadId=old.threadId AND type=old.type; END; history-service-0.5/plugins/sqlite/schema/v5.sql000066400000000000000000000002031455450246700220210ustar00rootroot00000000000000DELETE FROM thread_participants WHERE (SELECT count(threadId) FROM threads WHERE threads.threadId=thread_participants.threadId)=0; history-service-0.5/plugins/sqlite/schema/v6.sql000066400000000000000000000103021455450246700220230ustar00rootroot00000000000000DROP TRIGGER text_events_insert_trigger; DROP TRIGGER text_events_update_trigger; DROP TRIGGER text_events_delete_trigger; ALTER TABLE text_events RENAME TO text_events_old; CREATE TABLE text_events ( accountId varchar(255), threadId varchar(255), eventId varchar(255), senderId varchar(255), timestamp datetime, newEvent bool, message varchar(512), messageType tinyint, messageStatus tinyint, readTimestamp datetime, subject varchar(256) ); INSERT INTO text_events ( accountId, threadId, eventId, senderId, timestamp, newEvent, message, messageType, messageStatus, readTimestamp, subject) SELECT accountId, threadId, eventId, senderId, timestamp, newEvent, message, messageType, messageFlags, readTimestamp, subject FROM text_events_old; DROP TABLE text_events_old; CREATE TRIGGER text_events_insert_trigger AFTER INSERT ON text_events FOR EACH ROW BEGIN UPDATE threads SET count=(SELECT count(eventId) FROM text_events WHERE accountId=new.accountId AND threadId=new.threadId) WHERE accountId=new.accountId AND threadId=new.threadId AND type=0; UPDATE threads SET unreadCount=(SELECT count(eventId) FROM text_events WHERE accountId=new.accountId AND threadId=new.threadId AND newEvent='true') WHERE accountId=new.accountId AND threadId=new.threadId AND type=0; UPDATE threads SET lastEventId=(SELECT eventId FROM text_events WHERE accountId=new.accountId AND threadId=new.threadId ORDER BY timestamp DESC LIMIT 1) WHERE accountId=new.accountId AND threadId=new.threadId AND type=0; UPDATE threads SET lastEventTimestamp=(SELECT timestamp FROM text_events WHERE accountId=new.accountId AND threadId=new.threadId ORDER BY timestamp DESC LIMIT 1) WHERE accountId=new.accountId AND threadId=new.threadId AND type=0; END; CREATE TRIGGER text_events_update_trigger AFTER UPDATE ON text_events FOR EACH ROW BEGIN UPDATE threads SET count=(SELECT count(eventId) FROM text_events WHERE accountId=new.accountId AND threadId=new.threadId) WHERE accountId=new.accountId AND threadId=new.threadId AND type=0; UPDATE threads SET unreadCount=(SELECT count(eventId) FROM text_events WHERE accountId=new.accountId AND threadId=new.threadId AND newEvent='true') WHERE accountId=new.accountId AND threadId=new.threadId AND type=0; UPDATE threads SET lastEventId=(SELECT eventId FROM text_events WHERE accountId=new.accountId AND threadId=new.threadId ORDER BY timestamp DESC LIMIT 1) WHERE accountId=new.accountId AND threadId=new.threadId AND type=0; UPDATE threads SET lastEventTimestamp=(SELECT timestamp FROM text_events WHERE accountId=new.accountId AND threadId=new.threadId ORDER BY timestamp DESC LIMIT 1) WHERE accountId=new.accountId AND threadId=new.threadId AND type=0; END; CREATE TRIGGER text_events_delete_trigger AFTER DELETE ON text_events FOR EACH ROW BEGIN UPDATE threads SET count=(SELECT count(eventId) FROM text_events WHERE accountId=old.accountId AND threadId=old.threadId) WHERE accountId=old.accountId AND threadId=old.threadId AND type=0; UPDATE threads SET unreadCount=(SELECT count(eventId) FROM text_events WHERE accountId=old.accountId AND threadId=old.threadId AND newEvent='true') WHERE accountId=old.accountId AND threadId=old.threadId AND type=0; UPDATE threads SET lastEventId=(SELECT eventId FROM text_events WHERE accountId=old.accountId AND threadId=old.threadId ORDER BY timestamp DESC LIMIT 1) WHERE accountId=old.accountId AND threadId=old.threadId AND type=0; UPDATE threads SET lastEventTimestamp=(SELECT timestamp FROM text_events WHERE accountId=old.accountId AND threadId=old.threadId ORDER BY timestamp DESC LIMIT 1) WHERE accountId=old.accountId AND threadId=old.threadId AND type=0; DELETE from text_event_attachments WHERE accountId=old.accountId AND threadId=old.threadId AND eventId=old.eventId; END; history-service-0.5/plugins/sqlite/schema/v7.sql000066400000000000000000000000761455450246700220330ustar00rootroot00000000000000UPDATE text_events SET messageStatus=4 WHERE messageStatus=0; history-service-0.5/plugins/sqlite/schema/v8.sql000066400000000000000000000152521455450246700220360ustar00rootroot00000000000000DROP TRIGGER text_events_insert_trigger; DROP TRIGGER text_events_update_trigger; DROP TRIGGER text_events_delete_trigger; DROP TRIGGER voice_events_insert_trigger; DROP TRIGGER voice_events_update_trigger; DROP TRIGGER voice_events_delete_trigger; CREATE TRIGGER text_events_insert_trigger AFTER INSERT ON text_events FOR EACH ROW BEGIN UPDATE threads SET count=(SELECT count(eventId) FROM text_events WHERE accountId=new.accountId AND threadId=new.threadId) WHERE accountId=new.accountId AND threadId=new.threadId AND type=0; UPDATE threads SET unreadCount=(SELECT count(eventId) FROM text_events WHERE accountId=new.accountId AND threadId=new.threadId AND newEvent='1') WHERE accountId=new.accountId AND threadId=new.threadId AND type=0; UPDATE threads SET lastEventId=(SELECT eventId FROM text_events WHERE accountId=new.accountId AND threadId=new.threadId ORDER BY timestamp DESC LIMIT 1) WHERE accountId=new.accountId AND threadId=new.threadId AND type=0; UPDATE threads SET lastEventTimestamp=(SELECT timestamp FROM text_events WHERE accountId=new.accountId AND threadId=new.threadId ORDER BY timestamp DESC LIMIT 1) WHERE accountId=new.accountId AND threadId=new.threadId AND type=0; END; CREATE TRIGGER text_events_update_trigger AFTER UPDATE ON text_events FOR EACH ROW BEGIN UPDATE threads SET count=(SELECT count(eventId) FROM text_events WHERE accountId=new.accountId AND threadId=new.threadId) WHERE accountId=new.accountId AND threadId=new.threadId AND type=0; UPDATE threads SET unreadCount=(SELECT count(eventId) FROM text_events WHERE accountId=new.accountId AND threadId=new.threadId AND newEvent='1') WHERE accountId=new.accountId AND threadId=new.threadId AND type=0; UPDATE threads SET lastEventId=(SELECT eventId FROM text_events WHERE accountId=new.accountId AND threadId=new.threadId ORDER BY timestamp DESC LIMIT 1) WHERE accountId=new.accountId AND threadId=new.threadId AND type=0; UPDATE threads SET lastEventTimestamp=(SELECT timestamp FROM text_events WHERE accountId=new.accountId AND threadId=new.threadId ORDER BY timestamp DESC LIMIT 1) WHERE accountId=new.accountId AND threadId=new.threadId AND type=0; END; CREATE TRIGGER text_events_delete_trigger AFTER DELETE ON text_events FOR EACH ROW BEGIN UPDATE threads SET count=(SELECT count(eventId) FROM text_events WHERE accountId=old.accountId AND threadId=old.threadId) WHERE accountId=old.accountId AND threadId=old.threadId AND type=0; UPDATE threads SET unreadCount=(SELECT count(eventId) FROM text_events WHERE accountId=old.accountId AND threadId=old.threadId AND newEvent='1') WHERE accountId=old.accountId AND threadId=old.threadId AND type=0; UPDATE threads SET lastEventId=(SELECT eventId FROM text_events WHERE accountId=old.accountId AND threadId=old.threadId ORDER BY timestamp DESC LIMIT 1) WHERE accountId=old.accountId AND threadId=old.threadId AND type=0; UPDATE threads SET lastEventTimestamp=(SELECT timestamp FROM text_events WHERE accountId=old.accountId AND threadId=old.threadId ORDER BY timestamp DESC LIMIT 1) WHERE accountId=old.accountId AND threadId=old.threadId AND type=0; DELETE from text_event_attachments WHERE accountId=old.accountId AND threadId=old.threadId AND eventId=old.eventId; END; CREATE TRIGGER voice_events_insert_trigger AFTER INSERT ON voice_events FOR EACH ROW BEGIN UPDATE threads SET count=(SELECT count(eventId) FROM voice_events WHERE accountId=new.accountId AND threadId=new.threadId) WHERE accountId=new.accountId AND threadId=new.threadId AND type=1; UPDATE threads SET unreadCount=(SELECT count(eventId) FROM voice_events WHERE accountId=new.accountId AND threadId=new.threadId AND newEvent='1') WHERE accountId=new.accountId AND threadId=new.threadId AND type=1; UPDATE threads SET lastEventId=(SELECT eventId FROM voice_events WHERE accountId=new.accountId AND threadId=new.threadId ORDER BY timestamp DESC LIMIT 1) WHERE accountId=new.accountId AND threadId=new.threadId AND type=1; UPDATE threads SET lastEventTimestamp=(SELECT timestamp FROM voice_events WHERE accountId=new.accountId AND threadId=new.threadId ORDER BY timestamp DESC LIMIT 1) WHERE accountId=new.accountId AND threadId=new.threadId AND type=1; END; CREATE TRIGGER voice_events_update_trigger AFTER UPDATE ON voice_events FOR EACH ROW BEGIN UPDATE threads SET count=(SELECT count(eventId) FROM voice_events WHERE accountId=new.accountId AND threadId=new.threadId) WHERE accountId=new.accountId AND threadId=new.threadId AND type=1; UPDATE threads SET unreadCount=(SELECT count(eventId) FROM voice_events WHERE accountId=new.accountId AND threadId=new.threadId AND newEvent='1') WHERE accountId=new.accountId AND threadId=new.threadId AND type=1; UPDATE threads SET lastEventId=(SELECT eventId FROM voice_events WHERE accountId=new.accountId AND threadId=new.threadId ORDER BY timestamp DESC LIMIT 1) WHERE accountId=new.accountId AND threadId=new.threadId AND type=1; UPDATE threads SET lastEventTimestamp=(SELECT timestamp FROM voice_events WHERE accountId=new.accountId AND threadId=new.threadId ORDER BY timestamp DESC LIMIT 1) WHERE accountId=new.accountId AND threadId=new.threadId AND type=1; END; CREATE TRIGGER voice_events_delete_trigger AFTER DELETE ON voice_events FOR EACH ROW BEGIN UPDATE threads SET count=(SELECT count(eventId) FROM voice_events WHERE accountId=old.accountId AND threadId=old.threadId) WHERE accountId=old.accountId AND threadId=old.threadId AND type=1; UPDATE threads SET unreadCount=(SELECT count(eventId) FROM voice_events WHERE accountId=old.accountId AND threadId=old.threadId AND newEvent='1') WHERE accountId=old.accountId AND threadId=old.threadId AND type=1; UPDATE threads SET lastEventId=(SELECT eventId FROM voice_events WHERE accountId=old.accountId AND threadId=old.threadId ORDER BY timestamp DESC LIMIT 1) WHERE accountId=old.accountId AND threadId=old.threadId AND type=1; UPDATE threads SET lastEventTimestamp=(SELECT timestamp FROM voice_events WHERE accountId=old.accountId AND threadId=old.threadId ORDER BY timestamp DESC LIMIT 1) WHERE accountId=old.accountId AND threadId=old.threadId AND type=1; END; history-service-0.5/plugins/sqlite/schema/v9.sql000066400000000000000000000073001455450246700220320ustar00rootroot00000000000000DROP TRIGGER text_events_insert_trigger; DROP TRIGGER text_events_update_trigger; DROP TRIGGER text_events_delete_trigger; CREATE TRIGGER text_events_insert_trigger AFTER INSERT ON text_events FOR EACH ROW BEGIN UPDATE threads SET count=(SELECT count(eventId) FROM text_events WHERE accountId=new.accountId AND threadId=new.threadId AND messageType!=2) WHERE accountId=new.accountId AND threadId=new.threadId AND type=0; UPDATE threads SET unreadCount=(SELECT count(eventId) FROM text_events WHERE accountId=new.accountId AND threadId=new.threadId AND newEvent='1' AND messageType!=2) WHERE accountId=new.accountId AND threadId=new.threadId AND type=0; UPDATE threads SET lastEventId=(SELECT eventId FROM text_events WHERE accountId=new.accountId AND threadId=new.threadId AND messageType!=2 ORDER BY timestamp DESC LIMIT 1) WHERE accountId=new.accountId AND threadId=new.threadId AND type=0; UPDATE threads SET lastEventTimestamp=(SELECT timestamp FROM text_events WHERE accountId=new.accountId AND threadId=new.threadId AND messageType!=2 ORDER BY timestamp DESC LIMIT 1) WHERE accountId=new.accountId AND threadId=new.threadId AND type=0; END; CREATE TRIGGER text_events_update_trigger AFTER UPDATE ON text_events FOR EACH ROW BEGIN UPDATE threads SET count=(SELECT count(eventId) FROM text_events WHERE accountId=new.accountId AND threadId=new.threadId AND messageType!=2) WHERE accountId=new.accountId AND threadId=new.threadId AND type=0; UPDATE threads SET unreadCount=(SELECT count(eventId) FROM text_events WHERE accountId=new.accountId AND threadId=new.threadId AND newEvent='1' AND messageType!=2) WHERE accountId=new.accountId AND threadId=new.threadId AND type=0; UPDATE threads SET lastEventId=(SELECT eventId FROM text_events WHERE accountId=new.accountId AND threadId=new.threadId AND messageType!=2 ORDER BY timestamp DESC LIMIT 1) WHERE accountId=new.accountId AND threadId=new.threadId AND type=0; UPDATE threads SET lastEventTimestamp=(SELECT timestamp FROM text_events WHERE accountId=new.accountId AND threadId=new.threadId AND messageType!=2 ORDER BY timestamp DESC LIMIT 1) WHERE accountId=new.accountId AND threadId=new.threadId AND type=0; END; CREATE TRIGGER text_events_delete_trigger AFTER DELETE ON text_events FOR EACH ROW BEGIN UPDATE threads SET count=(SELECT count(eventId) FROM text_events WHERE accountId=old.accountId AND threadId=old.threadId AND messageType!=2) WHERE accountId=old.accountId AND threadId=old.threadId AND type=0; UPDATE threads SET unreadCount=(SELECT count(eventId) FROM text_events WHERE accountId=old.accountId AND threadId=old.threadId AND newEvent='1' AND messageType!=2) WHERE accountId=old.accountId AND threadId=old.threadId AND type=0; UPDATE threads SET lastEventId=(SELECT eventId FROM text_events WHERE accountId=old.accountId AND threadId=old.threadId AND messageType!=2 ORDER BY timestamp DESC LIMIT 1) WHERE accountId=old.accountId AND threadId=old.threadId AND type=0; UPDATE threads SET lastEventTimestamp=(SELECT timestamp FROM text_events WHERE accountId=old.accountId AND threadId=old.threadId AND messageType!=2 ORDER BY timestamp DESC LIMIT 1) WHERE accountId=old.accountId AND threadId=old.threadId AND type=0; DELETE from text_event_attachments WHERE accountId=old.accountId AND threadId=old.threadId AND eventId=old.eventId; END; history-service-0.5/plugins/sqlite/sqlitedatabase.cpp000066400000000000000000000424611455450246700232140ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 "phoneutils_p.h" #include "sqlite3.h" #include "sqlitedatabase.h" #include "types.h" #include "utils_p.h" #include #include #include #include #include #include #include #include Q_DECLARE_OPAQUE_POINTER(sqlite3*) Q_DECLARE_METATYPE(sqlite3*) // custom sqlite function "comparePhoneNumbers" used to compare IDs if necessary void comparePhoneNumbers(sqlite3_context *context, int argc, sqlite3_value **argv) { Q_ASSERT(argc >= 2); QString arg1((const char*)sqlite3_value_text(argv[0])); QString arg2((const char*)sqlite3_value_text(argv[1])); sqlite3_result_int(context, (int)History::PhoneUtils::comparePhoneNumbers(arg1, arg2)); } void compareNormalizedPhoneNumbers(sqlite3_context *context, int argc, sqlite3_value **argv) { Q_ASSERT(argc >= 2); QString arg1((const char*)sqlite3_value_text(argv[0])); QString arg2((const char*)sqlite3_value_text(argv[1])); sqlite3_result_int(context, (int)History::PhoneUtils::compareNormalizedPhoneNumbers(arg1, arg2)); } void normalizeId(sqlite3_context *context, int argc, sqlite3_value **argv) { Q_ASSERT(argc >= 2); QString accountId((const char*)sqlite3_value_text(argv[0])); QString id((const char*)sqlite3_value_text(argv[1])); QString normalizedId = History::Utils::normalizeId(accountId, id); sqlite3_result_text(context, strdup(normalizedId.toUtf8().data()), -1, &free); } SQLiteDatabase::SQLiteDatabase(QObject *parent) : QObject(parent), mSchemaVersion(0) { initializeDatabase(); } SQLiteDatabase *SQLiteDatabase::instance() { static SQLiteDatabase *self = new SQLiteDatabase(); return self; } bool SQLiteDatabase::initializeDatabase() { mDatabasePath = qgetenv("HISTORY_SQLITE_DBPATH"); if (mDatabasePath.isEmpty()) { mDatabasePath = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation); QDir dir(mDatabasePath); if (!dir.exists("history-service") && !dir.mkpath("history-service")) { qCritical() << "Failed to create dir"; return false; } dir.cd("history-service"); mDatabasePath = dir.absoluteFilePath("history.sqlite"); } mDatabase = QSqlDatabase::addDatabase("QSQLITE"); mDatabase.setDatabaseName(mDatabasePath); qDebug() << "Using database at" << mDatabasePath; // always run the createDatabase function at least during the development if (!createOrUpdateDatabase()) { qCritical() << "Failed to create or update the database"; return false; } return true; } QSqlDatabase SQLiteDatabase::database() const { return mDatabase; } bool SQLiteDatabase::beginTransation() { return mDatabase.transaction(); } bool SQLiteDatabase::finishTransaction() { return mDatabase.commit(); } bool SQLiteDatabase::rollbackTransaction() { return mDatabase.rollback(); } /// this method is to be used mainly by unit tests in order to clean up the database between /// tests. bool SQLiteDatabase::reopen() { mDatabase.close(); mDatabase.open(); // make sure the database is up-to-date after reopening. // this is mainly required for the memory backend used for testing return createOrUpdateDatabase(); } QString SQLiteDatabase::dumpSchema() const { // query copied from sqlite3's shell.c QSqlQuery query(mDatabase); if (!query.exec("SELECT sql FROM " " (SELECT sql sql, type type, tbl_name tbl_name, name name, rowid x" " FROM sqlite_master UNION ALL" " SELECT sql, type, tbl_name, name, rowid FROM sqlite_temp_master) " "WHERE type!='meta' AND sql NOTNULL AND name NOT LIKE 'sqlite_%' " "ORDER BY rowid")) { return QString(); } QString schema; while (query.next()) { schema += query.value("sql").toString() + ";\n"; } return schema; } void SQLiteDatabase::migrateAccount(const QString &fromAccountId, const QString &toAccountId) { qDebug() << "Start migration from" << fromAccountId << "to" << toAccountId; QStringList tables = QStringList() << "threads" << "thread_participants" << "text_event_attachments" << "text_events" << "voice_events" << "chat_room_info"; QSqlQuery query(mDatabase); beginTransation(); // disable triggers if (!query.exec("INSERT INTO disabled_triggers VALUES ('voice_events_update_trigger')")) { qWarning() << "Failed to disable triggers:" << query.lastQuery() << "Error:" << query.lastError(); rollbackTransaction(); } if (!query.exec("INSERT INTO disabled_triggers VALUES ('text_events_update_trigger')")) { qWarning() << "Failed to disable triggers:" << query.lastQuery() << "Error:" << query.lastError(); rollbackTransaction(); } // do the migration for (const auto& table : tables) { if (!query.exec(QString("UPDATE %1 SET accountId = \"%2\" WHERE accountId = \"%3\"").arg(table).arg(toAccountId).arg(fromAccountId))) { qWarning() << "Failed to migrate account ids:" << query.lastQuery() << "Error:" << query.lastError(); rollbackTransaction(); return; } } // restore triggers if (!query.exec("DELETE FROM disabled_triggers")) { qWarning() << "Failed to restore triggers:" << query.lastQuery() << "Error:" << query.lastError(); rollbackTransaction(); } finishTransaction(); qDebug() << "migration done"; } bool SQLiteDatabase::runMultipleStatements(const QStringList &statements, bool useTransaction) { if (statements.isEmpty()) { return false; } QSqlQuery query(mDatabase); if (useTransaction) { beginTransation(); } Q_FOREACH(const QString &statement, statements) { if (!query.exec(statement)) { if (useTransaction) { rollbackTransaction(); } qCritical() << "Failed to create or update database. SQL Statements:" << query.lastQuery() << "Error:" << query.lastError(); return false; } } if (useTransaction) { finishTransaction(); } return true; } void trace(void* /* something */, const char *query) { qDebug() << "SQLITE TRACE:" << query; } bool SQLiteDatabase::upgradeNeeded(int version) const { QSqlQuery query(mDatabase); if (version == 19) { // check for already exist column if (query.exec("SELECT sentTime FROM text_events LIMIT 1")) { return false; } } return true; } bool SQLiteDatabase::createOrUpdateDatabase() { bool create = !QFile(mDatabasePath).exists(); if (!mDatabase.open()) { return false; } // create the comparePhoneNumbers custom sqlite functions sqlite3 *handle = database().driver()->handle().value(); sqlite3_create_function(handle, "comparePhoneNumbers", 2, SQLITE_ANY, NULL, &comparePhoneNumbers, NULL, NULL); sqlite3_create_function(handle, "compareNormalizedPhoneNumbers", 2, SQLITE_ANY, NULL, &compareNormalizedPhoneNumbers, NULL, NULL); // and also create the normalizeId function sqlite3_create_function(handle, "normalizeId", 2, SQLITE_ANY, NULL, &normalizeId, NULL, NULL); #ifdef TRACE_SQLITE sqlite3_trace(handle, &trace, NULL); #endif parseVersionInfo(); QSqlQuery query(mDatabase); // use memory to create temporary tables query.exec("PRAGMA temp_store = MEMORY"); QStringList statements; int existingVersion = 0; int upgradeToVersion = 0; if (create) { statements = parseSchemaFile(":/database/schema/schema.sql"); } else { // if the database already exists, we don´t need to create the tables // only check if an update is needed query.exec("SELECT * FROM schema_version"); if (!query.exec() || !query.next()) { return false; } existingVersion = query.value(0).toInt(); upgradeToVersion = existingVersion + 1; while (upgradeToVersion <= mSchemaVersion) { if (upgradeNeeded(upgradeToVersion)) { statements += parseSchemaFile(QString(":/database/schema/v%1.sql").arg(QString::number(upgradeToVersion))); } ++upgradeToVersion; } } beginTransation(); if (!statements.isEmpty() && !runMultipleStatements(statements, false)) { rollbackTransaction(); return false; } //don't downgrade database version if (mSchemaVersion > existingVersion) { // now set the new database schema version if (!query.exec("DELETE FROM schema_version")) { qCritical() << "Failed to remove previous schema versions. SQL Statement:" << query.lastQuery() << "Error:" << query.lastError(); rollbackTransaction(); return false; } if (!query.exec(QString("INSERT INTO schema_version VALUES (%1)").arg(mSchemaVersion))) { qCritical() << "Failed to insert new schema version. SQL Statement:" << query.lastQuery() << "Error:" << query.lastError(); rollbackTransaction(); return false; } } // now check if any data updating is required if (existingVersion > 0) { // v10 - timestamps in UTC if (existingVersion > 0 && existingVersion < 10) { if (!changeTimestampsToUtc()) { qCritical() << "Failed to update existing data."; rollbackTransaction(); return false; } } if (existingVersion < 13) { // convert all ofono groups to Room type depending if the mms option is enabled QVariant mmsGroupChatEnabled = History::Utils::getUserValue("com.lomiri.touch.AccountsService.Phone", "MmsGroupChatEnabled"); // we must not fail if we cannot reach accounts service. if (mmsGroupChatEnabled.isValid()) { // if mms is disabled all chats will be turned into broadcast, otherwise // we turn them into Room if (mmsGroupChatEnabled.toBool()) { if (!convertOfonoGroupChatToRoom()) { qCritical() << "Failed to update existing group chats to Room type."; rollbackTransaction(); return false; } } } } } finishTransaction(); return true; } QStringList SQLiteDatabase::parseSchemaFile(const QString &fileName) { QFile schema(fileName); if (!schema.open(QFile::ReadOnly)) { qCritical() << "Failed to open " << fileName; return QStringList(); } bool parsingBlock = false; QString statement; QStringList statements; // FIXME: this parser is very basic, it needs to be improved in the future // it does a lot of assumptions based on the structure of the schema.sql file QTextStream stream(&schema); while (!stream.atEnd()) { QString line = stream.readLine(); bool statementEnded = false; statement += line; // check if we are parsing a trigger command if (line.trimmed().startsWith("CREATE TRIGGER", Qt::CaseInsensitive)) { parsingBlock = true; } else if (parsingBlock) { if (line.contains("END;")) { parsingBlock = false; statementEnded = true; } } else if (statement.contains(";")) { statementEnded = true; } statement += "\n"; if (statementEnded) { statements.append(statement); statement.clear(); } } return statements; } void SQLiteDatabase::parseVersionInfo() { QFile schema(":/database/schema/version.info"); if (!schema.open(QFile::ReadOnly)) { qDebug() << schema.error(); qCritical() << "Failed to get database version"; return; } QString version = schema.readAll(); mSchemaVersion = version.toInt(); } bool SQLiteDatabase::changeTimestampsToUtc() { // update the text events QSqlQuery query(database()); QString queryText = "SELECT accountId, threadId, eventId, timestamp, readTimestamp FROM text_events"; if (!query.exec(queryText)) { qWarning() << "Failed to update text events:" << query.lastError(); return false; } QList events; while (query.next()) { QVariantMap event; event[History::FieldAccountId] = query.value(0); event[History::FieldThreadId] = query.value(1); event[History::FieldEventId] = query.value(2); event[History::FieldTimestamp] = query.value(3); event[History::FieldReadTimestamp] = query.value(4); events << event; } query.clear(); queryText = "UPDATE text_events SET timestamp=:timestamp, readTimestamp=:readTimestamp"; queryText += " WHERE accountId=:accountId AND threadId=:threadId AND eventId=:eventId"; query.prepare(queryText); Q_FOREACH (const QVariantMap &event, events) { query.bindValue(":accountId", event[History::FieldAccountId]); query.bindValue(":threadId", event[History::FieldThreadId]); query.bindValue(":eventId", event[History::FieldEventId]); query.bindValue(":timestamp", event[History::FieldTimestamp].toDateTime().toUTC().toString("yyyy-MM-ddTHH:mm:ss.zzz")); query.bindValue(":readTimestamp", event[History::FieldReadTimestamp].toDateTime().toUTC().toString("yyyy-MM-ddTHH:mm:ss.zzz")); if (!query.exec()) { qWarning() << "Failed to update text event:" << query.lastError(); return false; } } // and do the same for voice events queryText = "SELECT accountId, threadId, eventId, timestamp FROM voice_events"; if (!query.exec(queryText)) { qWarning() << "Failed to update voice events:" << query.lastError(); return false; } events.clear(); while (query.next()) { QVariantMap event; event[History::FieldAccountId] = query.value(0); event[History::FieldThreadId] = query.value(1); event[History::FieldEventId] = query.value(2); event[History::FieldTimestamp] = query.value(3); events << event; } query.clear(); queryText = "UPDATE voice_events SET timestamp=:timestamp"; queryText += " WHERE accountId=:accountId AND threadId=:threadId AND eventId=:eventId"; query.prepare(queryText); Q_FOREACH (const QVariantMap &event, events) { query.bindValue(":accountId", event[History::FieldAccountId]); query.bindValue(":threadId", event[History::FieldThreadId]); query.bindValue(":eventId", event[History::FieldEventId]); query.bindValue(":timestamp", event[History::FieldTimestamp].toDateTime().toUTC().toString("yyyy-MM-ddTHH:mm:ss.zzz")); if (!query.exec()) { qWarning() << "Failed to update voice event:" << query.lastError(); return false; } } return true; } bool SQLiteDatabase::convertOfonoGroupChatToRoom() { QSqlQuery query(database()); QString queryText = "UPDATE threads SET chatType=2 WHERE accountId LIKE 'ofono/ofono%' AND (SELECT COUNT(participantId) from thread_participants WHERE thread_participants.threadId=threads.threadId and thread_participants.accountId=threads.accountId AND thread_participants.type=threads.type) > 1"; query.prepare(queryText); if (!query.exec()) { qWarning() << "Failed to update group chats to Room 1:" << query.executedQuery() << query.lastError(); return false; } query.clear(); // now insert a row in chat_room_info for each room if (!query.exec("SELECT accountId, threadId from threads WHERE accountId LIKE 'ofono/ofono%' AND chatType=2")) { qWarning() << "Failed to update group chats to Room 2:" << query.executedQuery() << query.lastError(); return false; } while (query.next()) { QSqlQuery queryInsertRoom(database()); QString accountId = query.value(0).toString(); QString threadId = query.value(1).toString(); queryInsertRoom.prepare("INSERT INTO chat_room_info (accountId, threadId, type, joined) VALUES (:accountId,:threadId,:type,:joined)"); queryInsertRoom.bindValue(":accountId", accountId); queryInsertRoom.bindValue(":threadId", threadId); queryInsertRoom.bindValue(":type", History::EventTypeText); queryInsertRoom.bindValue(":joined", true); if(!queryInsertRoom.exec()) { qWarning() << "Failed to update group chats to Room 3:" << queryInsertRoom.executedQuery() << queryInsertRoom.lastError(); return false; } queryInsertRoom.clear(); } query.clear(); return true; } history-service-0.5/plugins/sqlite/sqlitedatabase.h000066400000000000000000000033761455450246700226630ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 SQLITEDATABASE_H #define SQLITEDATABASE_H #include #include class SQLiteDatabase : public QObject { Q_OBJECT public: static SQLiteDatabase *instance(); bool initializeDatabase(); QSqlDatabase database() const; bool beginTransation(); bool finishTransaction(); bool rollbackTransaction(); bool reopen(); QString dumpSchema() const; void migrateAccount(const QString &fromAccountId, const QString &toAccountId); QStringList parseSchemaFile(const QString &fileName); bool runMultipleStatements(const QStringList &statements, bool useTransaction = true); protected: bool createOrUpdateDatabase(); void parseVersionInfo(); // data upgrade functions bool changeTimestampsToUtc(); bool convertOfonoGroupChatToRoom(); private: explicit SQLiteDatabase(QObject *parent = 0); bool upgradeNeeded(int version) const; QString mDatabasePath; QSqlDatabase mDatabase; int mSchemaVersion; }; #endif // SQLITEDATABASE_H history-service-0.5/plugins/sqlite/sqlitehistoryeventview.cpp000066400000000000000000000101631455450246700251000ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 "sqlitehistoryeventview.h" #include "sqlitedatabase.h" #include "sqlitehistoryplugin.h" #include "sort.h" #include #include #include SQLiteHistoryEventView::SQLiteHistoryEventView(SQLiteHistoryPlugin *plugin, History::EventType type, const History::Sort &sort, const History::Filter &filter) : History::PluginEventView(), mPlugin(plugin), mType(type), mSort(sort), mFilter(filter), mQuery(SQLiteDatabase::instance()->database()), mPageSize(15), mOffset(0), mValid(true) { mTemporaryTable = QString("eventview%1%2").arg(QString::number((qulonglong)this), QDateTime::currentDateTimeUtc().toString("yyyyMMddhhmmsszzz")); mQuery.setForwardOnly(true); // FIXME: validate the filter QVariantMap filterValues; QString condition = mPlugin->filterToString(filter, filterValues); QString order; if (!sort.sortField().isNull()) { // WORKAROUND: Supports multiple fields by split it using ',' Q_FOREACH(const QString& field, sort.sortField().split(",")) { order += QString("%1 %2, ") .arg(field.trimmed()) .arg(sort.sortOrder() == Qt::AscendingOrder ? "ASC" : "DESC"); } order = QString("ORDER BY %1").arg(order.mid(0, order.lastIndexOf(","))); // FIXME: check case sensitiviy } QString queryText = QString("CREATE TEMP TABLE %1 AS ").arg(mTemporaryTable); queryText += mPlugin->sqlQueryForEvents(type, condition, order); if (!mQuery.prepare(queryText)) { mValid = false; Q_EMIT Invalidated(); qCritical() << "Error:" << mQuery.lastError() << mQuery.lastQuery(); return; } Q_FOREACH(const QString &key, filterValues.keys()) { mQuery.bindValue(key, filterValues[key]); } if (!mQuery.exec()) { mValid = false; Q_EMIT Invalidated(); qCritical() << "Error:" << mQuery.lastError() << mQuery.lastQuery(); return; } } SQLiteHistoryEventView::~SQLiteHistoryEventView() { if (!mQuery.exec(QString("DROP TABLE IF EXISTS %1").arg(mTemporaryTable))) { qCritical() << "Error:" << mQuery.lastError() << mQuery.lastQuery(); return; } } QList SQLiteHistoryEventView::NextPage() { QList events; // now prepare for selecting from it mQuery.prepare(QString("SELECT * FROM %1 LIMIT %2 OFFSET %3").arg(mTemporaryTable, QString::number(mPageSize), QString::number(mOffset))); if (!mQuery.exec()) { mValid = false; Q_EMIT Invalidated(); qCritical() << "Error:" << mQuery.lastError() << mQuery.lastQuery(); return events; } events = mPlugin->parseEventResults(mType, mQuery); mOffset += mPageSize; mQuery.clear(); return events; } int SQLiteHistoryEventView::GetTotalCount() { mQuery.prepare(QString("SELECT count(*) FROM %1").arg(mTemporaryTable)); if (!mQuery.exec() || !mQuery.next()) { qWarning() << "Failed to get total count. Error:" << mQuery.lastError(); return 0; } int totalCount = mQuery.value(0).toUInt(); mQuery.clear(); return totalCount; } bool SQLiteHistoryEventView::IsValid() const { return mQuery.isActive(); } history-service-0.5/plugins/sqlite/sqlitehistoryeventview.h000066400000000000000000000031771455450246700245540ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 SQLITEHISTORYEVENTVIEW_H #define SQLITEHISTORYEVENTVIEW_H #include "plugineventview.h" #include "filter.h" #include "types.h" #include "sort.h" #include class SQLiteHistoryPlugin; class SQLiteHistoryEventView : public History::PluginEventView { Q_OBJECT public: SQLiteHistoryEventView(SQLiteHistoryPlugin *plugin, History::EventType type, const History::Sort &sort, const History::Filter &filter); ~SQLiteHistoryEventView(); QList NextPage(); int GetTotalCount(); bool IsValid() const; protected: private: SQLiteHistoryPlugin *mPlugin; History::EventType mType; History::Sort mSort; History::Filter mFilter; QSqlQuery mQuery; int mPageSize; int mOffset; bool mValid; QString mTemporaryTable; }; #endif // SQLITEHISTORYEVENTVIEW_H history-service-0.5/plugins/sqlite/sqlitehistoryplugin.cpp000066400000000000000000002314771455450246700243770ustar00rootroot00000000000000/* * Copyright (C) 2013-2016 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 "sqlitehistoryplugin.h" #include "phoneutils_p.h" #include "utils_p.h" #include "sqlitedatabase.h" #include "sqlitehistoryeventview.h" #include "sqlitehistorythreadview.h" #include "intersectionfilter.h" #include "unionfilter.h" #include "thread.h" #include "contactmatcher_p.h" #include "utils_p.h" #include #include #include #include #include #include static const QLatin1String timestampFormat("yyyy-MM-ddTHH:mm:ss.zzz"); QString generateThreadMapKey(const QString &accountId, const QString &threadId) { return accountId + threadId; } QString generateThreadMapKey(const History::Thread &thread) { return generateThreadMapKey(thread.accountId(), thread.threadId()); } SQLiteHistoryPlugin::SQLiteHistoryPlugin(QObject *parent) : QObject(parent), mInitialised(false) { // just trigger the database creation or update SQLiteDatabase::instance(); } bool SQLiteHistoryPlugin::initialised() { return mInitialised; } void SQLiteHistoryPlugin::updateGroupedThreadsCache() { History::PluginThreadView *view = queryThreads(History::EventTypeText, History::Sort("timestamp", Qt::DescendingOrder), History::Filter()); QList threads; while (view->IsValid()) { QList page = view->NextPage(); if (page.size() > 0) { threads += page; } else { break; } } addThreadsToCache(threads); } void SQLiteHistoryPlugin::addThreadsToCache(const QList &threads) { Q_FOREACH (QVariantMap properties, threads) { // FIXME: it would be better to just use UTC all the way through the client, // but that requires a lot of changes // so instead we just convert to UTC here on the cache and convert back to local time // when returning QDateTime timestamp = QDateTime::fromString(properties[History::FieldTimestamp].toString(), Qt::ISODate); properties[History::FieldTimestamp] = timestamp.toUTC().toString(timestampFormat); QDateTime sentTime = QDateTime::fromString(properties[History::FieldSentTime].toString(), Qt::ISODate); properties[History::FieldSentTime] = sentTime.toUTC().toString(timestampFormat); // the same for readTimestamp timestamp = QDateTime::fromString(properties[History::FieldReadTimestamp].toString(), Qt::ISODate); properties[History::FieldReadTimestamp] = timestamp.toUTC().toString(timestampFormat); History::Thread thread = History::Thread::fromProperties(properties); const QString &threadKey = generateThreadMapKey(thread); if (thread.type() != History::EventTypeText) { continue; } else if (!History::Utils::shouldGroupThread(thread)) { // never group non phone accounts mConversationsCache[threadKey] = History::Threads() << thread; mConversationsCacheKeys[threadKey] = threadKey; continue; } // find conversation grouping this thread if (mConversationsCacheKeys.contains(threadKey)) { QString conversationKey = mConversationsCacheKeys[threadKey]; History::Threads groupedThreads = mConversationsCache[conversationKey]; Q_FOREACH(const History::Thread &groupedThread, groupedThreads) { mConversationsCacheKeys.remove(generateThreadMapKey(groupedThread)); } groupedThreads.removeAll(thread); groupedThreads.append(thread); mConversationsCache[conversationKey] = groupedThreads; mConversationsCacheKeys.remove(threadKey); updateDisplayedThread(conversationKey); continue; } // if not found, we have to iterate the list and compare phone numbers bool found = false; QMap::iterator it = mConversationsCache.begin(); while (it != mConversationsCache.end()) { const QString &conversationKey = it.key(); History::Threads groupedThreads = it.value(); Q_FOREACH(const History::Thread &groupedThread, groupedThreads) { if (!History::Utils::shouldGroupThread(groupedThread) || thread.chatType() != groupedThread.chatType()) { continue; } found = History::Utils::compareNormalizedParticipants(thread.participants().identifiers(), groupedThread.participants().identifiers(), History::MatchPhoneNumber); if (found) { Q_FOREACH(const History::Thread &groupedThread, groupedThreads) { mConversationsCacheKeys.remove(generateThreadMapKey(groupedThread)); } mConversationsCache[conversationKey] += thread; updateDisplayedThread(conversationKey); break; } } if (found) { break; } it++; } if (!found) { mConversationsCache[threadKey] = History::Threads() << thread; mConversationsCacheKeys[threadKey] = threadKey; } } } bool SQLiteHistoryPlugin::lessThan(const QVariantMap &left, const QVariantMap &right) const { QVariant leftValue = left[History::FieldLastEventTimestamp]; QVariant rightValue = right[History::FieldLastEventTimestamp]; return leftValue < rightValue; } void SQLiteHistoryPlugin::updateDisplayedThread(const QString &displayedThreadKey) { History::Threads threads = mConversationsCache[displayedThreadKey]; History::Thread displayedThread = threads.first(); QVariantMap displayedProperties = displayedThread.properties(); Q_FOREACH(const History::Thread &other, threads) { if (lessThan(displayedProperties, other.properties())) { displayedThread = other; displayedProperties = displayedThread.properties(); } } QString newDisplayedThreadKey = generateThreadMapKey(displayedThread); mConversationsCache.remove(displayedThreadKey); mConversationsCache[newDisplayedThreadKey] = threads; // update reverse threadId -> conversationId map Q_FOREACH(const History::Thread &groupedThread, threads) { mConversationsCacheKeys[generateThreadMapKey(groupedThread)] = newDisplayedThreadKey; } } void SQLiteHistoryPlugin::removeThreadFromCache(const QVariantMap &properties) { History::Thread thread = History::Thread::fromProperties(properties); QString threadKey = generateThreadMapKey(thread); if (thread.type() != History::EventTypeText || !History::Utils::shouldGroupThread(thread)) { mConversationsCache.remove(threadKey); mConversationsCacheKeys.remove(threadKey); return; } // check if this is a main key first if (mConversationsCache.contains(threadKey)) { // Remove itself from the list and promote the next grouped thread if any History::Threads threads = mConversationsCache[threadKey]; threads.removeAll(thread); mConversationsCache.remove(threadKey); mConversationsCacheKeys.remove(threadKey); // remove all threads from reverse map. they will be readded // in updateDisplayedThread() if needed Q_FOREACH (const History::Thread &thread, threads) { mConversationsCacheKeys.remove(generateThreadMapKey(thread)); } if (!threads.isEmpty()) { threadKey = generateThreadMapKey(threads.first()); mConversationsCache[threadKey] = threads; updateDisplayedThread(threadKey); } } else { // check if it belongs to an existing grouped thread; QMap::iterator it = mConversationsCache.begin(); while (it != mConversationsCache.end()) { const QString &threadKey = it.key(); History::Threads threads = it.value(); int pos = threads.indexOf(thread); if (pos != -1) { const QString &threadKey = generateThreadMapKey(thread); mConversationsCache.remove(threadKey); mConversationsCacheKeys.remove(threadKey); if (threads.size() == 1) { return; } else { threads.removeAll(thread); const QString &newThreadKey = generateThreadMapKey(threads.first()); mConversationsCache[newThreadKey] = threads; updateDisplayedThread(newThreadKey); return; } } it++; } } } /** * @brief Parses the cached thread properties, change fields that might be necessary and return the data * @param thread the thread to extract properties from * @return the thread properties */ QVariantMap SQLiteHistoryPlugin::cachedThreadProperties(const History::Thread &thread) const { QVariantMap properties = thread.properties(); // FIXME: now we need to convert the timestamp back to local time // remove this once we change the flow to use UTC for everything QDateTime timestamp = QDateTime::fromString(properties[History::FieldTimestamp].toString(), Qt::ISODate); timestamp.setTimeSpec(Qt::UTC); properties[History::FieldTimestamp] = toLocalTimeString(timestamp); // and the readTimestamp too timestamp = QDateTime::fromString(properties[History::FieldReadTimestamp].toString(), Qt::ISODate); timestamp.setTimeSpec(Qt::UTC); properties[History::FieldReadTimestamp] = toLocalTimeString(timestamp); return properties; } /** * @brief Generates the cache containing contact data for all known participants. * * FIXME: this should probably be done outside of the plugin, but it requires a * refactory of \ref HistoryDaemon itself. */ void SQLiteHistoryPlugin::generateContactCache() { QTime time; time.start(); qDebug() << "---- HistoryService: start generating cached content"; QSqlQuery query(SQLiteDatabase::instance()->database()); if (!query.exec("SELECT DISTINCT accountId, normalizedId, alias, state FROM thread_participants")) { qWarning() << "Failed to generate contact cache:" << query.lastError().text(); return; } while (query.next()) { QString accountId = query.value(0).toString(); QString participantId = query.value(1).toString(); QString alias = query.value(2).toString(); QVariantMap properties; if (!alias.isEmpty()) { properties[History::FieldAlias] = alias; } // we don't care about the results, as long as the contact data is present in the cache for // future usage. History::ContactMatcher::instance()->contactInfo(accountId, participantId, true, properties); } updateGroupedThreadsCache(); qDebug() << "---- HistoryService: finished generating contact cache. elapsed time:" << time.elapsed() << "ms"; mInitialised = true; } /** * @brief Proceed to migration of ofono accounts in case accountId mismatches current accountId in database * e.g ofono/ofono/account0 to ofono/ofono/ril_0 */ void SQLiteHistoryPlugin::verifyAccount(const QString& accountId) { // we can handle only ofono accounts that end with index number if (!QRegExp("^ofono/ofono.*[0-9]{1}").exactMatch(accountId)) { return; } qDebug() << "verifying Telepathy account" << accountId << "matches database accountId"; QString suffix = accountId.right(1); // extract the index name part QSqlQuery query(SQLiteDatabase::instance()->database()); QString queryText = QString("SELECT DISTINCT accountId FROM threads WHERE accountId LIKE 'ofono/ofono%%1' AND accountId != '%2' ORDER BY accountId ").arg(suffix, accountId); if (!query.exec(queryText)) { qWarning() << "Failed to get threads:" << query.lastError(); return; } QStringList dbAccountIds; while (query.next()) { dbAccountIds << query.value(0).toString(); } if (dbAccountIds.isEmpty()) { qDebug() << "accountId match, nothing to do"; return; } for (const auto &dbAccountId: dbAccountIds) { SQLiteDatabase::instance()->migrateAccount(dbAccountId, accountId); } } // Reader History::PluginThreadView *SQLiteHistoryPlugin::queryThreads(History::EventType type, const History::Sort &sort, const History::Filter &filter, const QVariantMap &properties) { return new SQLiteHistoryThreadView(this, type, sort, filter, properties); } History::PluginEventView *SQLiteHistoryPlugin::queryEvents(History::EventType type, const History::Sort &sort, const History::Filter &filter) { return new SQLiteHistoryEventView(this, type, sort, filter); } QVariantMap SQLiteHistoryPlugin::markThreadAsRead(const QVariantMap &thread) { QSqlQuery query(SQLiteDatabase::instance()->database()); if (thread[History::FieldAccountId].toString().isEmpty() || thread[History::FieldThreadId].toString().isEmpty()) { return QVariantMap(); } // first check if the thread actually has anything to change query.prepare("SELECT unreadCount from threads WHERE accountId=:accountId AND threadId=:threadId AND type=:type"); query.bindValue(":accountId", thread[History::FieldAccountId].toString()); query.bindValue(":threadId", thread[History::FieldThreadId].toString()); query.bindValue(":type", (uint)History::EventTypeText); if (!query.exec() || !query.next()) { qCritical() << "Failed to verify the unread messages of the thread. Error:" << query.lastError(); return QVariantMap(); } int unreadCount = query.value(0).toUInt(); if (unreadCount == 0) { // no messages to ack, so no need to update anything return QVariantMap(); } query.prepare("UPDATE text_events SET newEvent=:newEvent WHERE accountId=:accountId AND threadId=:threadId AND newEvent=1"); query.bindValue(":accountId", thread[History::FieldAccountId].toString()); query.bindValue(":threadId", thread[History::FieldThreadId].toString()); query.bindValue(":newEvent", false); if (!query.exec()) { qCritical() << "Failed to mark thread as read: Error:" << query.lastError(); return QVariantMap(); } QVariantMap existingThread = getSingleThread((History::EventType) thread[History::FieldType].toInt(), thread[History::FieldAccountId].toString(), thread[History::FieldThreadId].toString(), QVariantMap()); if (!existingThread.isEmpty()) { addThreadsToCache(QList() << existingThread); return existingThread; } return QVariantMap(); } QVariantMap SQLiteHistoryPlugin::threadForProperties(const QString &accountId, History::EventType type, const QVariantMap &properties, History::MatchFlags matchFlags) { if (properties.isEmpty()) { return QVariantMap(); } History::ChatType chatType = (History::ChatType)properties[History::FieldChatType].toUInt(); if (chatType == History::ChatTypeRoom) { QString threadId = properties[History::FieldThreadId].toString(); if (threadId.isEmpty()) { return QVariantMap(); } return getSingleThread(type, accountId, threadId); } History::Participants participants = History::Participants::fromVariant(properties[History::FieldParticipantIds]); // if chatType != Room, then we select the thread based on the participant list. return threadForParticipants(accountId, type, participants.identifiers(), matchFlags); } QString SQLiteHistoryPlugin::threadIdForProperties(const QString &accountId, History::EventType type, const QVariantMap &properties, History::MatchFlags matchFlags) { if (properties.isEmpty()) { return QString(); } // if chat type is room, just get the threadId directly History::ChatType chatType = (History::ChatType)properties[History::FieldChatType].toUInt(); if (chatType == History::ChatTypeRoom) { QString threadId = properties[History::FieldThreadId].toString(); return threadId; } // if chat type is anything else, fallback to returning the threadId from the participants list History::Participants participants = History::Participants::fromVariant(properties[History::FieldParticipantIds]); return threadForParticipants(accountId, type, participants.identifiers(), matchFlags)[History::FieldThreadId].toString(); } QList SQLiteHistoryPlugin::participantsForThreads(const QList &threadIds) { QList results; Q_FOREACH(const QVariantMap &thread, threadIds) { QString accountId = thread[History::FieldAccountId].toString(); QString threadId = thread[History::FieldThreadId].toString(); History::EventType type = (History::EventType)thread[History::FieldType].toUInt(); QVariantMap result = thread; QSqlQuery query; query.prepare("SELECT normalizedId, alias, state, roles FROM thread_participants " "WHERE accountId=:accountId AND threadId=:threadId AND type=:type"); query.bindValue(":accountId", accountId); query.bindValue(":threadId", threadId); query.bindValue(":type", type); QVariantList participants; if (!query.exec()) { qWarning() << "Failed to retrieve participants. Error:" << query.lastError().text() << query.lastQuery(); results << result; continue; } while (query.next()) { QVariantMap participant; QString identifier = query.value(0).toString(); participant[History::FieldIdentifier] = identifier; participant[History::FieldAlias] = query.value(1); participant[History::FieldParticipantState] = query.value(2); participant[History::FieldParticipantRoles] = query.value(3); participants << History::ContactMatcher::instance()->contactInfo(accountId, identifier, true, participant); } result[History::FieldParticipants] = participants; results << result; } return results; } int SQLiteHistoryPlugin::unreadCount(History::EventType type) { QSqlQuery query(SQLiteDatabase::instance()->database()); QString queryString("SELECT SUM(unreadCount) FROM threads WHERE type = %1"); if (!query.exec(queryString.arg((int) type))) { qCritical() << "Error:" << query.lastError() << query.lastQuery(); return -1; } if(query.first()) { return query.value(0).toInt(); } else { return -1; } } QVariantMap SQLiteHistoryPlugin::threadForParticipants(const QString &accountId, History::EventType type, const QStringList &participants, History::MatchFlags matchFlags) { if (participants.isEmpty()) { return QVariantMap(); } bool phoneCompare = (matchFlags & History::MatchPhoneNumber); QSqlQuery query(SQLiteDatabase::instance()->database()); // select all the threads the first participant is listed in, and from that list // check if any of the threads has all the other participants listed // FIXME: find a better way to do this QString queryString("SELECT threadId FROM thread_participants WHERE %1 AND type=:type AND accountId=:accountId " "AND (SELECT chatType FROM threads WHERE threads.threadId=thread_participants.threadId AND " " threads.type=thread_participants.type)!=:chatType"); // FIXME: for now we just compare differently when using MatchPhoneNumber QString firstParticipant = participants.first(); if (phoneCompare) { queryString = queryString.arg("compareNormalizedPhoneNumbers(normalizedId, :participantId)"); firstParticipant = History::PhoneUtils::normalizePhoneNumber(firstParticipant); } else { queryString = queryString.arg("participantId=:participantId"); } query.prepare(queryString); query.bindValue(":participantId", firstParticipant); query.bindValue(":type", type); query.bindValue(":accountId", accountId); // we don't want to accidentally return a chat room for a multi-recipient conversation query.bindValue(":chatType", (int)History::ChatTypeRoom); if (!query.exec()) { qCritical() << "Error:" << query.lastError() << query.lastQuery(); return QVariantMap(); } QStringList threadIds; while (query.next()) { threadIds << query.value(0).toString(); } QString existingThread; QStringList normalizedParticipants; if (phoneCompare) { Q_FOREACH(const QString &participant, participants) { normalizedParticipants << History::PhoneUtils::normalizePhoneNumber(participant); } } else { normalizedParticipants = participants; } // now for each threadId, check if all the other participants are listed Q_FOREACH(const QString &threadId, threadIds) { queryString = "SELECT %1 FROM thread_participants WHERE " "threadId=:threadId AND type=:type AND accountId=:accountId"; query.prepare(queryString.arg(phoneCompare ? "normalizedId" : "participantId")); query.bindValue(":threadId", threadId); query.bindValue(":type", type); query.bindValue(":accountId", accountId); if (!query.exec()) { qCritical() << "Error:" << query.lastError() << query.lastQuery(); return QVariantMap(); } QStringList threadParticipants; while (query.next()) { threadParticipants << query.value(0).toString(); } // we can't use query.size() as it always return -1 if (threadParticipants.count() != normalizedParticipants.count()) { continue; } bool found = History::Utils::compareNormalizedParticipants(threadParticipants, normalizedParticipants, matchFlags); if (found) { existingThread = threadId; break; } } return getSingleThread(type, accountId, existingThread); } QList SQLiteHistoryPlugin::eventsForThread(const QVariantMap &thread) { QList results; QString accountId = thread[History::FieldAccountId].toString(); QString threadId = thread[History::FieldThreadId].toString(); History::EventType type = (History::EventType) thread[History::FieldType].toInt(); QString condition = QString("accountId=\"%1\" AND threadId=\"%2\"").arg(accountId, threadId); QString queryText = sqlQueryForEvents(type, condition, ""); QSqlQuery query(SQLiteDatabase::instance()->database()); if (!query.exec(queryText)) { qCritical() << "Error:" << query.lastError() << query.lastQuery(); return results; } results = parseEventResults(type, query); return results; } QList SQLiteHistoryPlugin::attachmentFilePathsForFilter(const History::Filter &filter) { QList attachments; QSqlQuery query(SQLiteDatabase::instance()->database()); QVariantMap filterValues; QString condition = filterToString(filter, filterValues); condition.prepend(" WHERE "); QString queryText = QString("SELECT filePath FROM text_event_attachments JOIN text_events ON text_events.eventId = text_event_attachments.eventId AND text_events.threadId = text_event_attachments.threadId %2").arg(condition); query.prepare(queryText); Q_FOREACH(const QString &key, filterValues.keys()) { query.bindValue(key, filterValues[key]); } if (!query.exec()) { qCritical() << "Error:" << query.lastError() << query.lastQuery(); } while (query.next()) { QString attachment = query.value(0).toString(); attachments << attachment; } query.clear(); return attachments; } QVariantMap SQLiteHistoryPlugin::getSingleThread(History::EventType type, const QString &accountId, const QString &threadId, const QVariantMap &properties) { QVariantMap result; bool grouped = false; if (accountId.isEmpty() || threadId.isEmpty()) { return result; } if (properties.contains(History::FieldGroupingProperty)) { grouped = properties[History::FieldGroupingProperty].toString() == History::FieldParticipants; } if (grouped) { const QString &threadKey = generateThreadMapKey(accountId, threadId); // we have to find which conversation this thread belongs to if (mConversationsCacheKeys.contains(threadKey)) { // found the thread. // get the displayed thread now const History::Threads &groupedThreads = mConversationsCache[mConversationsCacheKeys[threadKey]]; QVariantList finalGroupedThreads; Q_FOREACH(const History::Thread &displayedThread, groupedThreads) { QVariantMap properties = cachedThreadProperties(displayedThread); finalGroupedThreads << properties; if (generateThreadMapKey(displayedThread) == threadKey) { result = properties; } } result[History::FieldGroupedThreads] = QVariant::fromValue(finalGroupedThreads); return result; } return result; } QString condition = QString("accountId=\"%1\" AND threadId=\"%2\"").arg(accountId, threadId); QString queryText = sqlQueryForThreads(type, condition, QString()); queryText += " LIMIT 1"; QSqlQuery query(SQLiteDatabase::instance()->database()); if (!query.exec(queryText)) { qCritical() << "Error:" << query.lastError() << query.lastQuery(); return result; } QList results = parseThreadResults(type, query, properties); query.clear(); if (!results.isEmpty()) { result = results.first(); } return result; } QVariantMap SQLiteHistoryPlugin::getSingleEvent(History::EventType type, const QString &accountId, const QString &threadId, const QString &eventId) { QVariantMap result; QString condition = QString("accountId=\"%1\" AND threadId=\"%2\" AND eventId=\"%3\"").arg(accountId, threadId, eventId); QString queryText = sqlQueryForEvents(type, condition, QString()); queryText += " LIMIT 1"; QSqlQuery query(SQLiteDatabase::instance()->database()); if (!query.exec(queryText)) { qCritical() << "Error:" << query.lastError() << query.lastQuery(); return result; } QList results = parseEventResults(type, query); query.clear(); if (!results.isEmpty()) { result = results.first(); } return result; } bool SQLiteHistoryPlugin::updateRoomParticipants(const QString &accountId, const QString &threadId, History::EventType type, const QVariantList &participants) { QSqlQuery query(SQLiteDatabase::instance()->database()); if (accountId.isEmpty() || threadId.isEmpty()) { return false; } SQLiteDatabase::instance()->beginTransation(); QString deleteString("DELETE FROM thread_participants WHERE threadId=:threadId AND type=:type AND accountId=:accountId"); query.prepare(deleteString); query.bindValue(":accountId", accountId); query.bindValue(":threadId", threadId); query.bindValue(":type", type); if (!query.exec()) { qCritical() << "Error removing old participants:" << query.lastError() << query.lastQuery(); SQLiteDatabase::instance()->rollbackTransaction(); return false; } // and insert the participants Q_FOREACH(const QVariant &participantVariant, participants) { QVariantMap participant = participantVariant.toMap(); query.prepare("INSERT INTO thread_participants (accountId, threadId, type, participantId, normalizedId, alias, state, roles)" "VALUES (:accountId, :threadId, :type, :participantId, :normalizedId, :alias, :state, :roles)"); query.bindValue(":accountId", accountId); query.bindValue(":threadId", threadId); query.bindValue(":type", type); query.bindValue(":participantId", participant["identifier"].toString()); query.bindValue(":normalizedId", participant["identifier"].toString()); query.bindValue(":alias", participant["alias"].toString()); query.bindValue(":state", participant["state"].toUInt()); query.bindValue(":roles", participant["roles"].toUInt()); if (!query.exec()) { qCritical() << "Error:" << query.lastError() << query.lastQuery(); SQLiteDatabase::instance()->rollbackTransaction(); return false; } } if (!SQLiteDatabase::instance()->finishTransaction()) { qCritical() << "Failed to commit the transaction."; return false; } QVariantMap existingThread = getSingleThread(type, accountId, threadId, QVariantMap()); if (!existingThread.isEmpty()) { addThreadsToCache(QList() << existingThread); } return true; } bool SQLiteHistoryPlugin::updateRoomParticipantsRoles(const QString &accountId, const QString &threadId, History::EventType type, const QVariantMap &participantsRoles) { QSqlQuery query(SQLiteDatabase::instance()->database()); if (accountId.isEmpty() || threadId.isEmpty()) { return false; } SQLiteDatabase::instance()->beginTransation(); Q_FOREACH(const QString &participantId, participantsRoles.keys()) { query.prepare("UPDATE thread_participants SET roles=:roles WHERE accountId=:accountId AND threadId=:threadId AND type=:type AND participantId=:participantId"); query.bindValue(":roles", participantsRoles.value(participantId).toUInt()); query.bindValue(":accountId", accountId); query.bindValue(":threadId", threadId); query.bindValue(":type", type); query.bindValue(":participantId", participantId); if (!query.exec()) { qCritical() << "Error:" << query.lastError() << query.lastQuery(); SQLiteDatabase::instance()->rollbackTransaction(); return false; } } if (!SQLiteDatabase::instance()->finishTransaction()) { qCritical() << "Failed to commit the transaction."; return false; } QVariantMap existingThread = getSingleThread(type, accountId, threadId, QVariantMap()); if (!existingThread.isEmpty()) { addThreadsToCache(QList() << existingThread); } return true; } bool SQLiteHistoryPlugin::updateRoomInfo(const QString &accountId, const QString &threadId, History::EventType type, const QVariantMap &properties, const QStringList& /* invalidated */) { QSqlQuery query(SQLiteDatabase::instance()->database()); if (threadId.isEmpty() || accountId.isEmpty()) { return false; } SQLiteDatabase::instance()->beginTransation(); QDateTime creationTimestamp = QDateTime::fromTime_t(properties["CreationTimestamp"].toUInt()); QDateTime timestamp = QDateTime::fromTime_t(properties["Timestamp"].toUInt()); QVariantMap propertyMapping; propertyMapping["RoomName"] = "roomName"; propertyMapping["Server"] = "server"; propertyMapping["Creator"] = "creator"; propertyMapping["CreationTimestamp"] = "creationTimestamp"; propertyMapping["Anonymous"] = "anonymous"; propertyMapping["InviteOnly"] = "inviteOnly"; propertyMapping["Limit"] = "participantLimit"; propertyMapping["Moderated"] = "moderated"; propertyMapping["Title"] = "title"; propertyMapping["Description"] = "description"; propertyMapping["Persistent"] = "persistent"; propertyMapping["Private"] = "private"; propertyMapping["PasswordProtected"] = "passwordProtected"; propertyMapping["Password"] = "password"; propertyMapping["PasswordHint"] = "passwordHint"; propertyMapping["CanUpdateConfiguration"] = "canUpdateConfiguration"; propertyMapping["Subject"] = "subject"; propertyMapping["Actor"] = "actor"; propertyMapping["Timestamp"] = "timestamp"; propertyMapping["Joined"] = "joined"; propertyMapping["SelfRoles"] = "selfRoles"; QStringList changedPropListValues; // populate sql query Q_FOREACH (const QString &key, properties.keys()) { if (propertyMapping.contains(key)) { QString prop = propertyMapping[key].toString(); changedPropListValues << QString(prop+"=:"+ prop); } } if (changedPropListValues.isEmpty()) { return false; } query.prepare("UPDATE chat_room_info SET "+ changedPropListValues.join(", ")+" WHERE accountId=:accountId AND threadId=:threadId AND type=:type"); query.bindValue(":accountId", accountId); query.bindValue(":threadId", threadId); query.bindValue(":type", (int) type); query.bindValue(":roomName", properties["RoomName"].toString()); query.bindValue(":server", properties["Server"].toString()); query.bindValue(":creator", properties["Creator"].toString()); query.bindValue(":creationTimestamp", creationTimestamp.toUTC().toString(timestampFormat)); query.bindValue(":anonymous", properties["Anonymous"].toBool()); query.bindValue(":inviteOnly", properties["InviteOnly"].toBool()); query.bindValue(":participantLimit", properties["Limit"].toInt()); query.bindValue(":moderated", properties["Moderated"].toBool()); query.bindValue(":title", properties["Title"].toString()); query.bindValue(":description", properties["Description"].toString()); query.bindValue(":persistent", properties["Persistent"].toBool()); query.bindValue(":private", properties["Private"].toBool()); query.bindValue(":passwordProtected", properties["PasswordProtected"].toBool()); query.bindValue(":password", properties["Password"].toString()); query.bindValue(":passwordHint", properties["PasswordHint"].toString()); query.bindValue(":canUpdateConfiguration", properties["CanUpdateConfiguration"].toBool()); query.bindValue(":subject", properties["Subject"].toString()); query.bindValue(":actor", properties["Actor"].toString()); query.bindValue(":timestamp", timestamp.toUTC().toString(timestampFormat)); query.bindValue(":joined", properties["Joined"].toBool()); query.bindValue(":selfRoles", properties["SelfRoles"].toInt()); if (!query.exec()) { qCritical() << "Error:" << query.lastError() << query.lastQuery(); SQLiteDatabase::instance()->rollbackTransaction(); return false; } if (!SQLiteDatabase::instance()->finishTransaction()) { qCritical() << "Failed to commit the transaction."; return false; } QVariantMap existingThread = getSingleThread(type, accountId, threadId, QVariantMap()); if (!existingThread.isEmpty()) { addThreadsToCache(QList() << existingThread); } return true; } QVariantMap SQLiteHistoryPlugin::createThreadForProperties(const QString &accountId, History::EventType type, const QVariantMap &properties) { // WARNING: this function does NOT test to check if the thread is already created, you should check using HistoryReader::threadForParticipants() QVariantMap thread; History::Participants participants = History::Participants::fromVariant(properties[History::FieldParticipantIds]); // Create a new thread // FIXME: define what the threadId will be QString threadId; History::ChatType chatType = (History::ChatType)properties[History::FieldChatType].toInt(); QVariantMap chatRoomInfo; SQLiteDatabase::instance()->beginTransation(); if (chatType == History::ChatTypeRoom) { threadId = properties[History::FieldThreadId].toString(); // we cannot save chat room without threadId if (accountId.isEmpty() || threadId.isEmpty()) { SQLiteDatabase::instance()->rollbackTransaction(); return thread; } chatRoomInfo = properties[History::FieldChatRoomInfo].toMap(); QSqlQuery query(SQLiteDatabase::instance()->database()); QDateTime creationTimestamp = QDateTime::fromTime_t(chatRoomInfo["CreationTimestamp"].toUInt()); QDateTime timestamp = QDateTime::fromTime_t(chatRoomInfo["Timestamp"].toUInt()); query.prepare("INSERT INTO chat_room_info (accountId, threadId, type, roomName, server, creator, creationTimestamp, anonymous, inviteOnly, participantLimit, moderated, title, description, persistent, private, passwordProtected, password, passwordHint, canUpdateConfiguration, subject, actor, timestamp, joined, selfRoles) " "VALUES (:accountId, :threadId, :type, :roomName, :server, :creator, :creationTimestamp, :anonymous, :inviteOnly, :participantLimit, :moderated, :title, :description, :persistent, :private, :passwordProtected, :password, :passwordHint, :canUpdateConfiguration, :subject, :actor, :timestamp, :joined, :selfRoles)"); query.bindValue(":accountId", accountId); query.bindValue(":threadId", threadId); query.bindValue(":type", (int) type); query.bindValue(":roomName", chatRoomInfo["RoomName"].toString()); query.bindValue(":server", chatRoomInfo["Server"].toString()); query.bindValue(":creator", chatRoomInfo["Creator"].toString()); query.bindValue(":creationTimestamp", creationTimestamp.toUTC().toString(timestampFormat)); query.bindValue(":anonymous", chatRoomInfo["Anonymous"].toBool()); query.bindValue(":inviteOnly", chatRoomInfo["InviteOnly"].toBool()); query.bindValue(":participantLimit", chatRoomInfo["Limit"].toInt()); query.bindValue(":moderated", chatRoomInfo["Moderated"].toBool()); query.bindValue(":title", chatRoomInfo["Title"].toString()); query.bindValue(":description", chatRoomInfo["Description"].toString()); query.bindValue(":persistent", chatRoomInfo["Persistent"].toBool()); query.bindValue(":private", chatRoomInfo["Private"].toBool()); query.bindValue(":passwordProtected", chatRoomInfo["PasswordProtected"].toBool()); query.bindValue(":password", chatRoomInfo["Password"].toString()); query.bindValue(":passwordHint", chatRoomInfo["PasswordHint"].toString()); query.bindValue(":canUpdateConfiguration", chatRoomInfo["CanUpdateConfiguration"].toBool()); query.bindValue(":subject", chatRoomInfo["Subject"].toString()); query.bindValue(":actor", chatRoomInfo["Actor"].toString()); query.bindValue(":timestamp", timestamp.toUTC().toString(timestampFormat)); query.bindValue(":joined", chatRoomInfo["Joined"].toBool()); query.bindValue(":selfRoles", chatRoomInfo["SelfRoles"].toInt()); if (!query.exec()) { qCritical() << "Error:" << query.lastError() << query.lastQuery(); SQLiteDatabase::instance()->rollbackTransaction(); return QVariantMap(); } for (QVariantMap::iterator iter = chatRoomInfo.begin(); iter != chatRoomInfo.end();) { if (!iter.value().isValid()) { iter = chatRoomInfo.erase(iter); } else { iter++; } } thread[History::FieldChatRoomInfo] = chatRoomInfo; } else if (chatType == History::ChatTypeContact) { threadId = participants.identifiers().join("%"); } else { threadId = QString("broadcast:%1").arg(QString(QCryptographicHash::hash(participants.identifiers().join(";").toLocal8Bit(),QCryptographicHash::Md5).toHex()));; } QSqlQuery query(SQLiteDatabase::instance()->database()); query.prepare("INSERT INTO threads (accountId, threadId, type, count, unreadCount, chatType, lastEventTimestamp)" "VALUES (:accountId, :threadId, :type, :count, :unreadCount, :chatType, :lastEventTimestamp)"); query.bindValue(":accountId", accountId); query.bindValue(":threadId", threadId); query.bindValue(":type", (int) type); query.bindValue(":count", 0); query.bindValue(":unreadCount", 0); query.bindValue(":chatType", (int) chatType); // make sure threads are created with an up-to-date timestamp query.bindValue(":lastEventTimestamp", QDateTime::currentDateTimeUtc().toString(timestampFormat)); if (!query.exec()) { qCritical() << "Error:" << query.lastError() << query.lastQuery(); SQLiteDatabase::instance()->rollbackTransaction(); return QVariantMap(); } // and insert the participants Q_FOREACH(const History::Participant &participant, participants) { query.prepare("INSERT INTO thread_participants (accountId, threadId, type, participantId, normalizedId, alias, state, roles)" "VALUES (:accountId, :threadId, :type, :participantId, :normalizedId, :alias, :state, :roles)"); query.bindValue(":accountId", accountId); query.bindValue(":threadId", threadId); query.bindValue(":type", type); query.bindValue(":participantId", participant.identifier()); query.bindValue(":normalizedId", History::Utils::normalizeId(accountId, participant.identifier())); query.bindValue(":alias", participant.alias()); query.bindValue(":state", participant.state()); query.bindValue(":roles", participant.roles()); if (!query.exec()) { qCritical() << "Error:" << query.lastError() << query.lastQuery(); SQLiteDatabase::instance()->rollbackTransaction(); return QVariantMap(); } } if (!SQLiteDatabase::instance()->finishTransaction()) { qCritical() << "Failed to commit the transaction."; return QVariantMap(); } // and finally create the thread thread[History::FieldAccountId] = accountId; thread[History::FieldThreadId] = threadId; thread[History::FieldType] = (int) type; QVariantList contactList; QVariantList contactInfo = History::ContactMatcher::instance()->contactInfo(accountId, participants.identifiers(), true); for (int i = 0; i < participants.count(); ++i) { QVariantMap map = contactInfo[i].toMap(); History::Participant participant = participants[i]; map["state"] = participant.state(); map["roles"] = participant.roles(); contactList << map; } thread[History::FieldParticipants] = contactList; thread[History::FieldCount] = 0; thread[History::FieldUnreadCount] = 0; thread[History::FieldChatType] = (int)chatType; addThreadsToCache(QList() << thread); return thread; } // Writer QVariantMap SQLiteHistoryPlugin::createThreadForParticipants(const QString &accountId, History::EventType type, const QStringList &participants) { QVariantMap properties; properties[History::FieldParticipantIds] = participants; properties[History::FieldChatType] = participants.size() != 1 ? History::ChatTypeNone : History::ChatTypeContact; return createThreadForProperties(accountId, type, properties); } bool SQLiteHistoryPlugin::removeThread(const QVariantMap &thread) { QSqlQuery query(SQLiteDatabase::instance()->database()); query.prepare("DELETE FROM threads WHERE accountId=:accountId AND threadId=:threadId AND type=:type"); query.bindValue(":accountId", thread[History::FieldAccountId]); query.bindValue(":threadId", thread[History::FieldThreadId]); query.bindValue(":type", thread[History::FieldType]); if (!query.exec()) { qCritical() << "Failed to remove the thread: Error:" << query.lastError() << query.lastQuery(); return false; } removeThreadFromCache(thread); return true; } History::EventWriteResult SQLiteHistoryPlugin::writeTextEvent(const QVariantMap &event) { QSqlQuery query(SQLiteDatabase::instance()->database()); // check if the event exists QVariantMap existingEvent = getSingleEvent((History::EventType) event[History::FieldType].toInt(), event[History::FieldAccountId].toString(), event[History::FieldThreadId].toString(), event[History::FieldEventId].toString()); SQLiteDatabase::instance()->beginTransation(); History::EventWriteResult result; if (existingEvent.isEmpty()) { // create new query.prepare("INSERT INTO text_events (accountId, threadId, eventId, senderId, timestamp, newEvent, message, messageType, messageStatus, readTimestamp, subject, informationType, sentTime)" "VALUES (:accountId, :threadId, :eventId, :senderId, :timestamp, :newEvent, :message, :messageType, :messageStatus, :readTimestamp, :subject, :informationType, :sentTime)"); result = History::EventWriteCreated; } else { // update existing event query.prepare("UPDATE text_events SET senderId=:senderId, timestamp=:timestamp, sentTime=:sentTime, newEvent=:newEvent, message=:message, messageType=:messageType, informationType=:informationType, " "messageStatus=:messageStatus, readTimestamp=:readTimestamp, subject=:subject, informationType=:informationType WHERE accountId=:accountId AND threadId=:threadId AND eventId=:eventId"); result = History::EventWriteModified; } query.bindValue(":accountId", event[History::FieldAccountId]); query.bindValue(":threadId", event[History::FieldThreadId]); query.bindValue(":eventId", event[History::FieldEventId]); query.bindValue(":senderId", event[History::FieldSenderId]); query.bindValue(":timestamp", event[History::FieldTimestamp].toDateTime().toUTC()); query.bindValue(":sentTime", event[History::FieldSentTime].toDateTime().toUTC()); query.bindValue(":newEvent", event[History::FieldNewEvent]); query.bindValue(":message", event[History::FieldMessage]); query.bindValue(":messageType", event[History::FieldMessageType]); query.bindValue(":messageStatus", event[History::FieldMessageStatus]); query.bindValue(":readTimestamp", event[History::FieldReadTimestamp].toDateTime().toUTC()); query.bindValue(":subject", event[History::FieldSubject].toString()); query.bindValue(":informationType", event[History::FieldInformationType].toInt()); if (!query.exec()) { qCritical() << "Failed to save the text event: Error:" << query.lastError() << query.lastQuery(); SQLiteDatabase::instance()->rollbackTransaction(); return History::EventWriteError; } History::MessageType messageType = (History::MessageType) event[History::FieldMessageType].toInt(); if (messageType == History::MessageTypeMultiPart) { // if the writing is an update, we need to remove the previous attachments if (result == History::EventWriteModified) { query.prepare("DELETE FROM text_event_attachments WHERE accountId=:accountId AND threadId=:threadId " "AND eventId=:eventId"); query.bindValue(":accountId", event[History::FieldAccountId]); query.bindValue(":threadId", event[History::FieldThreadId]); query.bindValue(":eventId", event[History::FieldEventId]); if (!query.exec()) { qCritical() << "Could not erase previous attachments. Error:" << query.lastError() << query.lastQuery(); SQLiteDatabase::instance()->rollbackTransaction(); return History::EventWriteError; } } // save the attachments QList attachments = qdbus_cast >(event[History::FieldAttachments]); Q_FOREACH(const QVariantMap &attachment, attachments) { query.prepare("INSERT INTO text_event_attachments VALUES (:accountId, :threadId, :eventId, :attachmentId, :contentType, :filePath, :status)"); query.bindValue(":accountId", attachment[History::FieldAccountId]); query.bindValue(":threadId", attachment[History::FieldThreadId]); query.bindValue(":eventId", attachment[History::FieldEventId]); query.bindValue(":attachmentId", attachment[History::FieldAttachmentId]); query.bindValue(":contentType", attachment[History::FieldContentType]); query.bindValue(":filePath", attachment[History::FieldFilePath]); query.bindValue(":status", attachment[History::FieldStatus]); if (!query.exec()) { qCritical() << "Failed to save attachment to database" << query.lastError() << attachment; SQLiteDatabase::instance()->rollbackTransaction(); return History::EventWriteError; } } } if (!SQLiteDatabase::instance()->finishTransaction()) { qCritical() << "Failed to commit transaction."; return History::EventWriteError; } if (result == History::EventWriteModified || result == History::EventWriteCreated) { QVariantMap existingThread = getSingleThread((History::EventType) event[History::FieldType].toInt(), event[History::FieldAccountId].toString(), event[History::FieldThreadId].toString(), QVariantMap()); addThreadsToCache(QList() << existingThread); } return result; } bool SQLiteHistoryPlugin::removeTextEvent(const QVariantMap &event) { QSqlQuery query(SQLiteDatabase::instance()->database()); query.prepare("DELETE FROM text_events WHERE accountId=:accountId AND threadId=:threadId AND eventId=:eventId"); query.bindValue(":accountId", event[History::FieldAccountId]); query.bindValue(":threadId", event[History::FieldThreadId]); query.bindValue(":eventId", event[History::FieldEventId]); if (!query.exec()) { qCritical() << "Failed to remove the text event: Error:" << query.lastError() << query.lastQuery(); return false; } QVariantMap existingThread = getSingleThread((History::EventType) event[History::FieldType].toInt(), event[History::FieldAccountId].toString(), event[History::FieldThreadId].toString(), QVariantMap()); if (!existingThread.isEmpty()) { addThreadsToCache(QList() << existingThread); } return true; } History::EventWriteResult SQLiteHistoryPlugin::writeVoiceEvent(const QVariantMap &event) { QSqlQuery query(SQLiteDatabase::instance()->database()); // check if the event exists QVariantMap existingEvent = getSingleEvent((History::EventType) event[History::FieldType].toInt(), event[History::FieldAccountId].toString(), event[History::FieldThreadId].toString(), event[History::FieldEventId].toString()); History::EventWriteResult result; if (existingEvent.isEmpty()) { // create new query.prepare("INSERT INTO voice_events (accountId, threadId, eventId, senderId, timestamp, newEvent, duration, missed, remoteParticipant) " "VALUES (:accountId, :threadId, :eventId, :senderId, :timestamp, :newEvent, :duration, :missed, :remoteParticipant)"); result = History::EventWriteCreated; } else { // update existing event query.prepare("UPDATE voice_events SET senderId=:senderId, timestamp=:timestamp, newEvent=:newEvent, duration=:duration, " "missed=:missed, remoteParticipant=:remoteParticipant " "WHERE accountId=:accountId AND threadId=:threadId AND eventId=:eventId"); result = History::EventWriteModified; } query.bindValue(":accountId", event[History::FieldAccountId]); query.bindValue(":threadId", event[History::FieldThreadId]); query.bindValue(":eventId", event[History::FieldEventId]); query.bindValue(":senderId", event[History::FieldSenderId]); query.bindValue(":timestamp", event[History::FieldTimestamp].toDateTime().toUTC()); query.bindValue(":newEvent", event[History::FieldNewEvent]); query.bindValue(":duration", event[History::FieldDuration]); query.bindValue(":missed", event[History::FieldMissed]); query.bindValue(":remoteParticipant", event[History::FieldRemoteParticipant]); if (!query.exec()) { qCritical() << "Failed to save the voice event: Error:" << query.lastError() << query.lastQuery(); result = History::EventWriteError; } return result; } bool SQLiteHistoryPlugin::removeVoiceEvent(const QVariantMap &event) { QSqlQuery query(SQLiteDatabase::instance()->database()); query.prepare("DELETE FROM voice_events WHERE accountId=:accountId AND threadId=:threadId AND eventId=:eventId"); query.bindValue(":accountId", event[History::FieldAccountId]); query.bindValue(":threadId", event[History::FieldThreadId]); query.bindValue(":eventId", event[History::FieldEventId]); if (!query.exec()) { qCritical() << "Failed to remove the voice event: Error:" << query.lastError() << query.lastQuery(); return false; } return true; } int SQLiteHistoryPlugin::removeEvents(History::EventType type, const History::Filter &filter) { QString table; switch (type) { case History::EventTypeText: table = "text_events"; break; case History::EventTypeVoice: table = "voice_events"; break; case History::EventTypeNull: qWarning("SQLiteHistoryPlugin::sqlQueryForThreads: Got EventTypeNull, ignoring!"); return -1; break; } QSqlQuery query(SQLiteDatabase::instance()->database()); QVariantMap filterValues; QString condition = filterToString(filter, filterValues); condition.prepend(" WHERE "); QString queryText = QString("DELETE FROM %1 %2").arg(table).arg(condition); query.prepare(queryText); Q_FOREACH(const QString &key, filterValues.keys()) { query.bindValue(key, filterValues[key]); } if (!query.exec()) { qWarning() << "Failed to remove events. Error:" << query.lastError(); return -1; } int deletedEvents = query.numRowsAffected(); if (deletedEvents <= 0) { return deletedEvents; } // now need to remove threads that have count == 0 if any QSqlQuery queryThread(SQLiteDatabase::instance()->database()); queryThread.prepare("DELETE FROM threads WHERE type=:type AND count=0"); queryThread.bindValue(":type", type); if (!queryThread.exec()) { qCritical() << "Failed to remove threads: Error:" << queryThread.lastError() << queryThread.lastQuery(); return -1; } // refresh cache if (queryThread.numRowsAffected()>0) { updateGroupedThreadsCache(); } return deletedEvents; } int SQLiteHistoryPlugin::eventsCount(History::EventType type, const History::Filter &filter) { QString table; switch (type) { case History::EventTypeText: table = "text_events"; break; case History::EventTypeVoice: table = "voice_events"; break; case History::EventTypeNull: qWarning("SQLiteHistoryPlugin::sqlQueryForThreads: Got EventTypeNull, ignoring!"); return 0; break; } QSqlQuery query(SQLiteDatabase::instance()->database()); QVariantMap filterValues; QString condition = filterToString(filter, filterValues); condition.prepend(" WHERE "); QString queryText = QString("SELECT count(*) FROM %1 %2").arg(table).arg(condition); query.prepare(queryText); Q_FOREACH(const QString &key, filterValues.keys()) { query.bindValue(key, filterValues[key]); } if (!query.exec() || !query.next()) { qWarning() << "Failed to get total count. Error:" << query.lastError(); return 0; } return query.value(0).toUInt(); } bool SQLiteHistoryPlugin::beginBatchOperation() { return SQLiteDatabase::instance()->beginTransation(); } bool SQLiteHistoryPlugin::endBatchOperation() { return SQLiteDatabase::instance()->finishTransaction(); } bool SQLiteHistoryPlugin::rollbackBatchOperation() { return SQLiteDatabase::instance()->rollbackTransaction(); } QString SQLiteHistoryPlugin::sqlQueryForThreads(History::EventType type, const QString &condition, const QString &order) { QString modifiedCondition = condition; if (!modifiedCondition.isEmpty()) { modifiedCondition.prepend(" AND "); // FIXME: the filters should be implemented in a better way modifiedCondition.replace("accountId=", "threads.accountId="); modifiedCondition.replace("threadId=", "threads.threadId="); modifiedCondition.replace("count=", "threads.count="); modifiedCondition.replace("unreadCount=", "threads.unreadCount="); } QString modifiedOrder = order; if (!modifiedOrder.isEmpty()) { modifiedOrder.replace(" accountId", " threads.accountId"); modifiedOrder.replace(" threadId", " threads.threadId"); modifiedOrder.replace(" count", " threads.count"); modifiedOrder.replace(" unreadCount", " threads.unreadCount"); } QStringList fields; fields << "threads.accountId" << "threads.threadId" << "threads.lastEventId" << "threads.count" << "threads.unreadCount" << "threads.lastEventTimestamp"; QStringList extraFields; QString table; switch (type) { case History::EventTypeText: table = "text_events"; extraFields << "text_events.message" << "text_events.messageType" << "text_events.messageStatus" << "text_events.readTimestamp" << "chatType" << "text_events.subject" << "text_events.informationType" << "text_events.sentTime"; break; case History::EventTypeVoice: table = "voice_events"; extraFields << "voice_events.duration" << "voice_events.missed" << "voice_events.remoteParticipant"; break; case History::EventTypeNull: qWarning("SQLiteHistoryPlugin::sqlQueryForThreads: Got EventTypeNull, ignoring this event!"); break; } fields << QString("%1.senderId").arg(table) << QString("%1.newEvent").arg(table); fields << extraFields; QString queryText = QString("SELECT %1 FROM threads LEFT JOIN %2 ON threads.threadId=%2.threadId AND " "threads.accountId=%2.accountId AND threads.lastEventId=%2.eventId WHERE threads.type=%3 %4 %5") .arg(fields.join(", "), table, QString::number((int)type), modifiedCondition, modifiedOrder); return queryText; } QList SQLiteHistoryPlugin::parseThreadResults(History::EventType type, QSqlQuery &query, const QVariantMap &properties) { QList threads; QList threadsWithoutParticipants; QSqlQuery attachmentsQuery(SQLiteDatabase::instance()->database()); QList attachments; bool grouped = false; if (properties.contains(History::FieldGroupingProperty)) { grouped = properties[History::FieldGroupingProperty].toString() == History::FieldParticipants; } while (query.next()) { QVariantMap thread; QString accountId = query.value(0).toString(); QString threadId = query.value(1).toString(); if (threadId.trimmed().isEmpty()) { continue; } thread[History::FieldType] = (int) type; thread[History::FieldAccountId] = accountId; thread[History::FieldThreadId] = threadId; if (grouped) { const QString &threadKey = generateThreadMapKey(accountId, threadId); if (mInitialised && type == History::EventTypeText && !mConversationsCache.contains(threadKey)) { continue; } QVariantList groupedThreads; if (mConversationsCache.contains(threadKey)) { Q_FOREACH (const History::Thread &thread, mConversationsCache[threadKey]) { groupedThreads << cachedThreadProperties(thread); } } thread[History::FieldGroupedThreads] = QVariant::fromValue(groupedThreads); } thread[History::FieldEventId] = query.value(2); thread[History::FieldCount] = query.value(3); thread[History::FieldUnreadCount] = query.value(4); // the generic event fields thread[History::FieldSenderId] = query.value(6); thread[History::FieldTimestamp] = toLocalTimeString(query.value(5).toDateTime()); thread[History::FieldNewEvent] = query.value(7).toBool(); // the next step is to get the last event switch (type) { case History::EventTypeText: attachmentsQuery.prepare("SELECT attachmentId, contentType, filePath, status FROM text_event_attachments " "WHERE accountId=:accountId and threadId=:threadId and eventId=:eventId"); attachmentsQuery.bindValue(":accountId", query.value(0)); attachmentsQuery.bindValue(":threadId", query.value(1)); attachmentsQuery.bindValue(":eventId", query.value(2)); if (!attachmentsQuery.exec()) { qCritical() << "Error:" << attachmentsQuery.lastError() << attachmentsQuery.lastQuery(); } while (attachmentsQuery.next()) { QVariantMap attachment; attachment[History::FieldAccountId] = query.value(0); attachment[History::FieldThreadId] = query.value(1); attachment[History::FieldEventId] = query.value(2); attachment[History::FieldAttachmentId] = attachmentsQuery.value(0); attachment[History::FieldContentType] = attachmentsQuery.value(1); attachment[History::FieldFilePath] = attachmentsQuery.value(2); attachment[History::FieldStatus] = attachmentsQuery.value(3); attachments << attachment; } attachmentsQuery.clear(); if (attachments.size() > 0) { thread[History::FieldAttachments] = QVariant::fromValue(attachments); attachments.clear(); } thread[History::FieldMessage] = query.value(8); thread[History::FieldMessageType] = query.value(9); thread[History::FieldMessageStatus] = query.value(10); thread[History::FieldReadTimestamp] = toLocalTimeString(query.value(11).toDateTime()); thread[History::FieldChatType] = query.value(12).toUInt(); if (thread[History::FieldChatType].toInt() == History::ChatTypeRoom) { QVariantMap chatRoomInfo; QSqlQuery query1(SQLiteDatabase::instance()->database()); query1.prepare("SELECT roomName, server, creator, creationTimestamp, anonymous, inviteOnly, participantLimit, moderated, title, description, persistent, private, passwordProtected, password, passwordHint, canUpdateConfiguration, subject, actor, timestamp, joined, selfRoles FROM chat_room_info WHERE accountId=:accountId AND threadId=:threadId AND type=:type LIMIT 1"); query1.bindValue(":accountId", thread[History::FieldAccountId]); query1.bindValue(":threadId", thread[History::FieldThreadId]); query1.bindValue(":type", thread[History::FieldType].toInt()); if (!query1.exec()) { qCritical() << "Failed to get chat room info for thread: Error:" << query1.lastError() << query1.lastQuery(); break; } query1.next(); if (query1.value(0).isValid()) chatRoomInfo["RoomName"] = query1.value(0); if (query1.value(1).isValid()) chatRoomInfo["Server"] = query1.value(1); if (query1.value(2).isValid()) chatRoomInfo["Creator"] = query1.value(2); if (query1.value(3).isValid()) chatRoomInfo["CreationTimestamp"] = toLocalTimeString(query1.value(3).toDateTime()); if (query1.value(4).isValid()) chatRoomInfo["Anonymous"] = query1.value(4).toBool(); if (query1.value(5).isValid()) chatRoomInfo["InviteOnly"] = query1.value(5).toBool(); if (query1.value(6).isValid()) chatRoomInfo["Limit"] = query1.value(6).toInt(); if (query1.value(7).isValid()) chatRoomInfo["Moderated"] = query1.value(7).toBool(); if (query1.value(8).isValid()) chatRoomInfo["Title"] = query1.value(8); if (query1.value(9).isValid()) chatRoomInfo["Description"] = query1.value(9); if (query1.value(10).isValid()) chatRoomInfo["Persistent"] = query1.value(10).toBool(); if (query1.value(11).isValid()) chatRoomInfo["Private"] = query1.value(11).toBool(); if (query1.value(12).isValid()) chatRoomInfo["PasswordProtected"] = query1.value(12).toBool(); if (query1.value(13).isValid()) chatRoomInfo["Password"] = query1.value(13); if (query1.value(14).isValid()) chatRoomInfo["PasswordHint"] = query1.value(14); if (query1.value(15).isValid()) chatRoomInfo["CanUpdateConfiguration"] = query1.value(15).toBool(); if (query1.value(16).isValid()) chatRoomInfo["Subject"] = query1.value(16); if (query1.value(17).isValid()) chatRoomInfo["Actor"] = query1.value(17); if (query1.value(18).isValid()) chatRoomInfo["Timestamp"] = toLocalTimeString(query1.value(18).toDateTime()); if (query1.value(19).isValid()) chatRoomInfo["Joined"] = query1.value(19).toBool(); if (query1.value(20).isValid()) chatRoomInfo["SelfRoles"] = query1.value(20).toInt(); thread[History::FieldChatRoomInfo] = chatRoomInfo; } if (!History::Utils::shouldIncludeParticipants(History::Thread::fromProperties(thread))) { thread.remove(History::FieldParticipants); threadsWithoutParticipants << thread; } else { threads << thread; } break; case History::EventTypeVoice: thread[History::FieldMissed] = query.value(9); thread[History::FieldDuration] = query.value(8); thread[History::FieldRemoteParticipant] = History::ContactMatcher::instance()->contactInfo(accountId, query.value(10).toString(), true); threads << thread; break; case History::EventTypeNull: qWarning("SQLiteHistoryPlugin::parseThreadResults: Got EventTypeNull, ignoring this event!"); break; } } // get the participants threads = participantsForThreads(threads); // and append the threads with no participants threads << threadsWithoutParticipants; return threads; } QString SQLiteHistoryPlugin::sqlQueryForEvents(History::EventType type, const QString &condition, const QString &order) { QString modifiedCondition = condition; if (!modifiedCondition.isEmpty()) { modifiedCondition.prepend(" WHERE "); } QString participantsField = "(SELECT group_concat(thread_participants.participantId, \"|,|\") " "FROM thread_participants WHERE thread_participants.accountId=%1.accountId " "AND thread_participants.threadId=%1.threadId " "AND thread_participants.type=%2 GROUP BY accountId,threadId,type) as participants"; QString queryText; switch (type) { case History::EventTypeText: // for text events we don't need the participants at all participantsField = "\"\" as participants"; queryText = QString("SELECT accountId, threadId, eventId, senderId, timestamp, newEvent, %1, " "message, messageType, messageStatus, readTimestamp, subject, informationType, sentTime FROM text_events %2 %3").arg(participantsField, modifiedCondition, order); break; case History::EventTypeVoice: participantsField = participantsField.arg("voice_events", QString::number(type)); queryText = QString("SELECT accountId, threadId, eventId, senderId, timestamp, newEvent, %1, " "duration, missed, remoteParticipant FROM voice_events %2 %3").arg(participantsField, modifiedCondition, order); break; case History::EventTypeNull: qWarning("SQLiteHistoryPlugin::sqlQueryForEvents: Got EventTypeNull, ignoring this event!"); break; } return queryText; } QList SQLiteHistoryPlugin::parseEventResults(History::EventType type, QSqlQuery &query) { QList events; while (query.next()) { QVariantMap event; History::MessageType messageType; QString accountId = query.value(0).toString(); QString threadId = query.value(1).toString(); QString eventId = query.value(2).toString(); // ignore events that don't have a threadId or an eventId if (threadId.trimmed().isEmpty() || eventId.trimmed().isEmpty()) { continue; } event[History::FieldType] = (int) type; event[History::FieldAccountId] = accountId; event[History::FieldThreadId] = threadId; event[History::FieldEventId] = eventId; event[History::FieldSenderId] = query.value(3); event[History::FieldTimestamp] = toLocalTimeString(query.value(4).toDateTime()); event[History::FieldNewEvent] = query.value(5).toBool(); if (type != History::EventTypeText) { QStringList participants = query.value(6).toString().split("|,|"); event[History::FieldParticipants] = History::ContactMatcher::instance()->contactInfo(accountId, participants, true); } switch (type) { case History::EventTypeText: messageType = (History::MessageType) query.value(8).toInt(); if (messageType == History::MessageTypeMultiPart) { QSqlQuery attachmentsQuery(SQLiteDatabase::instance()->database()); attachmentsQuery.prepare("SELECT attachmentId, contentType, filePath, status FROM text_event_attachments " "WHERE accountId=:accountId and threadId=:threadId and eventId=:eventId"); attachmentsQuery.bindValue(":accountId", accountId); attachmentsQuery.bindValue(":threadId", threadId); attachmentsQuery.bindValue(":eventId", eventId); if (!attachmentsQuery.exec()) { qCritical() << "Error:" << attachmentsQuery.lastError() << attachmentsQuery.lastQuery(); } QList attachments; while (attachmentsQuery.next()) { QVariantMap attachment; attachment[History::FieldAccountId] = accountId; attachment[History::FieldThreadId] = threadId; attachment[History::FieldEventId] = eventId; attachment[History::FieldAttachmentId] = attachmentsQuery.value(0); attachment[History::FieldContentType] = attachmentsQuery.value(1); attachment[History::FieldFilePath] = attachmentsQuery.value(2); attachment[History::FieldStatus] = attachmentsQuery.value(3); attachments << attachment; } attachmentsQuery.clear(); event[History::FieldAttachments] = QVariant::fromValue(attachments); } event[History::FieldMessage] = query.value(7); event[History::FieldMessageType] = query.value(8); event[History::FieldMessageStatus] = query.value(9); event[History::FieldReadTimestamp] = toLocalTimeString(query.value(10).toDateTime()); event[History::FieldSentTime] = toLocalTimeString(query.value(13).toDateTime()); if (!query.value(11).toString().isEmpty()) { event[History::FieldSubject] = query.value(11).toString(); } event[History::FieldInformationType] = query.value(12).toInt(); break; case History::EventTypeVoice: event[History::FieldDuration] = query.value(7).toInt(); event[History::FieldMissed] = query.value(8); event[History::FieldRemoteParticipant] = query.value(9).toString(); break; case History::EventTypeNull: qWarning("SQLiteHistoryPlugin::parseEventResults: Got EventTypeNull, ignoring this event!"); break; } events << event; } return events; } QString SQLiteHistoryPlugin::toLocalTimeString(const QDateTime ×tamp) { return QDateTime(timestamp.date(), timestamp.time(), Qt::UTC).toLocalTime().toString(timestampFormat); } QString SQLiteHistoryPlugin::filterToString(const History::Filter &filter, QVariantMap &bindValues, const QString &propertyPrefix) const { QString result; History::Filters filters; QString linking; QString value; int count; QString filterProperty = filter.filterProperty(); QVariant filterValue = filter.filterValue(); switch (filter.type()) { case History::FilterTypeIntersection: filters = History::IntersectionFilter(filter).filters(); linking = " AND "; // fall through case History::FilterTypeUnion: if (filter.type() == History::FilterTypeUnion) { filters = History::UnionFilter(filter).filters(); linking = " OR "; } if (filters.isEmpty()) { break; } result = "( "; count = filters.count(); for (int i = 0; i < count; ++i) { // run recursively through the inner filters result += QString("(%1)").arg(filterToString(filters[i], bindValues, propertyPrefix)); if (i != count-1) { result += linking; } } result += " )"; break; default: if (filterProperty.isEmpty() || filterValue.isNull()) { break; } QString bindId = QString(":filterValue%1").arg(bindValues.count()); QString propertyName = propertyPrefix.isNull() ? filterProperty : QString("%1.%2").arg(propertyPrefix, filterProperty); // FIXME: need to check for other match flags and multiple match flags if (filter.matchFlags() & History::MatchContains) { // FIXME: maybe we should use QString("%1 LIKE '\%'||%2'\%'").arg(bindId) ?? needs more time for investigating result = QString("%1 LIKE '\%%2\%' ESCAPE '\\'").arg(propertyName, escapeFilterValue(filterValue.toString())); } else { switch (filter.matchFlags()) { case History::MatchNotEquals: result = QString("%1!=%2").arg(propertyName, bindId); break; case History::MatchLess: result = QString("%1<%2").arg(propertyName, bindId); break; case History::MatchGreater: result = QString("%1>%2").arg(propertyName, bindId); break; case History::MatchLessOrEquals: result = QString("%1<=%2").arg(propertyName, bindId); break; case History::MatchGreaterOrEquals: result = QString("%1>=%2").arg(propertyName, bindId); break; default: result = QString("%1=%2").arg(propertyName, bindId); } bindValues[bindId] = filterValue; } } return result; } QString SQLiteHistoryPlugin::escapeFilterValue(const QString &value) const { QString escaped = value; escaped.replace("\\", "\\\\") .replace("'", "''") .replace("%", "\\%") .replace("_", "\\_"); return escaped; } history-service-0.5/plugins/sqlite/sqlitehistoryplugin.h000066400000000000000000000140571455450246700240350ustar00rootroot00000000000000/* * Copyright (C) 2013-2016 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 SQLITEHISTORYPLUGIN_H #define SQLITEHISTORYPLUGIN_H #include "plugin.h" #include "thread.h" #include #include class SQLiteHistoryReader; class SQLiteHistoryWriter; typedef QSharedPointer SQLiteHistoryReaderPtr; typedef QSharedPointer SQLiteHistoryWriterPtr; class SQLiteHistoryPlugin : public QObject, History::Plugin { Q_OBJECT Q_PLUGIN_METADATA(IID "com.lomiri.historyservice.Plugin") Q_INTERFACES(History::Plugin) public: explicit SQLiteHistoryPlugin(QObject *parent = 0); bool initialised(); // Reader part of the plugin History::PluginThreadView* queryThreads(History::EventType type, const History::Sort &sort = History::Sort(), const History::Filter &filter = History::Filter(), const QVariantMap &properties = QVariantMap()); History::PluginEventView* queryEvents(History::EventType type, const History::Sort &sort = History::Sort(), const History::Filter &filter = History::Filter()); QVariantMap threadForParticipants(const QString &accountId, History::EventType type, const QStringList &participants, History::MatchFlags matchFlags = History::MatchCaseSensitive); QVariantMap threadForProperties(const QString &accountId, History::EventType type, const QVariantMap &properties, History::MatchFlags matchFlags = History::MatchCaseSensitive); QString threadIdForProperties(const QString &accountId, History::EventType type, const QVariantMap &properties, History::MatchFlags matchFlags = History::MatchCaseSensitive) override; QList participantsForThreads(const QList &threadIds) override; QList eventsForThread(const QVariantMap &thread); QList attachmentFilePathsForFilter(const History::Filter &filter); QVariantMap getSingleThread(History::EventType type, const QString &accountId, const QString &threadId, const QVariantMap &properties = QVariantMap()); QVariantMap getSingleEvent(History::EventType type, const QString &accountId, const QString &threadId, const QString &eventId); // Writer part of the plugin QVariantMap createThreadForProperties(const QString &accountId, History::EventType type, const QVariantMap &properties); QVariantMap createThreadForParticipants(const QString &accountId, History::EventType type, const QStringList &participants); bool updateRoomParticipants(const QString &accountId, const QString &threadId, History::EventType type, const QVariantList &participants); bool updateRoomParticipantsRoles(const QString &accountId, const QString &threadId, History::EventType type, const QVariantMap &participantsRoles); bool updateRoomInfo(const QString &accountId, const QString &threadId, History::EventType type, const QVariantMap &properties, const QStringList &invalidated = QStringList()); bool removeThread(const QVariantMap &thread); QVariantMap markThreadAsRead(const QVariantMap &thread); History::EventWriteResult writeTextEvent(const QVariantMap &event); bool removeTextEvent(const QVariantMap &event); History::EventWriteResult writeVoiceEvent(const QVariantMap &event); bool removeVoiceEvent(const QVariantMap &event); int removeEvents(History::EventType type, const History::Filter &filter); int eventsCount(History::EventType type, const History::Filter &filter); int unreadCount(History::EventType type); bool beginBatchOperation(); bool endBatchOperation(); bool rollbackBatchOperation(); // functions to be used internally QString sqlQueryForThreads(History::EventType type, const QString &condition, const QString &order); QList parseThreadResults(History::EventType type, QSqlQuery &query, const QVariantMap &properties = QVariantMap()); QString sqlQueryForEvents(History::EventType type, const QString &condition, const QString &order); QList parseEventResults(History::EventType type, QSqlQuery &query); static QString toLocalTimeString(const QDateTime ×tamp); QString filterToString(const History::Filter &filter, QVariantMap &bindValues, const QString &propertyPrefix = QString()) const; QString escapeFilterValue(const QString &value) const; void generateContactCache(); void verifyAccount(const QString& accountId); private: bool lessThan(const QVariantMap &left, const QVariantMap &right) const; void updateGroupedThreadsCache(); void updateDisplayedThread(const QString &displayedThreadKey); void addThreadsToCache(const QList &threads); void removeThreadFromCache(const QVariantMap &thread); QVariantMap cachedThreadProperties(const History::Thread &thread) const; QMap mConversationsCache; QMap mConversationsCacheKeys; bool mInitialised; }; #endif // SQLITEHISTORYPLUGIN_H history-service-0.5/plugins/sqlite/sqlitehistorythreadview.cpp000066400000000000000000000076651455450246700252430ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 "sqlitehistorythreadview.h" #include "sqlitedatabase.h" #include "sqlitehistoryplugin.h" #include "sort.h" #include #include #include SQLiteHistoryThreadView::SQLiteHistoryThreadView(SQLiteHistoryPlugin *plugin, History::EventType type, const History::Sort &sort, const History::Filter &filter, const QVariantMap &properties) : History::PluginThreadView(), mPlugin(plugin), mType(type), mSort(sort), mFilter(filter), mPageSize(15), mQuery(SQLiteDatabase::instance()->database()), mOffset(0), mValid(true), mQueryProperties(properties) { mTemporaryTable = QString("threadview%1%2").arg(QString::number((qulonglong)this), QDateTime::currentDateTimeUtc().toString("yyyyMMddhhmmsszzz")); mQuery.setForwardOnly(true); // FIXME: validate the filter QVariantMap filterValues; QString condition = mPlugin->filterToString(filter, filterValues); QString order; if (!sort.sortField().isNull()) { // WORKAROUND: Supports multiple fields by split it using ',' Q_FOREACH(const QString& field, sort.sortField().split(",")) { order += QString("%1 %2, ") .arg(field.trimmed()) .arg(sort.sortOrder() == Qt::AscendingOrder ? "ASC" : "DESC"); } order = QString("ORDER BY %1").arg(order.mid(0, order.lastIndexOf(","))); // FIXME: check case sensitiviy } QString queryText = QString("CREATE TEMP TABLE %1 AS ").arg(mTemporaryTable); queryText += mPlugin->sqlQueryForThreads(type, condition, order); if (!mQuery.prepare(queryText)) { qCritical() << "Error:" << mQuery.lastError() << mQuery.lastQuery(); mValid = false; Q_EMIT Invalidated(); return; } Q_FOREACH(const QString &key, filterValues.keys()) { mQuery.bindValue(key, filterValues[key]); } // create the temporary table if (!mQuery.exec()) { qCritical() << "Error:" << mQuery.lastError() << mQuery.lastQuery(); mValid = false; Q_EMIT Invalidated(); return; } } SQLiteHistoryThreadView::~SQLiteHistoryThreadView() { if (!mQuery.exec(QString("DROP TABLE IF EXISTS %1").arg(mTemporaryTable))) { qCritical() << "Error:" << mQuery.lastError() << mQuery.lastQuery(); return; } } QList SQLiteHistoryThreadView::NextPage() { QList threads; // now prepare for selecting from it mQuery.prepare(QString("SELECT * FROM %1 LIMIT %2 OFFSET %3").arg(mTemporaryTable, QString::number(mPageSize), QString::number(mOffset))); if (!mQuery.exec()) { qCritical() << "Error:" << mQuery.lastError() << mQuery.lastQuery(); mValid = false; Q_EMIT Invalidated(); return threads; } threads = mPlugin->parseThreadResults(mType, mQuery, mQueryProperties); mOffset += mPageSize; mQuery.clear(); return threads; } bool SQLiteHistoryThreadView::IsValid() const { return mValid; } history-service-0.5/plugins/sqlite/sqlitehistorythreadview.h000066400000000000000000000033031455450246700246710ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 SQLITEHISTORYTHREADVIEW_H #define SQLITEHISTORYTHREADVIEW_H #include "pluginthreadview.h" #include "filter.h" #include "types.h" #include "sort.h" #include class SQLiteHistoryPlugin; class SQLiteHistoryThreadView : public History::PluginThreadView { Q_OBJECT public: SQLiteHistoryThreadView(SQLiteHistoryPlugin *plugin, History::EventType type, const History::Sort &sort, const History::Filter &filter, const QVariantMap &properties); ~SQLiteHistoryThreadView(); QList NextPage(); bool IsValid() const; private: SQLiteHistoryPlugin *mPlugin; History::EventType mType; History::Sort mSort; History::Filter mFilter; int mPageSize; QSqlQuery mQuery; QString mTemporaryTable; int mOffset; bool mValid; QVariantMap mQueryProperties; }; #endif // SQLITEHISTORYTHREADVIEW_H history-service-0.5/plugins/sqlite/update_qrc.sh000077500000000000000000000005731455450246700222060ustar00rootroot00000000000000#!/bin/sh QRC_FILE=$1 FILES=`ls schema/*.sql schema/*.info` # clear the file if [ -e $QRC_FILE ]; then rm -f $QRC_FILE fi # and print the contents echo '' >> $QRC_FILE echo ' ' >> $QRC_FILE for file in $FILES; do echo " $file" >> $QRC_FILE done echo ' ' >> $QRC_FILE echo '' >> $QRC_FILE history-service-0.5/src/000077500000000000000000000000001455450246700153205ustar00rootroot00000000000000history-service-0.5/src/CMakeLists.txt000066400000000000000000000053521455450246700200650ustar00rootroot00000000000000set(library_SRCS channelobserver.cpp contactmatcher.cpp event.cpp eventview.cpp filter.cpp intersectionfilter.cpp manager.cpp managerdbus.cpp mmsstoragemanager.cpp participant.cpp phoneutils.cpp pluginthreadview.cpp plugineventview.cpp sort.cpp telepathyhelper.cpp textevent.cpp texteventattachment.cpp thread.cpp threadview.cpp unionfilter.cpp utils.cpp voiceevent.cpp ) set(library_HDRS Event event.h EventView eventview.h Filter filter.h IntersectionFilter intersectionfilter.h Manager manager.h Participant participant.h Plugin plugin.h PluginThreadView pluginthreadview.h PluginEventView plugineventview.h Sort sort.h TextEvent textevent.h TextEventAttachment texteventattachment.h Thread thread.h ThreadView threadview.h Types types.h UnionFilter unionfilter.h VoiceEvent voiceevent.h ) set(library_PRIV_HDRS channelobserver_p.h contactmatcher_p.h event_p.h eventview_p.h filter_p.h intersectionfilter_p.h manager_p.h managerdbus_p.h mmsstoragemanager_p.h participant_p.h phoneutils_p.h pluginthreadview_p.h plugineventview_p.h sort_p.h telepathyhelper_p.h textevent_p.h texteventattachment_p.h thread_p.h threadview_p.h unionfilter_p.h utils_p.h voiceevent_p.h ) qt5_add_dbus_adaptor(library_SRCS PluginThreadView.xml pluginthreadview.h History::PluginThreadView) qt5_add_dbus_adaptor(library_SRCS PluginEventView.xml plugineventview.h History::PluginEventView) include_directories(${CMAKE_SOURCE_DIR}/src ${CMAKE_CURRENT_BINARY_DIR} ${LibPhoneNumber_INCLUDE_DIRS} ${TP_QT5_INCLUDE_DIRS}) add_library(historyservice SHARED ${library_SRCS} ${library_HDRS} ${library_PRIV_HDRS}) # Set the library version and the SOVERSION set_target_properties(historyservice PROPERTIES SOVERSION ${HISTORY_VERSION_MAJOR} VERSION ${HISTORY_VERSION_MAJOR}.${HISTORY_VERSION_MINOR}.${HISTORY_VERSION_PATCH}) target_link_libraries(historyservice ${LibPhoneNumber_LIBRARIES} ${TP_QT5_LIBRARIES}) qt5_use_modules(historyservice Contacts Core DBus) install(TARGETS historyservice DESTINATION ${CMAKE_INSTALL_LIBDIR}) set(INCLUDE_INSTALL_DIR include/${PROJECT_NAME}) install(FILES ${library_HDRS} DESTINATION ${INCLUDE_INSTALL_DIR}/History) configure_file(${CMAKE_CURRENT_SOURCE_DIR}/${PROJECT_NAME}.pc.in ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}.pc) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}.pc DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig COMPONENT pkgconfig) history-service-0.5/src/Event000066400000000000000000000000231455450246700163170ustar00rootroot00000000000000#include "event.h" history-service-0.5/src/EventView000066400000000000000000000000271455450246700171560ustar00rootroot00000000000000#include "eventview.h" history-service-0.5/src/Filter000066400000000000000000000000241455450246700164640ustar00rootroot00000000000000#include "filter.h" history-service-0.5/src/IntersectionFilter000066400000000000000000000000401455450246700210510ustar00rootroot00000000000000#include "intersectionfilter.h" history-service-0.5/src/Manager000066400000000000000000000000251455450246700166120ustar00rootroot00000000000000#include "manager.h" history-service-0.5/src/Participant000066400000000000000000000000311455450246700175130ustar00rootroot00000000000000#include "participant.h" history-service-0.5/src/Plugin000066400000000000000000000000241455450246700164750ustar00rootroot00000000000000#include "plugin.h" history-service-0.5/src/PluginEventView000066400000000000000000000001571455450246700203410ustar00rootroot00000000000000#ifndef PLUGINEVENTVIEW_H #define PLUGINEVENTVIEW_H #include "plugineventview.h" #endif // PLUGINEVENTVIEW_H history-service-0.5/src/PluginEventView.xml000066400000000000000000000032001455450246700211300ustar00rootroot00000000000000 An interface to the history service EventView object. history-service-0.5/src/PluginThreadView000066400000000000000000000001621455450246700204630ustar00rootroot00000000000000#ifndef PLUGINTHREADVIEW_H #define PLUGINTHREADVIEW_H #include "pluginthreadview.h" #endif // PLUGINTHREADVIEW_H history-service-0.5/src/PluginThreadView.xml000066400000000000000000000026521455450246700212700ustar00rootroot00000000000000 An interface to the history service ThreadView object. history-service-0.5/src/Sort000066400000000000000000000000221455450246700161640ustar00rootroot00000000000000#include "sort.h" history-service-0.5/src/TextEvent000066400000000000000000000000271455450246700171700ustar00rootroot00000000000000#include "textevent.h" history-service-0.5/src/TextEventAttachment000066400000000000000000000000411455450246700211750ustar00rootroot00000000000000#include "texteventattachment.h" history-service-0.5/src/Thread000066400000000000000000000000241455450246700164460ustar00rootroot00000000000000#include "thread.h" history-service-0.5/src/ThreadView000066400000000000000000000000301455450246700172760ustar00rootroot00000000000000#include "threadview.h" history-service-0.5/src/Types000066400000000000000000000000231455450246700163420ustar00rootroot00000000000000#include "types.h" history-service-0.5/src/UnionFilter000066400000000000000000000000311455450246700174730ustar00rootroot00000000000000#include "unionfilter.h" history-service-0.5/src/VoiceEvent000066400000000000000000000000301455450246700173030ustar00rootroot00000000000000#include "voiceevent.h" history-service-0.5/src/channelobserver.cpp000066400000000000000000000160131455450246700212050ustar00rootroot00000000000000/* * Copyright (C) 2012-2015 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 "channelobserver_p.h" #include "types.h" #include #include #include #include #include namespace History { ChannelObserver::ChannelObserver(QObject *parent) : QObject(parent), Tp::AbstractClientObserver(channelFilters(), true) { } Tp::ChannelClassSpecList ChannelObserver::channelFilters() const { Tp::ChannelClassSpecList specList; specList << Tp::ChannelClassSpec::audioCall(); specList << Tp::ChannelClassSpec::textChat(); specList << Tp::ChannelClassSpec::textChatroom(); specList << Tp::ChannelClassSpec::unnamedTextChat(); return specList; } void ChannelObserver::observeChannels(const Tp::MethodInvocationContextPtr<> &context, const Tp::AccountPtr &account, const Tp::ConnectionPtr &connection, const QList &channels, const Tp::ChannelDispatchOperationPtr &dispatchOperation, const QList &requestsSatisfied, const Tp::AbstractClientObserver::ObserverInfo &observerInfo) { Q_UNUSED(account) Q_UNUSED(connection) Q_UNUSED(dispatchOperation) Q_UNUSED(requestsSatisfied) Q_UNUSED(observerInfo) Q_FOREACH (Tp::ChannelPtr channel, channels) { // tp-qt has not support for the SMS interface if (channel->immutableProperties().contains(TP_QT_IFACE_CHANNEL_INTERFACE_SMS + QLatin1String(".Flash"))) { if (channel->immutableProperties()[TP_QT_IFACE_CHANNEL_INTERFACE_SMS + QLatin1String(".Flash")].toBool()) { continue; } } mContexts[channel.data()] = context; mChannels.append(channel); connect(channel.data(), SIGNAL(invalidated(Tp::DBusProxy*,const QString&, const QString&)), SLOT(onChannelInvalidated())); channel->setProperty(History::FieldAccountId, account->uniqueIdentifier()); qDebug() << "Saving account id:" << account->uniqueIdentifier(); Tp::CallChannelPtr callChannel = Tp::CallChannelPtr::dynamicCast(channel); if (callChannel) { Tp::PendingReady *ready = callChannel->becomeReady(Tp::Features() << Tp::CallChannel::FeatureCore << Tp::CallChannel::FeatureCallMembers << Tp::CallChannel::FeatureCallState << Tp::CallChannel::FeatureContents << Tp::CallChannel::FeatureLocalHoldState); connect(ready, SIGNAL(finished(Tp::PendingOperation*)), SLOT(onCallChannelReady(Tp::PendingOperation*))); mReadyMap[ready] = callChannel; } Tp::TextChannelPtr textChannel = Tp::TextChannelPtr::dynamicCast(channel); if (textChannel) { Tp::PendingReady *ready = textChannel->becomeReady(Tp::Features() << Tp::TextChannel::FeatureCore << Tp::TextChannel::FeatureChatState << Tp::TextChannel::FeatureMessageCapabilities << Tp::TextChannel::FeatureMessageQueue << Tp::TextChannel::FeatureMessageSentSignal); connect(ready, SIGNAL(finished(Tp::PendingOperation*)), SLOT(onTextChannelReady(Tp::PendingOperation*))); mReadyMap[ready] = textChannel; } } } void ChannelObserver::onCallChannelReady(Tp::PendingOperation *op) { Tp::PendingReady *ready = qobject_cast(op); if (!ready) { qCritical() << "Pending operation is not a pending ready:" << op; return; } if (!mReadyMap.contains(ready)) { qWarning() << "Pending ready finished but not on the map:" << ready; return; } Tp::CallChannelPtr callChannel = Tp::CallChannelPtr::dynamicCast(mReadyMap[ready]); mReadyMap.remove(ready); if (!callChannel) { qWarning() << "Ready channel is not a call channel:" << callChannel; return; } Q_EMIT callChannelAvailable(callChannel); checkContextFinished(callChannel.data()); } void ChannelObserver::onChannelInvalidated() { Tp::ChannelPtr channel(qobject_cast(sender())); mChannels.removeAll(channel); } void ChannelObserver::onTextChannelReady(Tp::PendingOperation *op) { Tp::PendingReady *ready = qobject_cast(op); if (!ready) { qCritical() << "Pending operation is not a pending ready:" << op; return; } if (!mReadyMap.contains(ready)) { qWarning() << "Pending ready finished but not on the map:" << ready; return; } Tp::TextChannelPtr textChannel = Tp::TextChannelPtr::dynamicCast(mReadyMap[ready]); mReadyMap.remove(ready); if (!textChannel) { qWarning() << "Ready channel is not a call channel:" << textChannel; return; } Q_EMIT textChannelAvailable(textChannel); checkContextFinished(textChannel.data()); } void ChannelObserver::checkContextFinished(Tp::Channel *channel) { if (!mContexts.contains(channel)) { qWarning() << "Context for channel not available:" << channel; return; } Tp::MethodInvocationContextPtr<> context = mContexts[channel]; mContexts.remove(channel); // check if this is the last channel from the context Q_FOREACH(Tp::MethodInvocationContextPtr<> otherContext, mContexts.values()) { // if we find the context, just return from the function. We need to wait // for the other channels to become ready before setting the context finished if (otherContext == context) { return; } } context->setFinished(); } } history-service-0.5/src/channelobserver_p.h000066400000000000000000000043161455450246700211740ustar00rootroot00000000000000/* * Copyright (C) 2012-2015 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 CHANNELOBSERVER_H #define CHANNELOBSERVER_H #include #include #include #include namespace History { class ChannelObserver : public QObject, public Tp::AbstractClientObserver { Q_OBJECT public: explicit ChannelObserver(QObject *parent = 0); Tp::ChannelClassSpecList channelFilters() const; void observeChannels(const Tp::MethodInvocationContextPtr<> &context, const Tp::AccountPtr &account, const Tp::ConnectionPtr &connection, const QList &channels, const Tp::ChannelDispatchOperationPtr &dispatchOperation, const QList &requestsSatisfied, const Tp::AbstractClientObserver::ObserverInfo &observerInfo); Q_SIGNALS: void textChannelAvailable(const Tp::TextChannelPtr &channel); void callChannelAvailable(const Tp::CallChannelPtr &channel); protected Q_SLOTS: void onChannelInvalidated(); void onCallChannelReady(Tp::PendingOperation *op); void onTextChannelReady(Tp::PendingOperation *op); protected: void checkContextFinished(Tp::Channel *channel); private: QMap > mContexts; QMap mReadyMap; QList mChannels; }; } #endif // CHANNELOBSERVER_H history-service-0.5/src/contactmatcher.cpp000066400000000000000000000431661455450246700210350ustar00rootroot00000000000000/* * Copyright (C) 2014-2016 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 "contactmatcher_p.h" #include "phoneutils_p.h" #include "telepathyhelper_p.h" #include "types.h" #include "utils_p.h" #include #include #include #include #include #include #include #include #include using namespace QtContacts; namespace History { ContactMatcher::ContactMatcher(QContactManager *manager, QObject *parent) : QObject(parent), mManager(manager) { if (!mManager) { mManager = new QContactManager("galera"); } // just trigger the creation of TelepathyHelper connect(History::TelepathyHelper::instance(), SIGNAL(setupReady()), SLOT(onSetupReady())); QObject::connect(mManager, &QContactManager::contactsAdded, this, &ContactMatcher::onContactsAdded); QObject::connect(mManager, &QContactManager::contactsChanged, this, &ContactMatcher::onContactsChanged); QObject::connect(mManager, &QContactManager::contactsRemoved, this, &ContactMatcher::onContactsRemoved); QObject::connect(mManager, &QContactManager::dataChanged, this, &ContactMatcher::onDataChanged); } void ContactMatcher::onSetupReady() { Q_FOREACH(const RequestInfo &request, mPendingRequests) { requestContactInfo(request.accountId, request.identifier); } mPendingRequests.clear(); } ContactMatcher::~ContactMatcher() { Q_FOREACH(QContactFetchRequest *request, mRequests.keys()) { request->cancel(); request->deleteLater(); } mRequests.clear(); mContactMap.clear(); mManager->disconnect(this); mManager->deleteLater(); } ContactMatcher *ContactMatcher::instance(QContactManager *manager) { static ContactMatcher self(manager); return &self; } /** * \brief Returns the contact information for the given \param identifier, taking into account * the addressable fields of the given \param accountId. * If \param synchronous is specified, a blocking synchronous request will be made to the contact * manager to return the specified data. * * Note that synchronous requests should only be placed after \ref TelepathyHelper is ready. */ QVariantMap ContactMatcher::contactInfo(const QString &accountId, const QString &identifier, bool synchronous, const QVariantMap &properties) { InternalContactMap &internalMap = mContactMap[accountId]; QString normalizedId = normalizeId(identifier); QVariantMap map; // first do a simple string match on the map if (internalMap.contains(normalizedId)) { map = internalMap[normalizedId]; } else if (History::TelepathyHelper::instance()->ready()) { // and if there was no match, asynchronously request the info, and return an empty map for now map = requestContactInfo(accountId, normalizedId, synchronous); } else if (!synchronous) { RequestInfo info{accountId, normalizedId}; mPendingRequests.append(info); } map[History::FieldIdentifier] = normalizedId; map[History::FieldAccountId] = accountId; QMapIterator i(properties); while (i.hasNext()) { i.next(); if (!map.contains(i.key())) { map[i.key()] = i.value(); } } mContactMap[accountId][normalizedId] = map; return map; } QVariantList ContactMatcher::contactInfo(const QString &accountId, const QStringList &identifiers, bool synchronous) { QVariantList contacts; Q_FOREACH(const QString &identifier, identifiers) { contacts << contactInfo(accountId, identifier, synchronous); } return contacts; } void ContactMatcher::watchIdentifier(const QString &accountId, const QString &identifier, const QVariantMap ¤tInfo) { // only add the identifier to the map of watched identifiers QVariantMap map = currentInfo; map[History::FieldIdentifier] = identifier; mContactMap[accountId][identifier] = map; } void ContactMatcher::onContactsAdded(QList ids) { QList contacts = mManager->contacts(ids); // walk through the list of requested phone numbers ContactMap::iterator it = mContactMap.begin(); ContactMap::iterator end = mContactMap.end(); for (; it != end; ++it) { QString accountId = it.key(); InternalContactMap &internalMap = it.value(); InternalContactMap::iterator it2 = internalMap.begin(); InternalContactMap::iterator end2 = internalMap.end(); for (; it2 != end2; ++it2) { QString identifier = it2.key(); // skip entries that already have a match if (hasMatch(it2.value())) { continue; } // now for each entry not populated, check if it matches one of the newly added contacts Q_FOREACH(const QContact &contact, contacts) { QVariantMap map = matchAndUpdate(accountId, identifier, contact); if (hasMatch(map)){ break; } } } } } void ContactMatcher::onContactsChanged(QList ids) { QList contacts = mManager->contacts(ids); // walk through the list of requested phone numbers ContactMap::iterator it = mContactMap.begin(); ContactMap::iterator end = mContactMap.end(); for (; it != end; ++it) { QString accountId = it.key(); InternalContactMap &internalMap = it.value(); InternalContactMap::iterator it2 = internalMap.begin(); InternalContactMap::iterator end2 = internalMap.end(); QStringList identifiersToMatch; for (; it2 != end2; ++it2) { QVariantMap &contactInfo = it2.value(); QString identifier = it2.key(); Q_FOREACH(const QContact &contact, contacts) { bool previousMatch = (contactInfo.contains(History::FieldContactId) && contactInfo[History::FieldContactId].toString() == contact.id().toString()); QVariantMap map = matchAndUpdate(accountId, identifier, contact); if (hasMatch(map)){ break; } else if (previousMatch) { // if there was a previous match but it does not match anymore, try to match the phone number // to a different contact identifiersToMatch << identifier; break; } } } Q_FOREACH(const QString &identifier, identifiersToMatch) { internalMap.remove(identifier); requestContactInfo(accountId, identifier); } } } void ContactMatcher::onContactsRemoved(QList ids) { // search for entries that were matching this contact ContactMap::iterator it = mContactMap.begin(); ContactMap::iterator end = mContactMap.end(); for (; it != end; ++it) { QString accountId = it.key(); InternalContactMap &internalMap = it.value(); InternalContactMap::iterator it2 = internalMap.begin(); InternalContactMap::iterator end2 = internalMap.end(); QStringList identifiersToMatch; for (; it2 != end2; ++it2) { QVariantMap &info = it2.value(); // skip entries that didn't have a match if (!hasMatch(info)) { continue; } Q_FOREACH(const QContactId &id, ids) { if (id.toString() == info[History::FieldContactId].toString()) { identifiersToMatch << it2.key(); break; } } } // now make sure to try a new match on the phone numbers whose contact was removed Q_FOREACH(const QString &identifier, identifiersToMatch) { internalMap.remove(identifier); Q_EMIT contactInfoChanged(accountId, identifier, contactInfo(accountId, identifier)); } } } void ContactMatcher::onDataChanged() { ContactMap::iterator it = mContactMap.begin(); ContactMap::iterator end = mContactMap.end(); for (; it != end; ++it) { QString accountId = it.key(); InternalContactMap &internalMap = it.value(); // invalidate the cache QStringList identifiers = internalMap.keys(); internalMap.clear(); Q_FOREACH(const QString &identifier, identifiers) { QVariantMap info; info[History::FieldIdentifier] = identifier; Q_EMIT contactInfoChanged(accountId, identifier, info); requestContactInfo(accountId, identifier); } } } void ContactMatcher::onRequestStateChanged(QContactAbstractRequest::State state) { QContactFetchRequest *request = qobject_cast(sender()); if (!request) { return; } if (!mRequests.contains(request)) { request->deleteLater(); return; } if (state == QContactAbstractRequest::FinishedState) { request->deleteLater(); RequestInfo info = mRequests.take(request); QContact contact; if (!request->contacts().isEmpty()) { contact = request->contacts().first(); } matchAndUpdate(info.accountId, info.identifier, contact); } else if (state == QContactAbstractRequest::CanceledState) { request->deleteLater(); mRequests.remove(request); } } /** * \brief Requests contact info, and if the preference is for a synchronous request returns the contact information in a * QVariantMap. For asynchronous requests, an empty QVariantMap is returned. */ QVariantMap ContactMatcher::requestContactInfo(const QString &accountId, const QString &identifier, bool synchronous) { QString normalizedId = normalizeId(identifier); QStringList addressableVCardFields = addressableFields(accountId); QVariantMap contactInfo; contactInfo[History::FieldIdentifier] = identifier; contactInfo[History::FieldAccountId] = accountId; if (addressableVCardFields.isEmpty()) { mContactMap[accountId][identifier] = contactInfo; // FIXME: add support for generic accounts return contactInfo; } bool phoneCompare = addressableVCardFields.contains("tel"); QContactFetchHint hint; hint.setMaxCountHint(1); // FIXME: maybe we need to fetch the full contact? hint.setDetailTypesHint(QList() << QContactDetail::TypeDisplayLabel << QContactDetail::TypePhoneNumber << QContactDetail::TypeAvatar << QContactDetail::TypeExtendedDetail); QContactUnionFilter topLevelFilter; Q_FOREACH(const QString &field, addressableVCardFields) { if (field == "tel") { topLevelFilter.append(QContactPhoneNumber::match(normalizedId)); } else { // FIXME: handle more fields // rely on a generic field filter QContactDetailFilter nameFilter = QContactDetailFilter(); nameFilter.setDetailType(QContactExtendedDetail::Type, QContactExtendedDetail::FieldName); nameFilter.setMatchFlags(QContactFilter::MatchExactly); nameFilter.setValue(field); QContactDetailFilter valueFilter = QContactDetailFilter(); valueFilter.setDetailType(QContactExtendedDetail::Type, QContactExtendedDetail::FieldData); valueFilter.setMatchFlags(QContactFilter::MatchExactly); valueFilter.setValue(normalizedId); QContactIntersectionFilter intersectionFilter; intersectionFilter.append(nameFilter); intersectionFilter.append(valueFilter); topLevelFilter.append(intersectionFilter); } } if (synchronous) { QList contacts = mManager->contacts(topLevelFilter, QList(), hint); if (contacts.isEmpty()) { mContactMap[accountId][identifier] = contactInfo; return contactInfo; } // for synchronous requests, return the results right away. return matchAndUpdate(accountId, normalizedId, contacts.first()); } else { // check if there is a request already going on for the given contact Q_FOREACH(const RequestInfo &info, mRequests.values()) { if (info.accountId != accountId) { // skip to the next item continue; } if (info.identifier == normalizedId) { // if so, just wait for it to finish return QVariantMap(); } } QContactFetchRequest *request = new QContactFetchRequest(this); request->setFetchHint(hint); request->setFilter(topLevelFilter); request->setManager(mManager); QObject::connect(request, &QContactFetchRequest::stateChanged, this, &ContactMatcher::onRequestStateChanged); RequestInfo info; info.accountId = accountId; info.identifier = normalizedId; mRequests[request] = info; request->start(); } return QVariantMap(); } QVariantList ContactMatcher::toVariantList(const QList &list) { QVariantList variantList; Q_FOREACH(int value, list) { variantList << value; } return variantList; } /** * \brief Matches contact data against the given identifier. If the match succeeds, return the updated data in a * QVariantMap, returns an empty map otherwise. */ QVariantMap ContactMatcher::matchAndUpdate(const QString &accountId, const QString &identifier, const QContact &contact) { QVariantMap contactInfo; contactInfo[History::FieldIdentifier] = identifier; contactInfo[History::FieldAccountId] = accountId; if (contact.isEmpty()) { return contactInfo; } QStringList fields = addressableFields(accountId); bool match = false; Q_FOREACH(const QString &field, fields) { if (field == "tel") { QList details = contact.details(QContactDetail::TypePhoneNumber); Q_FOREACH(const QContactPhoneNumber number, details) { if (History::PhoneUtils::comparePhoneNumbers(number.number(), identifier)) { QVariantMap detailProperties; detailProperties["phoneSubTypes"] = toVariantList(number.subTypes()); detailProperties["phoneContexts"] = toVariantList(number.contexts()); contactInfo[History::FieldDetailProperties] = detailProperties; match = true; break; } } } else { // FIXME: support more types of field // generic code for extra fields Q_FOREACH(const QContactExtendedDetail detail, contact.details(QContactDetail::TypeExtendedDetail)) { if (detail.name() == field && detail.data() == identifier) { match = true; break; } } } if (match) { break; } } if (match) { contactInfo[History::FieldContactId] = contact.id().toString(); contactInfo[History::FieldAlias] = QContactDisplayLabel(contact.detail(QContactDetail::TypeDisplayLabel)).label(); contactInfo[History::FieldAvatar] = QContactAvatar(contact.detail(QContactDetail::TypeAvatar)).imageUrl().toString(); mContactMap[accountId][identifier] = contactInfo; Q_EMIT contactInfoChanged(accountId, identifier, contactInfo); } return contactInfo; } QStringList ContactMatcher::addressableFields(const QString &accountId) { if (mAddressableFields.contains(accountId)) { return mAddressableFields[accountId]; } // FIXME: hardcoding account IDs here is not a good idea, we have to fix addressable fields on // the protocols themselves if (accountId.startsWith("irc/irc")) { QStringList empty; mAddressableFields[accountId] = empty; return empty; } Tp::AccountPtr account = History::TelepathyHelper::instance()->accountForId(accountId); QStringList fields; if (!account.isNull()) { fields = account->protocolInfo().addressableVCardFields(); } // fallback to phone number matching in case everything else fails if (fields.isEmpty()) { fields << "tel"; } mAddressableFields[accountId] = fields; return fields; } bool ContactMatcher::hasMatch(const QVariantMap &map) const { return (map.contains(History::FieldContactId) && !map[History::FieldContactId].toString().isEmpty()); } QString ContactMatcher::normalizeId(const QString &id) { QString normalizedId = id; // FIXME: this is a hack so that SIP URIs get converted into phone numbers for contact matching if (normalizedId.startsWith("sip:")) { normalizedId.remove("sip:").remove(QRegularExpression("@.*$")); } return normalizedId; } } history-service-0.5/src/contactmatcher_p.h000066400000000000000000000057041455450246700210150ustar00rootroot00000000000000/* * Copyright (C) 2014-2016 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 CONTACTMATCHER_P_H #define CONTACTMATCHER_P_H #include #include #include #include using namespace QtContacts; namespace History { typedef QMap InternalContactMap; typedef QMap ContactMap; typedef struct { QString accountId; QString identifier; } RequestInfo; class ContactMatcher : public QObject { Q_OBJECT public: static ContactMatcher *instance(QContactManager *manager = 0); QVariantMap contactInfo(const QString &accountId, const QString &identifier, bool synchronous = false, const QVariantMap &properties = QVariantMap()); QVariantList contactInfo(const QString &accountId, const QStringList &identifiers, bool synchronous = false); // this will only watch for contact changes affecting the identifier, but won't fetch contact info void watchIdentifier(const QString &accountId, const QString &identifier, const QVariantMap ¤tInfo = QVariantMap()); static QString normalizeId(const QString &id); Q_SIGNALS: void contactInfoChanged(const QString &acountId, const QString &identifier, const QVariantMap &contactInfo); protected Q_SLOTS: void onContactsAdded(QList ids); void onContactsChanged(QList ids); void onContactsRemoved(QList ids); void onDataChanged(); void onRequestStateChanged(QContactAbstractRequest::State state); void onSetupReady(); protected: QVariantMap requestContactInfo(const QString &accountId, const QString &identifier, bool synchronous = false); QVariantList toVariantList(const QList &list); QVariantMap matchAndUpdate(const QString &accountId, const QString &identifier, const QContact &contact); QStringList addressableFields(const QString &accountId); bool hasMatch(const QVariantMap &map) const; private: explicit ContactMatcher(QContactManager *manager = 0, QObject *parent = 0); ~ContactMatcher(); ContactMap mContactMap; QMap mRequests; QMap mAddressableFields; QList mPendingRequests; QContactManager *mManager; }; } #endif // CONTACTMATCHER_P_H history-service-0.5/src/event.cpp000066400000000000000000000132471455450246700171540ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 "event.h" #include "event_p.h" namespace History { // ------------- EventPrivate ------------------------------------------------ EventPrivate::EventPrivate() { } EventPrivate::EventPrivate(const QString &theAccountId, const QString &theThreadId, const QString &theEventId, const QString &theSenderId, const QDateTime &theTimestamp, bool theNewEvent, const Participants &theParticipants) : accountId(theAccountId), threadId(theThreadId), eventId(theEventId), senderId(theSenderId), timestamp(theTimestamp), newEvent(theNewEvent), participants(theParticipants) { } EventPrivate::~EventPrivate() { } QVariantMap EventPrivate::properties() const { QVariantMap map; map[FieldAccountId] = accountId; map[FieldThreadId] = threadId; map[FieldEventId] = eventId; map[FieldSenderId] = senderId; map[FieldTimestamp] = timestamp.toString("yyyy-MM-ddTHH:mm:ss.zzz"); map[FieldDate] = timestamp.date().toString(Qt::ISODate); map[FieldNewEvent] = newEvent; map[FieldType] = type(); map[FieldParticipants] = participants.toVariantList(); return map; } // ------------- Event ------------------------------------------------------- /*! * \class Event * * \brief The Event class provides the base class for all events stored * and loaded from the history backends. * * This class should not be used directly and instead * the derived classes should be used. * * \sa TextEvent, VoiceEvent */ /*! * \brief Constructs an empty Event */ Event::Event() : d_ptr(new EventPrivate()) { } /*! * \brief Constructs an Event by copying the data from another one. * \param other The item to be copied; */ Event::Event(const Event &other) : d_ptr(other.d_ptr->clone()) { } /*! \internal * \brief Constructor to be used by derived classes to pass a EventPrivate instance * \param p The instance of the private class; */ Event::Event(EventPrivate &p) : d_ptr(&p) { } Event::~Event() { } /*! * \brief Assign operator for the Event class * \param other The event to be copied; */ Event& Event::operator=(const Event &other) { if (&other == this) { return *this; } d_ptr = QSharedPointer(other.d_ptr->clone()); return *this; } /*! * \brief Returns the account ID this event belongs to. */ QString Event::accountId() const { Q_D(const Event); return d->accountId; } /*! * \brief Returns the ID of the communication thread this event belongs to. * \sa HistoryThread */ QString Event::threadId() const { Q_D(const Event); return d->threadId; } /*! * \brief Returns the ID that uniquely identifies this event. */ QString Event::eventId() const { Q_D(const Event); return d->eventId; } /*! * \brief Returns the ID of the sender of this event. */ QString Event::senderId() const { Q_D(const Event); return d->senderId; } /*! * \brief Returns the timestamp of when the event happened. */ QDateTime Event::timestamp() const { Q_D(const Event); return d->timestamp; } /*! * \brief Returns whether the event is new (not yet seen by the user). * \sa Event::setNewEvent() */ bool Event::newEvent() const { Q_D(const Event); return d->newEvent; } /*! * \brief Set whether this event is new (not yet seen by the user). * \param value True if the event is new. False otherwise. * \sa Event::newEvent() */ void Event::setNewEvent(bool value) { Q_D(Event); d->newEvent = value; } /*! * \brief Returns the type of this event. */ EventType Event::type() const { Q_D(const Event); return d->type(); } Participants Event::participants() const { Q_D(const Event); return d->participants; } /*! * \brief Returns the event properties */ QVariantMap Event::properties() const { Q_D(const Event); return d->properties(); } /*! * \brief Return whether this event is a null event. */ bool Event::isNull() const { Q_D(const Event); return d->accountId.isNull() && d->threadId.isNull() && d->eventId.isNull(); } /*! * \brief Compare this event with another one. * \param other The other event; */ bool Event::operator==(const Event &other) const { Q_D(const Event); if (type() != other.type()) { return false; } if (d->accountId != other.d_ptr->accountId) { return false; } if (d->threadId != other.d_ptr->threadId) { return false; } if (d->eventId != other.d_ptr->eventId) { return false; } return true; } bool Event::operator!=(const Event &other) const { return !(*this == other); } bool Event::operator<(const Event &other) const { QString selfData = accountId() + threadId() + eventId(); QString otherData = other.accountId() + other.threadId() + other.eventId(); return selfData < otherData; } } history-service-0.5/src/event.h000066400000000000000000000033611455450246700166150ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 HISTORY_EVENT_H #define HISTORY_EVENT_H #include #include #include #include #include #include "types.h" #include "participant.h" namespace History { class EventPrivate; class Event { Q_DECLARE_PRIVATE(Event) public: explicit Event(); Event(const Event &other); virtual ~Event(); Event& operator=(const Event &other); QString accountId() const; QString threadId() const; QString eventId() const; QString senderId() const; QDateTime timestamp() const; bool newEvent() const; void setNewEvent(bool value); EventType type() const; Participants participants() const; QVariantMap properties() const; bool isNull() const; bool operator==(const Event &other) const; bool operator!=(const Event &other) const; bool operator<(const Event &other) const; protected: Event(EventPrivate &p); QSharedPointer d_ptr; }; typedef QList Events; } #endif history-service-0.5/src/event_p.h000066400000000000000000000047221455450246700171360ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 HISTORY_EVENT_P_H #define HISTORY_EVENT_P_H #include #include #include #include "types.h" #include "participant.h" #define HISTORY_EVENT_DECLARE_CLONE(Class) \ virtual EventPrivate *clone() { return new Class##Private(*this); } #define HISTORY_EVENT_DEFINE_COPY(Class, Type) \ Class::Class(const Event &other) { \ if (other.type() == Type) { d_ptr = QSharedPointer(reinterpret_cast(EventPrivate::getD(other)->clone())); } \ else { d_ptr = QSharedPointer(new Class##Private()); } \ } \ Class& Class::operator=(const Event &other) { \ if (other.type() == Type) { d_ptr = QSharedPointer(reinterpret_cast(EventPrivate::getD(other)->clone())); } \ return *this; \ } namespace History { class EventPrivate { public: EventPrivate(); EventPrivate(const QString &theAccountId, const QString &theThreadId, const QString &theEventId, const QString &theSenderId, const QDateTime &theTimestamp, bool theNewEvent, const Participants &theParticipants); virtual ~EventPrivate(); virtual EventType type() const { return EventTypeNull; } virtual QVariantMap properties() const; QString accountId; QString threadId; QString eventId; QString senderId; QString receiver; QDateTime timestamp; bool newEvent; Participants participants; static const QSharedPointer& getD(const Event& other) { return other.d_ptr; } HISTORY_EVENT_DECLARE_CLONE(Event) }; } #endif // HISTORY_EVENT_P_H history-service-0.5/src/eventview.cpp000066400000000000000000000126331455450246700200450ustar00rootroot00000000000000/* * Copyright (C) 2013-2017 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 "eventview.h" #include "eventview_p.h" #include "event.h" #include "filter.h" #include "manager.h" #include "sort.h" #include "textevent.h" #include "voiceevent.h" #include #include #include namespace History { // ------------- EventViewPrivate ------------------------------------------------ EventViewPrivate::EventViewPrivate(History::EventType theType, const History::Sort &theSort, const History::Filter &theFilter) : type(theType), sort(theSort), filter(theFilter), valid(true), dbus(0) { } Events EventViewPrivate::filteredEvents(const Events &events) { bool filterNull = filter.isNull(); Events filtered; Q_FOREACH(const Event &event, events) { if (event.type() != type) { continue; } if (filterNull || filter.match(event.properties())) { filtered << event; } } return filtered; } void EventViewPrivate::_d_eventsAdded(const Events &events) { Q_Q(EventView); Events filtered = filteredEvents(events); if (!filtered.isEmpty()) { Q_EMIT q->eventsAdded(filtered); } } void EventViewPrivate::_d_eventsModified(const Events &events) { Q_Q(EventView); Events filtered = filteredEvents(events); if (!filtered.isEmpty()) { Q_EMIT q->eventsModified(filtered); } } void EventViewPrivate::_d_eventsRemoved(const Events &events) { Q_Q(EventView); Events filtered = filteredEvents(events); if (!filtered.isEmpty()) { Q_EMIT q->eventsRemoved(filtered); } } // ------------- EventView ------------------------------------------------------- EventView::EventView(EventType type, const History::Sort &sort, const History::Filter &filter) : d_ptr(new EventViewPrivate(type, sort, filter)) { d_ptr->q_ptr = this; if (!Manager::instance()->isServiceRunning()) { Q_EMIT invalidated(); d_ptr->valid = false; return; } QDBusInterface interface(History::DBusService, History::DBusObjectPath, History::DBusInterface); QDBusReply reply = interface.call("QueryEvents", (int) type, sort.properties(), filter.properties()); if (!reply.isValid()) { Q_EMIT invalidated(); d_ptr->valid = false; return; } d_ptr->objectPath = reply.value(); d_ptr->dbus = new QDBusInterface(History::DBusService, d_ptr->objectPath, History::EventViewInterface, QDBusConnection::sessionBus(), this); connect(Manager::instance(), SIGNAL(eventsAdded(History::Events)), SLOT(_d_eventsAdded(History::Events))); connect(Manager::instance(), SIGNAL(eventsModified(History::Events)), SLOT(_d_eventsModified(History::Events))); connect(Manager::instance(), SIGNAL(eventsRemoved(History::Events)), SLOT(_d_eventsRemoved(History::Events))); // we don't filter thread signals connect(Manager::instance(), SIGNAL(threadsRemoved(History::Threads)), SIGNAL(threadsRemoved(History::Threads))); } EventView::~EventView() { Q_D(EventView); if (d->valid) { d->dbus->call("Destroy"); } } int EventView::getTotalCount() { Q_D(EventView); if (!d->valid) { return 0; } QDBusReply reply = d->dbus->call("GetTotalCount"); if (!reply.isValid()) { qWarning() << reply.error(); return 0; } return reply.value(); } QList EventView::nextPage() { Q_D(EventView); QList events; if (!d->valid) { return events; } QDBusReply > reply = d->dbus->call("NextPage"); if (!reply.isValid()) { d->valid = false; Q_EMIT invalidated(); return events; } QList eventsProperties = reply.value(); Q_FOREACH(const QVariantMap &properties, eventsProperties) { Event event; switch (d->type) { case EventTypeText: event = TextEvent::fromProperties(properties); break; case EventTypeVoice: event = VoiceEvent::fromProperties(properties); break; case EventTypeNull: qWarning("EventView::nextPage(): Got EventTypeNull, ignoring this event!"); break; } if (!event.isNull()) { events << event; } } return events; } bool EventView::isValid() const { Q_D(const EventView); return d->valid; } } #include "moc_eventview.cpp" history-service-0.5/src/eventview.h000066400000000000000000000035501455450246700175100ustar00rootroot00000000000000/* * Copyright (C) 2013-2017 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 HISTORY_EVENTVIEW_H #define HISTORY_EVENTVIEW_H #include "types.h" #include "event.h" #include "thread.h" #include "filter.h" #include "sort.h" #include namespace History { class EventViewPrivate; class EventView : public QObject { Q_OBJECT Q_DECLARE_PRIVATE(EventView) public: EventView(History::EventType type, const History::Sort &sort, const History::Filter &filter); virtual ~EventView(); QList nextPage(); int getTotalCount(); bool isValid() const; Q_SIGNALS: void eventsAdded(const History::Events &events); void eventsModified(const History::Events &events); void eventsRemoved(const History::Events &events); void threadsRemoved(const History::Threads &threads); void invalidated(); private: Q_PRIVATE_SLOT(d_func(), void _d_eventsAdded(const History::Events &events)) Q_PRIVATE_SLOT(d_func(), void _d_eventsModified(const History::Events &events)) Q_PRIVATE_SLOT(d_func(), void _d_eventsRemoved(const History::Events &events)) QScopedPointer d_ptr; }; } #endif // HISTORY_EVENTVIEW_H history-service-0.5/src/eventview_p.h000066400000000000000000000031561455450246700200310ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 EVENTVIEW_P_H #define EVENTVIEW_P_H #include "types.h" #include "filter.h" #include "sort.h" #include namespace History { class EventView; class EventViewPrivate { Q_DECLARE_PUBLIC(EventView) public: EventViewPrivate(History::EventType theType, const History::Sort &theSort, const History::Filter &theFilter); EventType type; Sort sort; Filter filter; QString objectPath; bool valid; QDBusInterface *dbus; Events filteredEvents(const Events &events); // private slots void _d_eventsAdded(const History::Events &events); void _d_eventsModified(const History::Events &events); void _d_eventsRemoved(const History::Events &events); EventView *q_ptr; }; } #endif // EVENTVIEW_P_H history-service-0.5/src/filter.cpp000066400000000000000000000150671455450246700173220ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 "filter.h" #include "filter_p.h" #include "intersectionfilter.h" #include "unionfilter.h" #include #include namespace History { // ------------- FilterPrivate ------------------------------------------------ FilterPrivate::FilterPrivate() { } FilterPrivate::FilterPrivate(const QString &theFilterProperty, const QVariant &theFilterValue, MatchFlags theMatchFlags) : filterProperty(theFilterProperty), filterValue(theFilterValue), matchFlags(theMatchFlags) { } FilterPrivate::~FilterPrivate() { } QString FilterPrivate::toString(const QString &propertyPrefix) const { // FIXME: remove the toString() functionality or replace it by a better implementation if (filterProperty.isEmpty() || filterValue.isNull()) { return QString(); } QString value; switch (filterValue.type()) { case QVariant::String: // FIXME: need to escape strings // wrap strings value = QString("\"%1\"").arg(filterValue.toString()); break; case QVariant::Bool: value = filterValue.toBool() ? "1" : "0"; break; case QVariant::Int: value = QString::number(filterValue.toInt()); break; case QVariant::Double: value = QString::number(filterValue.toDouble()); break; default: value = filterValue.toString(); } QString propertyName = propertyPrefix.isNull() ? filterProperty : QString("%1.%2").arg(propertyPrefix, filterProperty); // FIXME2: need to check for the match flags QString condition; switch (matchFlags) { case History::MatchNotEquals: condition = QString("%1!=%2"); break; case History::MatchLess: condition = QString("%1<%2"); break; case History::MatchGreater: condition = QString("%1>%2"); break; case History::MatchLessOrEquals: condition = QString("%1<=%2"); break; case History::MatchGreaterOrEquals: condition = QString("%1>=%2"); break; default: condition = QString("%1=%2"); } return QString(condition).arg(propertyName, value); } bool FilterPrivate::match(const QVariantMap properties) const { // assume empty filters match anything if (filterProperty.isEmpty() || !filterValue.isValid() || !properties.contains(filterProperty)) { return true; } switch (matchFlags) { case History::MatchNotEquals: return properties[filterProperty] != filterValue; break; case History::MatchLess: return properties[filterProperty] < filterValue; break; case History::MatchGreater: return properties[filterProperty] > filterValue; break; case History::MatchLessOrEquals: return properties[filterProperty] <= filterValue; break; case History::MatchGreaterOrEquals: return properties[filterProperty] >= filterValue; break; default: return properties[filterProperty] == filterValue; } } QVariantMap FilterPrivate::properties() const { QVariantMap map; if (!isValid()) { return map; } map[FieldFilterType] = (int)FilterTypeStandard; map[FieldFilterProperty] = filterProperty; map[FieldFilterValue] = filterValue; map[FieldMatchFlags] = (int)matchFlags; return map; } // ------------- Filter ------------------------------------------------------- Filter::Filter(FilterPrivate &p) : d_ptr(&p) { } Filter::Filter(const QString &filterProperty, const QVariant &filterValue, MatchFlags matchFlags) : d_ptr(new FilterPrivate(filterProperty, filterValue, matchFlags)) { } Filter::Filter(const Filter &other) : d_ptr(other.d_ptr->clone()) { } Filter::~Filter() { } Filter &Filter::operator=(const Filter &other) { if (&other == this) { return *this; } d_ptr = QSharedPointer(other.d_ptr->clone()); return *this; } QString Filter::filterProperty() const { Q_D(const Filter); return d->filterProperty; } void Filter::setFilterProperty(const QString &value) { Q_D(Filter); d->filterProperty = value; } QVariant Filter::filterValue() const { Q_D(const Filter); return d->filterValue; } void Filter::setFilterValue(const QVariant &value) { Q_D(Filter); d->filterValue = value; } MatchFlags Filter::matchFlags() const { Q_D(const Filter); return d->matchFlags; } void Filter::setMatchFlags(const MatchFlags &flags) { Q_D(Filter); d->matchFlags = flags; } QString Filter::toString(const QString &propertyPrefix) const { Q_D(const Filter); return d->toString(propertyPrefix); } bool Filter::match(const QVariantMap properties) const { Q_D(const Filter); return d->match(properties); } FilterType Filter::type() const { Q_D(const Filter); return d->type(); } bool Filter::operator==(const Filter &other) const { // FIXME: implement in a more performant way return toString() == other.toString(); } bool Filter::isValid() const { Q_D(const Filter); return d->isValid(); } QVariantMap Filter::properties() const { Q_D(const Filter); return d->properties(); } Filter Filter::fromProperties(const QVariantMap &properties) { Filter filter; if (properties.isEmpty()) { return filter; } switch ((FilterType)properties[FieldFilterType].toInt()) { case FilterTypeStandard: filter = Filter(properties[FieldFilterProperty].toString(), properties[FieldFilterValue], (MatchFlags)properties[FieldMatchFlags].toInt()); break; case FilterTypeIntersection: filter = IntersectionFilter::fromProperties(properties); break; case FilterTypeUnion: filter = UnionFilter::fromProperties(properties); break; } return filter; } } history-service-0.5/src/filter.h000066400000000000000000000041061455450246700167570ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 HISTORY_FILTER_H #define HISTORY_FILTER_H #include #include #include #include "types.h" namespace History { class FilterPrivate; // simple filter class Filter { Q_DECLARE_PRIVATE(Filter) public: Filter(const QString &filterProperty = QString(), const QVariant &filterValue = QVariant(), MatchFlags matchFlags = MatchCaseSensitive); Filter(const Filter &other); virtual ~Filter(); Filter& operator=(const Filter& other); QString filterProperty() const; void setFilterProperty(const QString &value); QVariant filterValue() const; void setFilterValue(const QVariant &value); MatchFlags matchFlags() const; void setMatchFlags(const MatchFlags &flags); QString toString(const QString &propertyPrefix = QString()) const; bool match(const QVariantMap properties) const; FilterType type() const; bool operator==(const Filter &other) const; bool operator!=(const Filter &other) const { return !operator==(other); } bool isValid() const; bool isNull() const { return !isValid(); } QVariantMap properties() const; static Filter fromProperties(const QVariantMap &properties); protected: Filter(FilterPrivate &p); QSharedPointer d_ptr; }; typedef QList Filters; } #endif history-service-0.5/src/filter_p.h000066400000000000000000000046371455450246700173070ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 HISTORY_FILTER_P_H #define HISTORY_FILTER_P_H #include #include #include #include "types.h" #define HISTORY_FILTER_DECLARE_CLONE(Class) \ virtual FilterPrivate *clone() { return new Class##Private(*this); } #define HISTORY_FILTER_DEFINE_COPY(Class, Type) \ Class::Class(const Filter &other) { \ if (other.type() == Type) { d_ptr = QSharedPointer(reinterpret_cast(FilterPrivate::getD(other)->clone())); } \ else { d_ptr = QSharedPointer(new Class##Private()); } \ } \ Class& Class::operator=(const Filter &other) { \ if (other.type() == Type) { d_ptr = QSharedPointer(reinterpret_cast(FilterPrivate::getD(other)->clone())); } \ return *this; \ } namespace History { class FilterPrivate { public: FilterPrivate(); FilterPrivate(const QString &theFilterProperty, const QVariant &theFilterValue, MatchFlags theMatchFlags); virtual ~FilterPrivate(); QString filterProperty; QVariant filterValue; MatchFlags matchFlags; static const QSharedPointer& getD(const Filter& other) { return other.d_ptr; } virtual QString toString(const QString &propertyPrefix = QString()) const; virtual bool match(const QVariantMap properties) const; virtual FilterType type() const { return History::FilterTypeStandard; } virtual bool isValid() const { return (!filterProperty.isNull()) && (!filterValue.isNull()); } virtual QVariantMap properties() const; HISTORY_FILTER_DECLARE_CLONE(Filter) }; } #endif // HISTORY_FILTER_P_H history-service-0.5/src/history-service.pc.in000066400000000000000000000007001455450246700214050ustar00rootroot00000000000000prefix=${CMAKE_INSTALL_PREFIX} exec_prefix=${CMAKE_INSTALL_PREFIX} libdir=${CMAKE_INSTALL_PREFIX}/${LIB_INSTALL_DIR} includedir=${CMAKE_INSTALL_PREFIX}/${INCLUDE_INSTALL_DIR} Name: history-service Description: History service to store messages and calls Version: ${PACKAGE_VERSION} Requires.private: Qt5Core, Qt5DBus Libs: -L${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR} -lhistoryservice Cflags: -I${CMAKE_INSTALL_PREFIX}/${INCLUDE_INSTALL_DIR} history-service-0.5/src/intersectionfilter.cpp000066400000000000000000000102161455450246700217400ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 "intersectionfilter.h" #include "intersectionfilter_p.h" #include #include #include namespace History { IntersectionFilterPrivate::IntersectionFilterPrivate() { } IntersectionFilterPrivate::~IntersectionFilterPrivate() { } bool IntersectionFilterPrivate::match(const QVariantMap properties) const { // return true only if all filters match Q_FOREACH(const Filter &filter, filters) { if (!filter.match(properties)) { return false; } } return true; } bool IntersectionFilterPrivate::isValid() const { // FIXME: maybe we should check if at least one of the inner filters are valid? return !filters.isEmpty(); } QVariantMap IntersectionFilterPrivate::properties() const { QVariantMap map; if (!isValid()) { return map; } QVariantList filterList; Q_FOREACH(const Filter &filter, filters) { filterList << filter.properties(); } map[FieldFilters] = filterList; map[FieldFilterType] = (int) History::FilterTypeIntersection; return map; } QString IntersectionFilterPrivate::toString(const QString &propertyPrefix) const { if (filters.isEmpty()) { return QString(); } else if (filters.count() == 1) { return filters.first().toString(); } QStringList output; // wrap each filter string around parenthesis Q_FOREACH(const Filter &filter, filters) { QString value = filter.toString(propertyPrefix); if (!value.isEmpty()) { output << QString("(%1)").arg(value); } } return output.join(" AND "); } HISTORY_FILTER_DEFINE_COPY(IntersectionFilter, FilterTypeIntersection) IntersectionFilter::IntersectionFilter() : Filter(*new IntersectionFilterPrivate()) { } IntersectionFilter::~IntersectionFilter() { } void IntersectionFilter::setFilters(const Filters &filters) { Q_D(IntersectionFilter); d->filters = filters; } void IntersectionFilter::prepend(const Filter &filter) { Q_D(IntersectionFilter); d->filters.prepend(filter); } void IntersectionFilter::append(const Filter &filter) { Q_D(IntersectionFilter); d->filters.append(filter); } void IntersectionFilter::clear() { Q_D(IntersectionFilter); d->filters.clear(); } Filters IntersectionFilter::filters() const { Q_D(const IntersectionFilter); return d->filters; } Filter IntersectionFilter::fromProperties(const QVariantMap &properties) { IntersectionFilter filter; if (properties.isEmpty()) { return filter; } QVariant filters = properties[FieldFilters]; QVariantList filterList; // when the filter travels through DBus, it arrives marshalled into QDBusArguments. // cover that case too. if (filters.canConvert()) { QDBusArgument argument = filters.value(); QVariantList list; argument >> list; // and cast also the inner filters Q_FOREACH(const QVariant &var, list) { QDBusArgument arg = var.value(); QVariantMap map; arg >> map; filterList.append(map); } } else { filterList = filters.toList(); } Q_FOREACH(const QVariant &props, filterList) { Filter innerFilter = History::Filter::fromProperties(props.toMap()); if (innerFilter.isValid()) { filter.append(innerFilter); } } return filter; } } history-service-0.5/src/intersectionfilter.h000066400000000000000000000026751455450246700214170ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 HISTORY_INTERSECTIONFILTER_H #define HISTORY_INTERSECTIONFILTER_H #include "filter.h" #include "types.h" namespace History { class IntersectionFilterPrivate; // AND filter class IntersectionFilter : public Filter { Q_DECLARE_PRIVATE(IntersectionFilter) public: IntersectionFilter(); ~IntersectionFilter(); // copy related members IntersectionFilter(const Filter &other); IntersectionFilter& operator=(const Filter &other); void setFilters(const Filters &filters); void prepend(const Filter &filter); void append(const Filter &filter); void clear(); Filters filters() const; static Filter fromProperties(const QVariantMap &properties); }; } #endif history-service-0.5/src/intersectionfilter_p.h000066400000000000000000000026211455450246700217250ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 HISTORY_INTERSECTIONFILTER_P_H #define HISTORY_INTERSECTIONFILTER_P_H #include "filter_p.h" namespace History { class IntersectionFilter; class IntersectionFilterPrivate : public FilterPrivate { public: IntersectionFilterPrivate(); ~IntersectionFilterPrivate(); virtual FilterType type() const { return FilterTypeIntersection; } QString toString(const QString &propertyPrefix = QString()) const; bool match(const QVariantMap properties) const; bool isValid() const; Filters filters; virtual QVariantMap properties() const; HISTORY_FILTER_DECLARE_CLONE(IntersectionFilter) }; } #endif // HISTORY_INTERSECTIONFILTER_P_H history-service-0.5/src/manager.cpp000066400000000000000000000155331455450246700174450ustar00rootroot00000000000000/* * Copyright (C) 2013-2016 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 "manager.h" #include "manager_p.h" #include "managerdbus_p.h" #include "eventview.h" #include "intersectionfilter.h" #include "textevent.h" #include "thread.h" #include "threadview.h" #include "voiceevent.h" #include #include #include #include namespace History { // ------------- ManagerPrivate ------------------------------------------------ ManagerPrivate::ManagerPrivate() : dbus(new ManagerDBus()), serviceWatcher(DBusService, QDBusConnection::sessionBus()) { } ManagerPrivate::~ManagerPrivate() { } // ------------- Manager ------------------------------------------------------- Manager::Manager() : d_ptr(new ManagerPrivate()) { Q_D(Manager); // Propagate the signals from the event watcher connect(d->dbus.data(), SIGNAL(threadsAdded(History::Threads)), SIGNAL(threadsAdded(History::Threads))); connect(d->dbus.data(), SIGNAL(threadsModified(History::Threads)), SIGNAL(threadsModified(History::Threads))); connect(d->dbus.data(), SIGNAL(threadsRemoved(History::Threads)), SIGNAL(threadsRemoved(History::Threads))); connect(d->dbus.data(), SIGNAL(threadParticipantsChanged(History::Thread, History::Participants, History::Participants, History::Participants)), SIGNAL(threadParticipantsChanged(History::Thread, History::Participants, History::Participants, History::Participants))); connect(d->dbus.data(), SIGNAL(eventsAdded(History::Events)), SIGNAL(eventsAdded(History::Events))); connect(d->dbus.data(), SIGNAL(eventsModified(History::Events)), SIGNAL(eventsModified(History::Events))); connect(d->dbus.data(), SIGNAL(eventsRemoved(History::Events)), SIGNAL(eventsRemoved(History::Events))); // watch for the service going up and down connect(&d->serviceWatcher, &QDBusServiceWatcher::serviceRegistered, [&](const QString &serviceName) { qDebug() << "HistoryService: service registered:" << serviceName; this->d_ptr->serviceRunning = true; Q_EMIT this->serviceRunningChanged(); }); connect(&d->serviceWatcher, &QDBusServiceWatcher::serviceUnregistered, [&](const QString &serviceName) { qDebug() << "HistoryService: service unregistered:" << serviceName; this->d_ptr->serviceRunning = false; Q_EMIT this->serviceRunningChanged(); }); // and fetch the current status d->serviceRunning = false; QDBusReply reply = QDBusConnection::sessionBus().interface()->isServiceRegistered(DBusService); if (reply.isValid()) { d->serviceRunning = reply.value(); } } Manager::~Manager() { } Manager *Manager::instance() { static Manager *self = new Manager(); return self; } void Manager::markThreadsAsRead(const History::Threads &threads) { Q_D(Manager); d->dbus->markThreadsAsRead(threads); } ThreadViewPtr Manager::queryThreads(EventType type, const Sort &sort, const Filter &filter, const QVariantMap &properties) { return ThreadViewPtr(new ThreadView(type, sort, filter, properties)); } EventViewPtr Manager::queryEvents(EventType type, const Sort &sort, const Filter &filter) { return EventViewPtr(new EventView(type, sort, filter)); } Event Manager::getSingleEvent(EventType type, const QString &accountId, const QString &threadId, const QString &eventId) { Q_D(Manager); Event event = d->dbus->getSingleEvent(type, accountId, threadId, eventId); return event; } Thread Manager::threadForParticipants(const QString &accountId, EventType type, const QStringList &participants, MatchFlags matchFlags, bool create) { Q_D(Manager); QVariantMap properties; properties[History::FieldParticipantIds] = participants; if (participants.size() == 1) { properties[History::FieldChatType] = History::ChatTypeContact; } return d->dbus->threadForProperties(accountId, type, properties, matchFlags, create); } Thread Manager::threadForProperties(const QString &accountId, EventType type, const QVariantMap &properties, MatchFlags matchFlags, bool create) { Q_D(Manager); return d->dbus->threadForProperties(accountId, type, properties, matchFlags, create); } /** * @brief Request the list of participants of the given threads to the service * @param threads The threads to be filled * * This is an asychronous request. When finished, the signal @ref threadParticipantsChanged * will be emitted for the given threads. */ void Manager::requestThreadParticipants(const Threads &threads) { Q_D(Manager); d->dbus->requestThreadParticipants(threads); } Thread Manager::getSingleThread(EventType type, const QString &accountId, const QString &threadId, const QVariantMap &properties) { Q_D(Manager); Thread thread = d->dbus->getSingleThread(type, accountId, threadId, properties); return thread; } bool Manager::writeEvents(const Events &events) { Q_D(Manager); return d->dbus->writeEvents(events); } bool Manager::removeThreads(const Threads &threads) { Q_D(Manager); return d->dbus->removeThreads(threads); } bool Manager::removeEvents(const Events &events) { Q_D(Manager); return d->dbus->removeEvents(events); } void Manager::removeEvents(EventType type, const Filter &filter, std::function callback) { Q_D(Manager); d->dbus->removeEvents(type, filter, callback); } int Manager::getEventsCount(EventType type, const Filter &filter) { Q_D(Manager); return d->dbus->getEventsCount(type, filter); } bool Manager::isServiceRunning() const { Q_D(const Manager); return d->serviceRunning; } } history-service-0.5/src/manager.h000066400000000000000000000067451455450246700171170ustar00rootroot00000000000000/* * Copyright (C) 2013-2016 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 HISTORY_MANAGER_H #define HISTORY_MANAGER_H #include #include #include "types.h" #include "event.h" #include "filter.h" #include "sort.h" #include "thread.h" namespace History { class ManagerPrivate; class Manager : public QObject { Q_OBJECT Q_DECLARE_PRIVATE(Manager) public: ~Manager(); static Manager *instance(); ThreadViewPtr queryThreads(EventType type, const Sort &sort = Sort(), const Filter &filter = Filter(), const QVariantMap &properties = QVariantMap()); EventViewPtr queryEvents(EventType type, const Sort &sort = Sort(), const Filter &filter = Filter()); Event getSingleEvent(EventType type, const QString &accountId, const QString &threadId, const QString &eventId); Thread threadForParticipants(const QString &accountId, EventType type, const QStringList &participants, History::MatchFlags matchFlags = History::MatchCaseSensitive, bool create = false); Thread threadForProperties(const QString &accountId, EventType type, const QVariantMap &properties, History::MatchFlags matchFlags = History::MatchCaseSensitive, bool create = false); void requestThreadParticipants(const History::Threads &threads); Thread getSingleThread(EventType type, const QString &accountId, const QString &threadId, const QVariantMap &properties = QVariantMap()); bool writeEvents(const History::Events &events); bool removeThreads(const Threads &threads); bool removeEvents(const Events &events); void removeEvents(EventType type, const Filter &filter, std::function callback); int getEventsCount(EventType type, const Filter &filter); void markThreadsAsRead(const History::Threads &thread); bool isServiceRunning() const; Q_SIGNALS: void threadsAdded(const History::Threads &threads); void threadsModified(const History::Threads &threads); void threadsRemoved(const History::Threads &threads); void threadParticipantsChanged(const History::Thread &thread, const History::Participants &added, const History::Participants &removed, const History::Participants &modified); void eventsAdded(const History::Events &events); void eventsModified(const History::Events &events); void eventsRemoved(const History::Events &events); void serviceRunningChanged(); private: Manager(); QScopedPointer d_ptr; }; } #endif history-service-0.5/src/manager_p.h000066400000000000000000000022331455450246700174220ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 HISTORY_MANAGER_P_H #define HISTORY_MANAGER_P_H #include #include #include #include "types.h" namespace History { class Manager; class ManagerDBus; class ManagerPrivate { public: ManagerPrivate(); ~ManagerPrivate(); QScopedPointer dbus; bool serviceRunning; QDBusServiceWatcher serviceWatcher; }; } #endif // HISTORY_MANAGER_P_H history-service-0.5/src/managerdbus.cpp000066400000000000000000000261441455450246700203230ustar00rootroot00000000000000/* * Copyright (C) 2013-2016 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 "managerdbus_p.h" #include "event.h" #include "manager.h" #include "thread.h" #include "textevent.h" #include "voiceevent.h" #include #include #include Q_DECLARE_METATYPE(QList< QVariantMap >) namespace History { ManagerDBus::ManagerDBus(QObject *parent) : QObject(parent), mAdaptor(0), mInterface(DBusService, DBusObjectPath, DBusInterface) { qDBusRegisterMetaType >(); qRegisterMetaType >(); // listen for signals coming from the bus QDBusConnection connection = QDBusConnection::sessionBus(); connection.connect(DBusService, DBusObjectPath, DBusInterface, "ThreadsAdded", this, SLOT(onThreadsAdded(QList))); connection.connect(DBusService, DBusObjectPath, DBusInterface, "ThreadsModified", this, SLOT(onThreadsModified(QList))); connection.connect(DBusService, DBusObjectPath, DBusInterface, "ThreadsRemoved", this, SLOT(onThreadsRemoved(QList))); connection.connect(DBusService, DBusObjectPath, DBusInterface, "ThreadParticipantsChanged", this, SLOT(onThreadParticipantsChanged(QVariantMap, QList, QList, QList))); connection.connect(DBusService, DBusObjectPath, DBusInterface, "EventsAdded", this, SLOT(onEventsAdded(QList))); connection.connect(DBusService, DBusObjectPath, DBusInterface, "EventsModified", this, SLOT(onEventsModified(QList))); connection.connect(DBusService, DBusObjectPath, DBusInterface, "EventsRemoved", this, SLOT(onEventsRemoved(QList))); } Thread ManagerDBus::threadForParticipants(const QString &accountId, EventType type, const QStringList &participants, MatchFlags matchFlags, bool create) { QVariantMap properties; properties[History::FieldParticipantIds] = participants; return threadForProperties(accountId, type, properties, matchFlags, create); } void ManagerDBus::markThreadsAsRead(const History::Threads &threads) { QList threadMap = threadsToProperties(threads); if (threadMap.isEmpty()) { return; } mInterface.asyncCall("MarkThreadsAsRead", QVariant::fromValue(threadMap)); } Thread ManagerDBus::threadForProperties(const QString &accountId, EventType type, const QVariantMap &properties, MatchFlags matchFlags, bool create) { Thread thread; // FIXME: move to async call if possible QDBusReply reply = mInterface.call("ThreadForProperties", accountId, (int) type, properties, (int)matchFlags, create); if (reply.isValid()) { QVariantMap properties = reply.value(); thread = Thread::fromProperties(properties); } return thread; } void ManagerDBus::requestThreadParticipants(const Threads &threads) { QList ids; Q_FOREACH(const Thread &thread, threads) { QVariantMap id; id[History::FieldAccountId] = thread.accountId(); id[History::FieldThreadId] = thread.threadId(); id[History::FieldType] = thread.type(); ids << id; } QDBusPendingCall call = mInterface.asyncCall("ParticipantsForThreads", QVariant::fromValue(ids)); QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(call, this); connect(watcher, &QDBusPendingCallWatcher::finished, [this, threads](QDBusPendingCallWatcher *watcher) { QDBusPendingReply > reply = *watcher; Q_FOREACH(const QVariantMap &map, reply.value()) { History::Thread thread = History::Thread::fromProperties(map); Q_EMIT threadParticipantsChanged(thread, History::Participants(), History::Participants(), thread.participants()); watcher->deleteLater(); } }); } bool ManagerDBus::writeEvents(const Events &events) { QList eventMap = eventsToProperties(events); if (eventMap.isEmpty()) { return false; } QDBusReply reply = mInterface.call("WriteEvents", QVariant::fromValue(eventMap)); if (!reply.isValid()) { return false; } return reply.value(); } bool ManagerDBus::removeThreads(const Threads &threads) { QList threadMap = threadsToProperties(threads); if (threadMap.isEmpty()) { return false; } mInterface.asyncCall("RemoveThreads", QVariant::fromValue(threadMap)); return true; } bool ManagerDBus::removeEvents(const Events &events) { QList eventMap = eventsToProperties(events); if (eventMap.isEmpty()) { return false; } mInterface.asyncCall("RemoveEvents", QVariant::fromValue(eventMap)); return true; } void ManagerDBus::removeEvents(EventType type, const Filter &filter, std::function callback) { QDBusMessage message = QDBusMessage::createMethodCall(mInterface.service(), mInterface.path(), mInterface.interface(), "RemoveEventsBy"); message.setArguments({ (int)type, filter.properties()}); // 7500 events takes 38sec on N5, 2 min should be enough for most cases QDBusPendingCall async = mInterface.connection().asyncCall(message, 120000); QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(async, this); QObject::connect(watcher, &QDBusPendingCallWatcher::finished, [this, callback](QDBusPendingCallWatcher *call) { QDBusPendingReply reply = *call; if (reply.isError()) { qWarning() << "error:" << __func__ << reply.error().message(); callback(0, true); } else { int removedCount = reply.value(); qDebug() << __func__ << "Events removed:" << removedCount; callback(removedCount, false); } call->deleteLater(); }); } int ManagerDBus::getEventsCount(int type, const Filter &filter) { QDBusReply reply = mInterface.call("GetEventsCount", (int)type, filter.properties()); if (!reply.isValid()) { qWarning() << "invalid reply from GetEventsCount" << reply.error(); return 0; } return reply.value(); } Thread ManagerDBus::getSingleThread(EventType type, const QString &accountId, const QString &threadId, const QVariantMap &properties) { Thread thread; QDBusReply reply = mInterface.call("GetSingleThread", (int)type, accountId, threadId, properties); if (!reply.isValid()) { return thread; } thread = Thread::fromProperties(reply.value()); return thread; } Event ManagerDBus::getSingleEvent(EventType type, const QString &accountId, const QString &threadId, const QString &eventId) { Event event; QDBusReply reply = mInterface.call("GetSingleEvent", (int)type, accountId, threadId, eventId); if (!reply.isValid()) { return event; } event = eventFromProperties(reply.value()); return event; } void ManagerDBus::onThreadsAdded(const QList &threads) { Q_EMIT threadsAdded(threadsFromProperties(threads)); } void ManagerDBus::onThreadsModified(const QList &threads) { Q_EMIT threadsModified(threadsFromProperties(threads)); } void ManagerDBus::onThreadsRemoved(const QList &threads) { Q_EMIT threadsRemoved(threadsFromProperties(threads)); } void ManagerDBus::onThreadParticipantsChanged(const QVariantMap &thread, const QList &added, const QList &removed, const QList &modified) { Q_EMIT threadParticipantsChanged(threadsFromProperties(QList() << thread).first(), Participants::fromVariantMapList(added), Participants::fromVariantMapList(removed), Participants::fromVariantMapList(modified)); } void ManagerDBus::onEventsAdded(const QList &events) { Q_EMIT eventsAdded(eventsFromProperties(events)); } void ManagerDBus::onEventsModified(const QList &events) { Q_EMIT eventsModified(eventsFromProperties(events)); } void ManagerDBus::onEventsRemoved(const QList &events) { Q_EMIT eventsRemoved(eventsFromProperties(events)); } Threads ManagerDBus::threadsFromProperties(const QList &threadsProperties) { Threads threads; Q_FOREACH(const QVariantMap &map, threadsProperties) { Thread thread = Thread::fromProperties(map); if (!thread.isNull()) { threads << thread; } } return threads; } QList ManagerDBus::threadsToProperties(const Threads &threads) { QList threadsPropertyMap; Q_FOREACH(const Thread &thread, threads) { threadsPropertyMap << thread.properties(); } return threadsPropertyMap; } Event ManagerDBus::eventFromProperties(const QVariantMap &properties) { EventType type = (EventType)properties[FieldType].toInt(); switch (type) { case EventTypeText: return TextEvent::fromProperties(properties); case EventTypeVoice: return VoiceEvent::fromProperties(properties); case EventTypeNull: qWarning("ManagerDBus::eventFromProperties: Got EventTypeNull, returning NULL event!"); return Event(); } // We should NEVER reach this return Event(); } Events ManagerDBus::eventsFromProperties(const QList &eventsProperties) { Events events; Q_FOREACH(const QVariantMap &map, eventsProperties) { events << eventFromProperties(map); } return events; } QList ManagerDBus::eventsToProperties(const Events &events) { QList eventsPropertyMap; Q_FOREACH(const Event &event, events) { eventsPropertyMap << event.properties(); } return eventsPropertyMap; } } history-service-0.5/src/managerdbus_p.h000066400000000000000000000101051455450246700202750ustar00rootroot00000000000000/* * Copyright (C) 2013-2016 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 MANAGERDBUS_P_H #define MANAGERDBUS_P_H #include #include #include "types.h" #include "event.h" #include "filter.h" #include "sort.h" #include "thread.h" class HistoryServiceAdaptor; namespace History { class ManagerDBus : public QObject { Q_OBJECT public: explicit ManagerDBus(QObject *parent = 0); Thread threadForParticipants(const QString &accountId, EventType type, const QStringList &participants, History::MatchFlags matchFlags, bool create); Thread threadForProperties(const QString &accountId, EventType type, const QVariantMap &properties, History::MatchFlags matchFlags, bool create); void requestThreadParticipants(const History::Threads &threads); bool writeEvents(const History::Events &events); bool removeThreads(const Threads &threads); bool removeEvents(const Events &events); void removeEvents(EventType type, const Filter &filter, std::function callback); int getEventsCount(int type, const Filter &filter); Thread getSingleThread(EventType type, const QString &accountId, const QString &threadId, const QVariantMap &properties = QVariantMap()); Event getSingleEvent(EventType type, const QString &accountId, const QString &threadId, const QString &eventId); void markThreadsAsRead(const History::Threads &threads); Q_SIGNALS: // signals that will be triggered after processing bus signals void threadsAdded(const History::Threads &threads); void threadsModified(const History::Threads &threads); void threadsRemoved(const History::Threads &threads); void threadParticipantsChanged(const History::Thread &thread, const History::Participants &added, const History::Participants &removed, const History::Participants &modified); void eventsAdded(const History::Events &events); void eventsModified(const History::Events &events); void eventsRemoved(const History::Events &events); protected Q_SLOTS: void onThreadsAdded(const QList &threads); void onThreadsModified(const QList &threads); void onThreadsRemoved(const QList &threads); void onThreadParticipantsChanged(const QVariantMap &thread, const QList &added, const QList &removed, const QList &modified); void onEventsAdded(const QList &events); void onEventsModified(const QList &events); void onEventsRemoved(const QList &events); protected: Threads threadsFromProperties(const QList &threadsProperties); QList threadsToProperties(const Threads &threads); Event eventFromProperties(const QVariantMap &properties); Events eventsFromProperties(const QList &eventsProperties); QList eventsToProperties(const Events &events); private: HistoryServiceAdaptor *mAdaptor; QDBusInterface mInterface; }; } #endif // MANAGERDBUS_P_H history-service-0.5/src/mmsstoragemanager.cpp000066400000000000000000000204731455450246700215460ustar00rootroot00000000000000/* * Copyright (C) 2022 Ubports Foundation. * * Authors: * Lionel Duboeuf * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 "mmsstoragemanager_p.h" #include "types.h" #include #define STORAGE_PATH "history-service/attachments" namespace History { MmsStorageManager::MmsStorageManager() { QString location = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation); QDir dir(location); if (!dir.mkpath(STORAGE_PATH)) { qCritical() << "Failed to create attachment dir" << STORAGE_PATH; } else { mStoragePath = dir.absoluteFilePath(STORAGE_PATH); qDebug() << " MMS Storage set to" << mStoragePath; } } MmsStorageManager *MmsStorageManager::instance() { static MmsStorageManager* manager = new MmsStorageManager(); return manager; } /** * @brief MmsStorageManager::mmsStoragePath * Build a complete file path from * the storage root path and encrypted parts from the input parameters * e.g: /home/phablet/.local/share/history-service/attachments/xxxaccountIdxxx/xxxthreadIdxxx/xxxeventIdxxx * if optional parameters are not set, build the path accordingly: mmsStoragePath("accountId") will return /home/phablet/.local/share/history-service/attachments/xxxaccountIdxxx * @param accountId * @param threadId * @param eventId * @return absolute filePath */ QString MmsStorageManager::mmsStoragePath(const QString &accountId, const QString &threadId, const QString &eventId) const { if (mStoragePath.isEmpty()) { return QString::null; } QString normalizedAccountId = QString(QCryptographicHash::hash(accountId.toLatin1(), QCryptographicHash::Md5).toHex()); QString normalizedThreadId = QString(QCryptographicHash::hash(threadId.toLatin1(), QCryptographicHash::Md5).toHex()); QString subPath; if (threadId.isEmpty()) { subPath = QString("/%1").arg(normalizedAccountId); } else if (eventId.isEmpty()) { subPath = QString("/%1/%2").arg(normalizedAccountId, normalizedThreadId); } else { QString normalizedEventId = QString(QCryptographicHash::hash(eventId.toLatin1(), QCryptographicHash::Md5).toHex()); subPath = QString("/%1/%2/%3").arg(normalizedAccountId, normalizedThreadId, normalizedEventId); } return QString("%1%2").arg(mStoragePath).arg(subPath); } /** * @brief MmsStorageManager::removeAttachments * Remove attachments stored at accountId/threadId/eventId * if eventId is omitted, remove the accountId/threadId and all childs * @param accountId * @param threadId * @param eventId * @return true if no issues */ bool MmsStorageManager::removeAttachments(const QString &accountId, const QString &threadId, const QString &eventId) { QString mmsStoragePath = this->mmsStoragePath(accountId, threadId, eventId); if (mmsStoragePath.isNull()) { qCritical() << "Failed to access attachment directory "; return false; } QDir dir(mmsStoragePath); return dir.removeRecursively(); } /** * @brief MmsStorageManager::removeAttachments * From a list of absolute file path, remove them * @param attachments * @return true if went fine */ bool MmsStorageManager::removeAttachments(QList attachments) { bool ok = true; QStringList eventIdsPath; QStringList threadIdsPath; // extract eventId and thread paths Q_FOREACH(const QString &filePath, attachments) { QFile file(filePath); QString eventIdPath = QFileInfo(file).dir().path(); if (!eventIdsPath.contains(eventIdPath)) { eventIdsPath << eventIdPath; } QString threadPath = QFileInfo(eventIdPath).dir().path(); if (!threadIdsPath.contains(threadPath)) { threadIdsPath << threadPath; } } // start by removing eventIds path Q_FOREACH(const QString &dirToRemove, eventIdsPath) { QDir dir(dirToRemove); ok &= dir.removeRecursively(); } // now we can remove threads path if they are empty Q_FOREACH(const QString &dirToRemove, threadIdsPath) { QDir dir(dirToRemove); if (dir.isEmpty()) { qDebug() << "removed thread path:" << dirToRemove; ok &= dir.removeRecursively(); } } return ok; } /** * @brief MmsStorageManager::removeAttachmentsFromThreads * From a Thread list, remove all ../accountId/threadId directory and childs * @param threads * @return true if went fine */ bool MmsStorageManager::removeAttachmentsFromThreads(const QList &threads) { bool ok = true; Q_FOREACH(const QVariantMap &thread, threads) { History::EventType type = (History::EventType) thread[History::FieldType].toInt(); if (type == History::EventTypeText){ QString accountId = thread[History::FieldAccountId].toString(); QString threadId = thread[History::FieldThreadId].toString(); if (!this->removeAttachments(accountId, threadId)) { qWarning() << "Failed to remove attachment directory for threadId" << threadId; ok = false; } } } return ok; } /** * @brief MmsStorageManager::removeAttachmentsFromEvents * From a Event list, remove all ../accountId/threadId/eventId directory and childs * @param events * @return */ bool MmsStorageManager::removeAttachmentsFromEvents(const QList &events) { bool ok = true; Q_FOREACH(const QVariantMap &event, events) { History::EventType type = (History::EventType) event[History::FieldType].toInt(); if (type == History::EventTypeText){ QString accountId = event[History::FieldAccountId].toString(); QString threadId = event[History::FieldThreadId].toString(); QString eventId = event[History::FieldEventId].toString(); if (!this->removeAttachments(accountId, threadId, eventId)){ qWarning() << "Failed to remove attachment directory for eventId" << eventId; ok = false; } } } return ok; } /** * @brief MmsStorageManager::saveAttachment * store content part, to ../accountId/threadId/eventId/xxx target * @param part * @param accountId * @param threadId * @param eventId * @return absolute saved filename */ QString MmsStorageManager::saveAttachment(const Tp::MessagePart &part, const QString &accountId, const QString &threadId, const QString &eventId) const { QString mmsStoragePath = this->mmsStoragePath(accountId, threadId, eventId); if (mmsStoragePath.isNull()) { qWarning() << "Failed to access attachment directory "; return QString::null; } QDir dir(mmsStoragePath); if (!dir.mkpath(mmsStoragePath)) { qWarning() << "Failed to create attachment directory" << mmsStoragePath; return QString::null; } QString fileName = part["identifier"].variant().toString(); //filename may have forbidden characters, strip them fileName.replace(QRegularExpression("[" + QRegularExpression::escape("\\/:?\"<>|") + "]"), ""); // we can have empty attachmentId, provide a fallback name if (fileName.isEmpty()) { dir.setFilter( QDir::AllEntries | QDir::NoDotAndDotDot ); fileName = QString::number( dir.count() + 1); } QFile file(mmsStoragePath + "/" + fileName); if (!file.open(QIODevice::WriteOnly)) { qWarning() << "Failed to save attachment to" << mmsStoragePath << "/" << fileName ; return QString::null; } else { file.write(part["content"].variant().toByteArray()); file.close(); } return file.fileName(); } /** * @brief MmsStorageManager::storageRootPath * Get the absolute directory path of the mms storage * @return absolute mms storage directory */ QString MmsStorageManager::storageRootPath() const { return mStoragePath; } } history-service-0.5/src/mmsstoragemanager_p.h000066400000000000000000000031741455450246700215310ustar00rootroot00000000000000/* * Copyright (C) 2022 Ubports Foundation. * * Authors: * Lionel Duboeuf * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 MMSSTORAGEMANAGER_H #define MMSSTORAGEMANAGER_H #include namespace History { class MmsStorageManager { public: static MmsStorageManager *instance(); QString mmsStoragePath(const QString &accountId, const QString &threadId = QString(), const QString &eventId = QString()) const; bool removeAttachments(const QString &accountId, const QString &threadId, const QString &eventId = QString()); bool removeAttachments(QList files); bool removeAttachmentsFromThreads(const QList &threads); bool removeAttachmentsFromEvents(const QList &events); QString saveAttachment(const Tp::MessagePart &part, const QString &accountId, const QString &threadId, const QString &eventId) const; QString storageRootPath() const; private: explicit MmsStorageManager(); QString mStoragePath; }; } #endif // MMSSTORAGEMANAGER_H history-service-0.5/src/participant.cpp000066400000000000000000000200571455450246700203460ustar00rootroot00000000000000/* * Copyright (C) 2015-2016 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 "participant.h" #include "participant_p.h" #include "types.h" namespace History { ParticipantPrivate::ParticipantPrivate() { } ParticipantPrivate::ParticipantPrivate(const QString &theAccountId, const QString &theIdentifier, const QString &theContactId, const QString &theAlias, const QString &theAvatar, const uint theState, const uint theRoles, const QVariantMap &theDetailProperties) : accountId(theAccountId), identifier(theIdentifier), contactId(theContactId), alias(theAlias), avatar(theAvatar), state(theState), roles(theRoles), detailProperties(theDetailProperties) { } ParticipantPrivate::~ParticipantPrivate() { } Participant::Participant() : d_ptr(new ParticipantPrivate()) { } Participant::Participant(const QString &accountId, const QString &identifier, const QString &contactId, const QString &alias, const QString &avatar, uint state, uint roles, const QVariantMap &detailProperties) : d_ptr(new ParticipantPrivate(accountId, identifier, contactId, alias, avatar, state, roles, detailProperties)) { } Participant::Participant(const Participant &other) : d_ptr(new ParticipantPrivate(*other.d_ptr)) { } Participant &Participant::operator=(const Participant &other) { if (&other == this) { return *this; } d_ptr = QSharedPointer(new ParticipantPrivate(*other.d_ptr)); return *this; } Participant::~Participant() { } QString Participant::accountId() const { Q_D(const Participant); return d->accountId; } QString Participant::identifier() const { Q_D(const Participant); return d->identifier; } QString Participant::contactId() const { Q_D(const Participant); return d->contactId; } QString Participant::alias() const { Q_D(const Participant); return d->alias; } QString Participant::avatar() const { Q_D(const Participant); return d->avatar; } uint Participant::state() const { Q_D(const Participant); return d->state; } uint Participant::roles() const { Q_D(const Participant); return d->roles; } QVariantMap Participant::detailProperties() const { Q_D(const Participant); return d->detailProperties; } bool Participant::isNull() const { Q_D(const Participant); return d->accountId.isNull() || d->identifier.isNull(); } bool Participant::operator==(const Participant &other) const { Q_D(const Participant); return d->accountId == other.d_ptr->accountId && d->identifier == other.d_ptr->identifier; } bool Participant::operator<(const Participant &other) const { Q_D(const Participant); QString selfData = d->accountId + d->identifier; QString otherData = other.d_ptr->accountId + other.d_ptr->identifier; return selfData < otherData; } QVariantMap Participant::properties() const { Q_D(const Participant); QVariantMap map; map[FieldAccountId] = d->accountId; map[FieldIdentifier] = d->identifier; map[FieldContactId] = d->contactId; map[FieldAlias] = d->alias; map[FieldAvatar] = d->avatar; map[FieldParticipantState] = d->state; map[FieldParticipantRoles] = d->roles; map[FieldDetailProperties] = d->detailProperties; return map; } Participant Participant::fromProperties(const QVariantMap &properties) { Participant participant; if (properties.isEmpty()) { return participant; } QString accountId = properties[FieldAccountId].toString(); QString identifier = properties[FieldIdentifier].toString(); QString contactId = properties[FieldContactId].toString(); QString alias = properties[FieldAlias].toString(); QString avatar = properties[FieldAvatar].toString(); uint state = properties[FieldParticipantState].toUInt(); uint roles = properties[FieldParticipantRoles].toUInt(); QVariantMap detailProperties; QVariant detailPropertiesVariant = properties[FieldDetailProperties]; if (detailPropertiesVariant.canConvert()) { detailProperties = detailPropertiesVariant.toMap(); } else if (detailPropertiesVariant.canConvert()) { detailProperties = qdbus_cast(detailPropertiesVariant); Q_FOREACH(const QString &key, detailProperties.keys()) { QList list = qdbus_cast >(detailProperties[key]); detailProperties[key] = QVariant::fromValue(list); } } return Participant(accountId, identifier, contactId, alias, avatar, state, roles, detailProperties); } QStringList Participants::identifiers() const { QStringList result; Q_FOREACH(const Participant &participant, *this) { result << participant.identifier(); } return result; } Participants Participants::fromVariant(const QVariant &variant) { Participants participants; if (variant.type() == QVariant::StringList) { participants = Participants::fromStringList(variant.toStringList()); } else if (variant.canConvert()) { participants = Participants::fromVariantList(variant.toList()); } else if (variant.canConvert()) { QDBusArgument argument = variant.value(); argument >> participants; } return participants; } Participants Participants::fromStringList(const QStringList &list) { Participants participants; Q_FOREACH(const QString& participantId, list) { QVariantMap properties; properties[FieldIdentifier] = participantId; participants << Participant::fromProperties(properties); } return participants; } Participants Participants::fromVariantList(const QVariantList &list) { Participants participants; Q_FOREACH(const QVariant& entry, list) { participants << Participant::fromProperties(entry.toMap()); } return participants; } Participants Participants::fromVariantMapList(const QList &list) { Participants participants; Q_FOREACH(const QVariantMap& entry, list) { participants << Participant::fromProperties(entry); } return participants; } QVariantList Participants::toVariantList() const { QVariantList list; Q_FOREACH(const Participant &participant, *this) { list << participant.properties(); } return list; } Participants Participants::filterByState(uint state) const { Participants filtered; Q_FOREACH(const Participant &participant, *this) { if (participant.state() == state) { filtered << participant; } } return filtered; } const QDBusArgument &operator>>(const QDBusArgument &argument, Participants &participants) { argument.beginArray(); while (!argument.atEnd()) { QVariantMap props; // we are casting from a QVariantList, so the inner argument is a QVariant and not the map directly // that's why this intermediate QVariant cast is needed QVariant variant; argument >> variant; QDBusArgument innerArgument = variant.value(); if (!innerArgument.atEnd()) { innerArgument >> props; } participants << Participant::fromProperties(props); } argument.endArray(); return argument; } } history-service-0.5/src/participant.h000066400000000000000000000054031455450246700200110ustar00rootroot00000000000000/* * Copyright (C) 2015-2016 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 HISTORY_PARTICIPANT_H #define HISTORY_PARTICIPANT_H #include #include #include #include #include #include namespace History { class ParticipantPrivate; class Participant { Q_DECLARE_PRIVATE(Participant) public: explicit Participant(); Participant(const QString &accountId, const QString &identifier, const QString &contactId = QString(), const QString &alias = QString(), const QString &avatar = QString(), uint state = 0, uint roles = 0, const QVariantMap &detailProperties = QVariantMap()); Participant(const Participant &other); Participant& operator=(const Participant &other); virtual ~Participant(); QString accountId() const; QString identifier() const; QString contactId() const; QString alias() const; QString avatar() const; uint state() const; uint roles() const; QVariantMap detailProperties() const; bool isNull() const; bool operator==(const Participant &other) const; bool operator<(const Participant &other) const; virtual QVariantMap properties() const; static Participant fromProperties(const QVariantMap &properties); protected: QSharedPointer d_ptr; }; // define the participants list with toVariantList() and fromVariantList() helpers class Participants : public QList { public: QStringList identifiers() const; static Participants fromVariant(const QVariant &variant); static Participants fromVariantList(const QVariantList &list); static Participants fromVariantMapList(const QList &list); static Participants fromStringList(const QStringList &list); QVariantList toVariantList() const; History::Participants filterByState(uint state) const; }; const QDBusArgument &operator>>(const QDBusArgument &argument, Participants &participants); } #endif // HISTORY_PARTICIPANT_H history-service-0.5/src/participant_p.h000066400000000000000000000032161455450246700203300ustar00rootroot00000000000000/* * Copyright (C) 2015 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 HISTORY_PARTICIPANT_P_H #define HISTORY_PARTICIPANT_P_H #include #include namespace History { class Participant; class ParticipantPrivate { public: explicit ParticipantPrivate(); ParticipantPrivate(const QString &theAccountId, const QString &theIdentifier, const QString &theContactId = QString(), const QString &theAlias = QString(), const QString &theAvatar = QString(), uint theState = 0, uint theRoles = 0, const QVariantMap &theDetailProperties = QVariantMap()); virtual ~ParticipantPrivate(); QString accountId; QString identifier; QString contactId; QString alias; QString avatar; uint state; uint roles; QVariantMap detailProperties; }; } #endif // HISTORY_PARTICIPANT_P_H history-service-0.5/src/phoneutils.cpp000066400000000000000000000077121455450246700202250ustar00rootroot00000000000000/* * Copyright (C) 2012-2015 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * Renato Araujo Oliveira Filho * Tiago Salem Herrmann * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 "phoneutils_p.h" #include #include #include #include #include namespace History { PhoneUtils::PhoneUtils(QObject *parent) : QObject(parent) { } QString PhoneUtils::region() { QString countryCode = QLocale::system().name().split("_").last(); if (countryCode.size() < 2) { // fallback to US if no valid country code was provided, otherwise libphonenumber // will fail to parse any numbers return QString("US"); } return countryCode; } QString PhoneUtils::normalizePhoneNumber(const QString &phoneNumber) { static i18n::phonenumbers::PhoneNumberUtil *phonenumberUtil = i18n::phonenumbers::PhoneNumberUtil::GetInstance(); if (!isPhoneNumber(phoneNumber)) { return phoneNumber; } std::string number = phoneNumber.toStdString(); phonenumberUtil->NormalizeDiallableCharsOnly(&number); return QString::fromStdString(number); } bool PhoneUtils::comparePhoneNumbers(const QString &phoneNumberA, const QString &phoneNumberB) { // just do a simple string comparison first if (phoneNumberA == phoneNumberB) { return true; } QString normalizedPhoneNumberA = normalizePhoneNumber(phoneNumberA); QString normalizedPhoneNumberB = normalizePhoneNumber(phoneNumberB); return compareNormalizedPhoneNumbers(normalizedPhoneNumberA, normalizedPhoneNumberB); } bool PhoneUtils::compareNormalizedPhoneNumbers(const QString &numberA, const QString &numberB) { if (numberA == numberB) { return true; } static i18n::phonenumbers::PhoneNumberUtil *phonenumberUtil = i18n::phonenumbers::PhoneNumberUtil::GetInstance(); if (numberA.size() < 7 || numberB.size() < 7) { return false; } i18n::phonenumbers::PhoneNumberUtil::MatchType match = phonenumberUtil-> IsNumberMatchWithTwoStrings(numberA.toStdString(), numberB.toStdString()); return (match > i18n::phonenumbers::PhoneNumberUtil::NO_MATCH); } bool PhoneUtils::isPhoneNumber(const QString &phoneNumber) { static i18n::phonenumbers::PhoneNumberUtil *phonenumberUtil = i18n::phonenumbers::PhoneNumberUtil::GetInstance(); i18n::phonenumbers::PhoneNumber number; i18n::phonenumbers::PhoneNumberUtil::ErrorType error; error = phonenumberUtil->Parse(phoneNumber.toStdString(), region().toStdString(), &number); switch(error) { case i18n::phonenumbers::PhoneNumberUtil::INVALID_COUNTRY_CODE_ERROR: qWarning() << "Invalid country code for:" << phoneNumber; return false; case i18n::phonenumbers::PhoneNumberUtil::NOT_A_NUMBER: qWarning() << "The phone number is not a valid number:" << phoneNumber; return false; case i18n::phonenumbers::PhoneNumberUtil::TOO_SHORT_AFTER_IDD: case i18n::phonenumbers::PhoneNumberUtil::TOO_SHORT_NSN: case i18n::phonenumbers::PhoneNumberUtil::TOO_LONG_NSN: qWarning() << "Invalid phone number" << phoneNumber; return false; default: break; } return true; } } history-service-0.5/src/phoneutils_p.h000066400000000000000000000027151455450246700202070ustar00rootroot00000000000000/* * Copyright (C) 2012-2015 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * Renato Araujo Oliveira Filho * Tiago Salem Herrmann * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 PHONEUTILS_H #define PHONEUTILS_H #include namespace History { class PhoneUtils : public QObject { Q_OBJECT public: explicit PhoneUtils(QObject *parent = 0); Q_INVOKABLE static bool comparePhoneNumbers(const QString &numberA, const QString &numberB); Q_INVOKABLE static bool compareNormalizedPhoneNumbers(const QString &numberA, const QString &numberB); Q_INVOKABLE static bool isPhoneNumber(const QString &identifier); Q_INVOKABLE static QString normalizePhoneNumber(const QString &identifier); private: static QString region(); }; } #endif // PHONEUTILS_H history-service-0.5/src/plugin.h000066400000000000000000000127421455450246700167750ustar00rootroot00000000000000/* * Copyright (C) 2013-2016 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 HISTORY_PLUGIN_H #define HISTORY_PLUGIN_H #include #include "filter.h" #include "types.h" #include "sort.h" #include namespace History { class PluginThreadView; class PluginEventView; class Plugin { public: virtual ~Plugin() {} virtual bool initialised() { return true; } // Reader part of the plugin virtual PluginThreadView* queryThreads(EventType type, const Sort &sort = Sort(), const Filter &filter = Filter(), const QVariantMap &properties = QVariantMap()) = 0; virtual PluginEventView* queryEvents(EventType type, const Sort &sort = Sort(), const Filter &filter = Filter()) = 0; virtual int eventsCount(EventType type, const Filter &filter = Filter()) = 0; virtual QVariantMap getSingleThread(EventType type, const QString &accountId, const QString &threadId, const QVariantMap &properties = QVariantMap()) = 0; virtual QVariantMap getSingleEvent(EventType type, const QString &accountId, const QString &threadId, const QString &eventId) = 0; virtual QVariantMap threadForParticipants(const QString &accountId, EventType type, const QStringList &participants, History::MatchFlags matchFlags = History::MatchCaseSensitive) = 0; virtual QVariantMap threadForProperties(const QString &accountId, EventType type, const QVariantMap &properties, History::MatchFlags matchFlags = History::MatchCaseSensitive) = 0; virtual QString threadIdForProperties(const QString &accountId, EventType type, const QVariantMap &properties, History::MatchFlags matchFlags = History::MatchCaseSensitive) = 0; virtual QList participantsForThreads(const QList &threadIds) = 0; virtual int unreadCount(EventType type) = 0; virtual QList eventsForThread(const QVariantMap &thread) = 0; virtual QList attachmentFilePathsForFilter(const History::Filter &filter) = 0; // Writer part of the plugin virtual QVariantMap createThreadForParticipants(const QString& /* accountId */, EventType /* type */, const QStringList& /* participants */) { return QVariantMap(); } virtual QVariantMap createThreadForProperties(const QString& /* accountId */, EventType /* type */, const QVariantMap& /* properties */) { return QVariantMap(); } virtual bool updateRoomParticipants(const QString& /* accountId */, const QString& /* threadId */, History::EventType /* type */, const QVariantList& /* participants */) { return false; }; virtual bool updateRoomParticipantsRoles(const QString& /* accountId */, const QString& /* threadId */, History::EventType /* type */, const QVariantMap& /* participantsRoles */) { return false; }; virtual bool updateRoomInfo(const QString& /* accountId */, const QString& /* threadId */, EventType /* type */, const QVariantMap& /* properties */, const QStringList& /* invalidated */ = QStringList()) { return false; }; virtual bool removeThread(const QVariantMap& /* thread */) { return false; } virtual QVariantMap markThreadAsRead(const QVariantMap& /* thread */) { return QVariantMap(); } virtual EventWriteResult writeTextEvent(const QVariantMap& /* event */) { return EventWriteError; } virtual bool removeTextEvent(const QVariantMap& /* event */) { return false; } virtual EventWriteResult writeVoiceEvent(const QVariantMap& /* event */) { return EventWriteError; } virtual bool removeVoiceEvent(const QVariantMap& /* event */) { return false; } virtual int removeEvents(EventType type, const Filter &filter = Filter()) = 0; virtual bool beginBatchOperation() { return false; } virtual bool endBatchOperation() { return false; } virtual bool rollbackBatchOperation() { return false; } // FIXME: this is hackish, but changing it required a broad refactory of HistoryDaemon virtual void generateContactCache() {} virtual void verifyAccount(const QString& accountId) {}; }; } Q_DECLARE_INTERFACE(History::Plugin, "com.lomiri.historyservice.Plugin") #endif // HISTORY_PLUGIN_H history-service-0.5/src/plugineventview.cpp000066400000000000000000000037351455450246700212670ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 "plugineventview.h" #include "plugineventview_p.h" #include "plugineventviewadaptor.h" #include "types.h" #include #include Q_DECLARE_METATYPE(QList< QVariantMap >) namespace History { PluginEventViewPrivate::PluginEventViewPrivate() : adaptor(0) { } PluginEventView::PluginEventView(QObject *parent) : QObject(parent), d_ptr(new PluginEventViewPrivate()) { Q_D(PluginEventView); qDBusRegisterMetaType >(); d->adaptor = new EventViewAdaptor(this); QString id = QString("eventview%1%2").arg(QString::number((qulonglong)this), QDateTime::currentDateTimeUtc().toString("yyyyMMddhhmmsszzz")); d->objectPath = QString("%1/%2").arg(History::DBusObjectPath, id); QDBusConnection::sessionBus().registerObject(d->objectPath, this); } PluginEventView::~PluginEventView() { Q_D(PluginEventView); QDBusConnection::sessionBus().unregisterObject(d->objectPath); } void PluginEventView::Destroy() { Q_D(PluginEventView); deleteLater(); } bool PluginEventView::IsValid() const { return true; } int PluginEventView::GetTotalCount() { return 0; } QString PluginEventView::objectPath() const { Q_D(const PluginEventView); return d->objectPath; } } history-service-0.5/src/plugineventview.h000066400000000000000000000027561455450246700207360ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 PLUGINEVENTVIEW_H #define PLUGINEVENTVIEW_H #include #include #include #include namespace History { class PluginEventViewPrivate; class PluginEventView : public QObject, public QDBusContext { Q_OBJECT Q_DECLARE_PRIVATE(PluginEventView) public: explicit PluginEventView(QObject *parent = 0); virtual ~PluginEventView(); // DBus exposed methods Q_NOREPLY void Destroy(); virtual QList NextPage() = 0; virtual bool IsValid() const; virtual int GetTotalCount() = 0; // other methods QString objectPath() const; Q_SIGNALS: void Invalidated(); private: QScopedPointer d_ptr; }; } #endif // PLUGINEVENTVIEW_H history-service-0.5/src/plugineventview_p.h000066400000000000000000000020251455450246700212420ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 PLUGINEVENTVIEW_P_H #define PLUGINEVENTVIEW_P_H #include class EventViewAdaptor; namespace History { class PluginEventViewPrivate { public: PluginEventViewPrivate(); EventViewAdaptor *adaptor; QString objectPath; }; } #endif // PLUGINEVENTVIEW_P_H history-service-0.5/src/pluginthreadview.cpp000066400000000000000000000036701455450246700214130ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 "pluginthreadview.h" #include "pluginthreadview_p.h" #include "pluginthreadviewadaptor.h" #include "types.h" #include #include Q_DECLARE_METATYPE(QList< QVariantMap >) namespace History { PluginThreadViewPrivate::PluginThreadViewPrivate() : adaptor(0) { } PluginThreadView::PluginThreadView(QObject *parent) : QObject(parent), d_ptr(new PluginThreadViewPrivate()) { Q_D(PluginThreadView); qDBusRegisterMetaType >(); d->adaptor = new ThreadViewAdaptor(this); QString id = QString("threadview%1%2").arg(QString::number((qulonglong)this), QDateTime::currentDateTimeUtc().toString("yyyyMMddhhmmsszzz")); d->objectPath = QString("%1/%2").arg(History::DBusObjectPath, id); QDBusConnection::sessionBus().registerObject(d->objectPath, this); } PluginThreadView::~PluginThreadView() { Q_D(PluginThreadView); QDBusConnection::sessionBus().unregisterObject(d->objectPath); } void PluginThreadView::Destroy() { Q_D(PluginThreadView); deleteLater(); } bool PluginThreadView::IsValid() const { return true; } QString PluginThreadView::objectPath() const { Q_D(const PluginThreadView); return d->objectPath; } } history-service-0.5/src/pluginthreadview.h000066400000000000000000000027221455450246700210550ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 PLUGINTHREADVIEW_H #define PLUGINTHREADVIEW_H #include #include #include #include namespace History { class PluginThreadViewPrivate; class PluginThreadView : public QObject, public QDBusContext { Q_OBJECT Q_DECLARE_PRIVATE(PluginThreadView) public: explicit PluginThreadView(QObject *parent = 0); virtual ~PluginThreadView(); // DBus exposed methods Q_NOREPLY void Destroy(); virtual QList NextPage() = 0; virtual bool IsValid() const; // other methods QString objectPath() const; Q_SIGNALS: void Invalidated(); private: QScopedPointer d_ptr; }; } #endif // PLUGINTHREADVIEW_H history-service-0.5/src/pluginthreadview_p.h000066400000000000000000000020341455450246700213700ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 PLUGINTHREADVIEW_P_H #define PLUGINTHREADVIEW_P_H #include class ThreadViewAdaptor; namespace History { class PluginThreadViewPrivate { public: PluginThreadViewPrivate(); ThreadViewAdaptor *adaptor; QString objectPath; }; } #endif // PLUGINTHREADVIEW_P_H history-service-0.5/src/sort.cpp000066400000000000000000000053541455450246700170220ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 "sort.h" #include "sort_p.h" namespace History { // ------------- SortPrivate ------------------------------------------------ SortPrivate::SortPrivate(const QString &theSortField, Qt::SortOrder theSortOrder, Qt::CaseSensitivity theCaseSensitivity) : sortField(theSortField), sortOrder(theSortOrder), caseSensitivity(theCaseSensitivity) { } SortPrivate::~SortPrivate() { } // ------------- Sort ------------------------------------------------------- Sort::Sort(const QString &sortField, Qt::SortOrder sortOrder, Qt::CaseSensitivity caseSensitivity) : d_ptr(new SortPrivate(sortField, sortOrder, caseSensitivity)) { } Sort::Sort(const Sort &other) : d_ptr(new SortPrivate(*other.d_ptr)) { } Sort::~Sort() { } QString Sort::sortField() const { Q_D(const Sort); return d->sortField; } void Sort::setSortField(const QString &value) { Q_D(Sort); d->sortField = value; } Qt::SortOrder Sort::sortOrder() const { Q_D(const Sort); return d->sortOrder; } void Sort::setSortOrder(Qt::SortOrder value) { Q_D(Sort); d->sortOrder = value; } Qt::CaseSensitivity Sort::caseSensitivity() const { Q_D(const Sort); return d->caseSensitivity; } void Sort::setCaseSensitivity(Qt::CaseSensitivity value) { Q_D(Sort); d->caseSensitivity = value; } QVariantMap Sort::properties() const { Q_D(const Sort); QVariantMap map; map[FieldSortField] = d->sortField; map[FieldSortOrder] = (int)d->sortOrder; map[FieldCaseSensitivity] = (int)d->caseSensitivity; return map; } Sort Sort::fromProperties(const QVariantMap &properties) { Sort sort; if (properties.isEmpty()) { return sort; } sort = Sort(properties[FieldSortField].toString(), (Qt::SortOrder) properties[FieldSortOrder].toInt(), (Qt::CaseSensitivity) properties[FieldCaseSensitivity].toInt()); return sort; } } history-service-0.5/src/sort.h000066400000000000000000000032101455450246700164540ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 HISTORY_SORT_H #define HISTORY_SORT_H #include #include #include #include #include "types.h" namespace History { class SortPrivate; class Sort { Q_DECLARE_PRIVATE(Sort) public: Sort(const QString &sortField = FieldTimestamp, Qt::SortOrder sortOrder = Qt::AscendingOrder, Qt::CaseSensitivity caseSensitivity = Qt::CaseInsensitive); Sort(const Sort &other); ~Sort(); QString sortField() const; void setSortField(const QString &value); Qt::SortOrder sortOrder() const; void setSortOrder(Qt::SortOrder value); Qt::CaseSensitivity caseSensitivity() const; void setCaseSensitivity(Qt::CaseSensitivity value); QVariantMap properties() const; static Sort fromProperties(const QVariantMap &properties); protected: QSharedPointer d_ptr; }; } #endif // HISTORY_SORT_H history-service-0.5/src/sort_p.h000066400000000000000000000023051455450246700167770ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 HISTORY_SORT_P_H #define HISTORY_SORT_P_H #include #include "types.h" namespace History { class Sort; class SortPrivate { public: SortPrivate(const QString &theSortField, Qt::SortOrder theSortOrder, Qt::CaseSensitivity theCaseSensitivity); virtual ~SortPrivate(); QString sortField; Qt::SortOrder sortOrder; Qt::CaseSensitivity caseSensitivity; }; } #endif // HISTORY_SORT_P_H history-service-0.5/src/telepathyhelper.cpp000066400000000000000000000126461455450246700212340ustar00rootroot00000000000000/* * Copyright (C) 2012-2015 Canonical, Ltd. * * Authors: * Tiago Salem Herrmann * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 "telepathyhelper_p.h" #include #include #include namespace History { TelepathyHelper::TelepathyHelper(QObject *parent) : QObject(parent), mChannelObserver(0), mReady(false) { // initialize telepathy Tp::registerTypes(); Tp::enableWarnings(true); mAccountFeatures << Tp::Account::FeatureCore << Tp::Account::FeatureProtocolInfo; mContactFeatures << Tp::Contact::FeatureAlias << Tp::Contact::FeatureAvatarData << Tp::Contact::FeatureAvatarToken << Tp::Contact::FeatureCapabilities << Tp::Contact::FeatureSimplePresence; mConnectionFeatures << Tp::Connection::FeatureCore << Tp::Connection::FeatureSelfContact << Tp::Connection::FeatureSimplePresence; Tp::ChannelFactoryPtr channelFactory = Tp::ChannelFactory::create(QDBusConnection::sessionBus()); channelFactory->addCommonFeatures(Tp::Channel::FeatureCore); mAccountManager = Tp::AccountManager::create( Tp::AccountFactory::create(QDBusConnection::sessionBus(), mAccountFeatures), Tp::ConnectionFactory::create(QDBusConnection::sessionBus(), mConnectionFeatures), channelFactory, Tp::ContactFactory::create(mContactFeatures)); connect(mAccountManager->becomeReady(Tp::AccountManager::FeatureCore), SIGNAL(finished(Tp::PendingOperation*)), SLOT(onAccountManagerReady(Tp::PendingOperation*))); mClientRegistrar = Tp::ClientRegistrar::create(mAccountManager); } TelepathyHelper::~TelepathyHelper() { } TelepathyHelper *TelepathyHelper::instance() { static TelepathyHelper* helper = new TelepathyHelper(); return helper; } bool TelepathyHelper::ready() const { return mReady; } ChannelObserver *TelepathyHelper::channelObserver() const { return mChannelObserver; } void TelepathyHelper::registerChannelObserver() { // check if this instance is running on the main phone application // or if it is just the plugin imported somewhere else QString observerName = "HistoryDaemonObserver"; mChannelObserver = new ChannelObserver(this); registerClient(mChannelObserver, observerName); Q_EMIT channelObserverCreated(mChannelObserver); } void TelepathyHelper::registerClient(Tp::AbstractClient *client, QString name) { Tp::AbstractClientPtr clientPtr(client); bool succeeded = mClientRegistrar->registerClient(clientPtr, name); if (!succeeded) { name.append("%1"); int count = 0; // limit the number of registered clients to 20, that should be a safe margin while (!succeeded && count < 20) { succeeded = mClientRegistrar->registerClient(clientPtr, name.arg(++count)); if (succeeded) { name = name.arg(count); } } } if (succeeded) { QObject *object = dynamic_cast(client); if (object) { object->setProperty("clientName", TP_QT_IFACE_CLIENT + "." + name ); } } } Tp::AccountPtr TelepathyHelper::accountForId(const QString &accountId) { Q_FOREACH(const Tp::AccountPtr &account, mAccounts) { if (account->uniqueIdentifier() == accountId) { return account; } } return Tp::AccountPtr(); } QList TelepathyHelper::accounts() const { return mAccounts; } void TelepathyHelper::onAccountManagerReady(Tp::PendingOperation *op) { Q_UNUSED(op) Q_FOREACH(const Tp::AccountPtr &account, mAccountManager->allAccounts()) { onNewAccount(account); } connect(mAccountManager.data(), SIGNAL(newAccount(Tp::AccountPtr)), SLOT(onNewAccount(Tp::AccountPtr))); mReady = true; Q_EMIT setupReady(); } void TelepathyHelper::onNewAccount(const Tp::AccountPtr &account) { connect(account.data(), SIGNAL(removed()), SLOT(onAccountRemoved())); mAccounts.append(account); Q_EMIT accountAdded(account); } void TelepathyHelper::onAccountRemoved() { Tp::AccountPtr account(qobject_cast(sender())); if (account.isNull() || !mAccounts.contains(account)) { qWarning() << "The removed account was not found."; return; } account.data()->disconnect(this); mAccounts.removeAll(account); Q_EMIT accountRemoved(account); } bool TelepathyHelper::connected() const { Q_FOREACH(const Tp::AccountPtr &account, mAccounts) { if (!account->connection().isNull()) { return true; } } return false; } } history-service-0.5/src/telepathyhelper_p.h000066400000000000000000000044611455450246700212140ustar00rootroot00000000000000/* * Copyright (C) 2012-2015 Canonical, Ltd. * * Authors: * Tiago Salem Herrmann * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 TELEPATHYHELPER_H #define TELEPATHYHELPER_H #include #include #include #include #include #include #include "channelobserver_p.h" namespace History { class TelepathyHelper : public QObject { Q_OBJECT public: ~TelepathyHelper(); static TelepathyHelper *instance(); ChannelObserver *channelObserver() const; void registerClient(Tp::AbstractClient *client, QString name); Tp::AccountPtr accountForId(const QString &accountId); QList accounts() const; bool connected() const; bool ready() const; Q_SIGNALS: void channelObserverCreated(ChannelObserver *observer); void accountAdded(const Tp::AccountPtr &account); void accountRemoved(const Tp::AccountPtr &account); void setupReady(); public Q_SLOTS: void registerChannelObserver(); private Q_SLOTS: void onAccountManagerReady(Tp::PendingOperation *op); void onNewAccount(const Tp::AccountPtr &account); void onAccountRemoved(); private: explicit TelepathyHelper(QObject *parent = 0); Tp::AccountManagerPtr mAccountManager; Tp::Features mAccountManagerFeatures; Tp::Features mAccountFeatures; Tp::Features mContactFeatures; Tp::Features mConnectionFeatures; Tp::ClientRegistrarPtr mClientRegistrar; ChannelObserver *mChannelObserver; QList mAccounts; bool mReady; }; } #endif // TELEPATHYHELPER_H history-service-0.5/src/textevent.cpp000066400000000000000000000214701455450246700200560ustar00rootroot00000000000000/* * Copyright (C) 2013-2015 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 #include #include "textevent.h" #include "textevent_p.h" #include "texteventattachment.h" Q_DECLARE_METATYPE(QList< QVariantMap >) namespace History { // ------------- TextEventPrivate ------------------------------------------------ TextEventPrivate::TextEventPrivate() { } TextEventPrivate::TextEventPrivate(const QString &theAccountId, const QString &theThreadId, const QString &theEventId, const QString &theSender, const QDateTime &theTimestamp, const QDateTime &theSentTime, bool theNewEvent, const QString &theMessage, MessageType theMessageType, MessageStatus theMessageStatus, const QDateTime &theReadTimestamp, const QString &theSubject, InformationType theInformationType, const TextEventAttachments &theAttachments, const Participants &theParticipants) : EventPrivate(theAccountId, theThreadId, theEventId, theSender, theTimestamp, theNewEvent, theParticipants), message(theMessage), messageType(theMessageType), messageStatus(theMessageStatus), readTimestamp(theReadTimestamp), subject(theSubject), informationType(theInformationType), attachments(theAttachments), sentTime(theSentTime) { } TextEventPrivate::~TextEventPrivate() { } EventType TextEventPrivate::type() const { return EventTypeText; } QVariantMap TextEventPrivate::properties() const { QVariantMap map = EventPrivate::properties(); map[FieldMessage] = message; map[FieldMessageType] = (int)messageType; map[FieldMessageStatus] = (int)messageStatus; map[FieldReadTimestamp] = readTimestamp.toString("yyyy-MM-ddTHH:mm:ss.zzz"); map[FieldSentTime] = sentTime.toString("yyyy-MM-ddTHH:mm:ss.zzz"); map[FieldSubject] = subject; map[FieldInformationType] = informationType; QList attachmentsMap; Q_FOREACH(const TextEventAttachment &attachment, attachments) { attachmentsMap << attachment.properties(); } map[FieldAttachments] = QVariant::fromValue(attachmentsMap); return map; } // ------------- TextEvent ------------------------------------------------------- HISTORY_EVENT_DEFINE_COPY(TextEvent, EventTypeText) TextEvent::TextEvent() : Event(*new TextEventPrivate()) { } TextEvent::TextEvent(const QString &accountId, const QString &threadId, const QString &eventId, const QString &sender, const QDateTime ×tamp, const QDateTime &sentTime, bool newEvent, const QString &message, MessageType messageType, MessageStatus messageStatus, const QDateTime &readTimestamp, const QString &subject, InformationType informationType, const TextEventAttachments &attachments, const Participants &participants) : Event(*new TextEventPrivate(accountId, threadId, eventId, sender, timestamp, sentTime, newEvent, message, messageType, messageStatus, readTimestamp, subject, informationType, attachments, participants)) { qDBusRegisterMetaType >(); qRegisterMetaType >(); } TextEvent::TextEvent(const QString &accountId, const QString &threadId, const QString &eventId, const QString &sender, const QDateTime ×tamp, bool newEvent, const QString &message, MessageType messageType, MessageStatus messageStatus, const QDateTime &readTimestamp, const QString &subject, InformationType informationType, const TextEventAttachments &attachments, const Participants &participants) : Event(*new TextEventPrivate(accountId, threadId, eventId, sender, timestamp, timestamp, newEvent, message, messageType, messageStatus, readTimestamp, subject, informationType, attachments, participants)) { qDBusRegisterMetaType >(); qRegisterMetaType >(); } TextEvent::~TextEvent() { } QString TextEvent::message() const { Q_D(const TextEvent); return d->message; } MessageType TextEvent::messageType() const { Q_D(const TextEvent); return d->messageType; } MessageStatus TextEvent::messageStatus() const { Q_D(const TextEvent); return d->messageStatus; } void TextEvent::setMessageStatus(const MessageStatus &value) { Q_D(TextEvent); d->messageStatus = value; } QDateTime TextEvent::sentTime() const { Q_D(const TextEvent); return d->sentTime; } void TextEvent::setSentTime(const QDateTime &value) { Q_D(TextEvent); d->sentTime = value; } QDateTime TextEvent::readTimestamp() const { Q_D(const TextEvent); return d->readTimestamp; } void TextEvent::setReadTimestamp(const QDateTime &value) { Q_D(TextEvent); d->readTimestamp = value; } QString TextEvent::subject() const { Q_D(const TextEvent); return d->subject; } InformationType TextEvent::informationType() const { Q_D(const TextEvent); return d->informationType; } TextEventAttachments TextEvent::attachments() const { Q_D(const TextEvent); return d->attachments; } Event TextEvent::fromProperties(const QVariantMap &properties) { Event event; if (properties.isEmpty()) { return event; } QString accountId = properties[FieldAccountId].toString(); QString threadId = properties[FieldThreadId].toString(); QString eventId = properties[FieldEventId].toString(); QString senderId = properties[FieldSenderId].toString(); QDateTime timestamp = QDateTime::fromString(properties[FieldTimestamp].toString(), Qt::ISODate); QDateTime sentTime = QDateTime::fromString(properties[FieldSentTime].toString(), Qt::ISODate); bool newEvent = properties[FieldNewEvent].toBool(); Participants participants = Participants::fromVariant(properties[FieldParticipants]); QString message = properties[FieldMessage].toString(); QString subject = properties[FieldSubject].toString(); InformationType informationType = (InformationType) properties[FieldInformationType].toInt(); MessageType messageType = (MessageType) properties[FieldMessageType].toInt(); MessageStatus messageStatus = (MessageStatus) properties[FieldMessageStatus].toInt(); QDateTime readTimestamp = QDateTime::fromString(properties[FieldReadTimestamp].toString(), Qt::ISODate); // read the attachments QList attachmentProperties = qdbus_cast >(properties[FieldAttachments]); // dbus_cast fails if the map was generated by a qml app, so we demarshal it by hand if (attachmentProperties.isEmpty()) { QVariantList attachmentList = properties[FieldAttachments].toList(); Q_FOREACH(const QVariant &attachmentMap, attachmentList) { attachmentProperties << attachmentMap.toMap(); } } TextEventAttachments attachments; Q_FOREACH(const QVariantMap &map, attachmentProperties) { TextEventAttachment attachment = TextEventAttachment::fromProperties(map); if (!attachment.isNull()) { attachments << attachment; } } // sentTime may be empty if (!sentTime.isValid()) { sentTime = timestamp; } // and finally create the event event = TextEvent(accountId, threadId, eventId, senderId, timestamp, sentTime, newEvent, message, messageType, messageStatus, readTimestamp, subject, informationType, attachments, participants); return event; } } history-service-0.5/src/textevent.h000066400000000000000000000061441455450246700175240ustar00rootroot00000000000000/* * Copyright (C) 2013-2015 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 HISTORY_TEXTEVENT_H #define HISTORY_TEXTEVENT_H #include "event.h" #include "texteventattachment.h" namespace History { class TextEventPrivate; class ItemFactory; class TextEvent : public Event { Q_DECLARE_PRIVATE(TextEvent) friend class ItemFactory; public: explicit TextEvent(); TextEvent(const QString &accountId, const QString &threadId, const QString &eventId, const QString &sender, const QDateTime ×tamp, bool newEvent, const QString &message, MessageType messageType, MessageStatus messageStatus = MessageStatusUnknown, const QDateTime &readTimestamp = QDateTime(), const QString &subject = QString(), InformationType informationType = InformationTypeNone, const TextEventAttachments &attachments = TextEventAttachments(), const Participants &participants = Participants()); TextEvent(const QString &accountId, const QString &threadId, const QString &eventId, const QString &sender, const QDateTime ×tamp, const QDateTime &sentTime, bool newEvent, const QString &message, MessageType messageType, MessageStatus messageStatus = MessageStatusUnknown, const QDateTime &readTimestamp = QDateTime(), const QString &subject = QString(), InformationType informationType = InformationTypeNone, const TextEventAttachments &attachments = TextEventAttachments(), const Participants &participants = Participants()); ~TextEvent(); // copy related members TextEvent(const Event &other); TextEvent& operator=(const Event &other); QString message() const; MessageType messageType() const; MessageStatus messageStatus() const; void setMessageStatus(const MessageStatus &value); QDateTime sentTime() const; void setSentTime(const QDateTime &value); QDateTime readTimestamp() const; void setReadTimestamp(const QDateTime &value); QString subject() const; InformationType informationType() const; TextEventAttachments attachments() const; static Event fromProperties(const QVariantMap &properties); }; } #endif // HISTORY_TEXTEVENT_H history-service-0.5/src/textevent_p.h000066400000000000000000000041341455450246700200400ustar00rootroot00000000000000/* * Copyright (C) 2013-2015 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 HISTORY_TEXTEVENT_P_H #define HISTORY_TEXTEVENT_P_H #include "event_p.h" #include "types.h" namespace History { class TextEvent; class TextEventPrivate : public EventPrivate { public: TextEventPrivate(); TextEventPrivate(const QString &theAccountId, const QString &theThreadId, const QString &theEventId, const QString &theSender, const QDateTime &theTimestamp, const QDateTime &theSentTime, bool theNewEvent, const QString &theMessage, MessageType theMessageType, MessageStatus theMessageStatus, const QDateTime &theReadTimestamp, const QString &theSubject, InformationType theInformationType, const TextEventAttachments &theAttachments, const Participants &theParticipants); ~TextEventPrivate(); QString message; MessageType messageType; MessageStatus messageStatus; QDateTime readTimestamp; QString subject; InformationType informationType; TextEventAttachments attachments; QDateTime sentTime; EventType type() const; QVariantMap properties() const; HISTORY_EVENT_DECLARE_CLONE(TextEvent) }; } #endif // HISTORY_TEXTEVENT_P_H history-service-0.5/src/texteventattachment.cpp000066400000000000000000000136071455450246700221320ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * Tiago Salem Herrmann * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 "texteventattachment.h" #include "texteventattachment_p.h" namespace History { // ------------- TextEventAttachmentPrivate ------------------------------------------------ TextEventAttachmentPrivate::TextEventAttachmentPrivate() { } TextEventAttachmentPrivate::TextEventAttachmentPrivate(const QString &theAccountId, const QString &theThreadId, const QString &theEventId, const QString &theAttachmentId, const QString &theContentType, const QString &theFilePath, const AttachmentFlags &theStatus) : accountId(theAccountId), threadId(theThreadId), eventId(theEventId), attachmentId(theAttachmentId), contentType(theContentType), filePath(theFilePath), status(theStatus) { } TextEventAttachmentPrivate::~TextEventAttachmentPrivate() { } // ------------- TextEventAttachment ------------------------------------------------------ /*! * \class TextEventAttachment * * \brief The TextEventAttachment class provides a way to store a single attachment * belonging to a text event. */ TextEventAttachment::TextEventAttachment() : d_ptr(new TextEventAttachmentPrivate()) { } TextEventAttachment::TextEventAttachment(const QString &accountId, const QString &threadId, const QString &eventId, const QString &attachmentId, const QString &contentType, const QString &filePath, const AttachmentFlags &status) : d_ptr(new TextEventAttachmentPrivate(accountId, threadId, eventId, attachmentId, contentType, filePath, status)) { } TextEventAttachment::TextEventAttachment(const TextEventAttachment &other) : d_ptr(new TextEventAttachmentPrivate(*other.d_ptr)) { } TextEventAttachment::~TextEventAttachment() { } TextEventAttachment& TextEventAttachment::operator=(const TextEventAttachment &other) { d_ptr = QSharedPointer(new TextEventAttachmentPrivate(*other.d_ptr)); return *this; } /*! * \brief Returns the account ID this attachment belongs to. */ QString TextEventAttachment::accountId() const { Q_D(const TextEventAttachment); return d->accountId; } /*! * \brief Returns the thread ID this attachment belongs to. */ QString TextEventAttachment::threadId() const { Q_D(const TextEventAttachment); return d->threadId; } /*! * \brief Returns the event ID this attachment belongs to. */ QString TextEventAttachment::eventId() const { Q_D(const TextEventAttachment); return d->eventId; } /*! * \brief Returns the attachment ID */ QString TextEventAttachment::attachmentId() const { Q_D(const TextEventAttachment); return d->attachmentId; } /*! * \brief Returns the content type of this attachment */ QString TextEventAttachment::contentType() const { Q_D(const TextEventAttachment); return d->contentType; } /*! * \brief Returns the file path of this attachment */ QString TextEventAttachment::filePath() const { Q_D(const TextEventAttachment); return d->filePath; } /*! * \brief Returns the status of this attachment */ AttachmentFlags TextEventAttachment::status() const { Q_D(const TextEventAttachment); return d->status; } QVariantMap TextEventAttachment::properties() const { Q_D(const TextEventAttachment); QVariantMap map; map[FieldAccountId] = d->accountId; map[FieldThreadId] = d->threadId; map[FieldEventId] = d->eventId; map[FieldAttachmentId] = d->attachmentId; map[FieldContentType] = d->contentType; map[FieldFilePath] = d->filePath; map[FieldStatus] = (int)d->status; return map; } TextEventAttachment TextEventAttachment::fromProperties(const QVariantMap &properties) { TextEventAttachment attachment; if (properties.isEmpty()) { return attachment; } attachment = TextEventAttachment(properties[FieldAccountId].toString(), properties[FieldThreadId].toString(), properties[FieldEventId].toString(), properties[FieldAttachmentId].toString(), properties[FieldContentType].toString(), properties[FieldFilePath].toString(), (History::AttachmentFlags)properties[FieldStatus].toInt()); return attachment; } bool TextEventAttachment::isNull() const { Q_D(const TextEventAttachment); return d->accountId.isNull() && d->threadId.isNull() && d->eventId.isNull() && d->attachmentId.isNull(); } bool TextEventAttachment::operator==(const TextEventAttachment &other) { Q_D(TextEventAttachment); if (d->accountId != other.d_ptr->accountId) { return false; } if (d->threadId != other.d_ptr->threadId) { return false; } if (d->eventId != other.d_ptr->eventId) { return false; } if (d->attachmentId != other.d_ptr->attachmentId) { return false; } return true; } } history-service-0.5/src/texteventattachment.h000066400000000000000000000043421455450246700215730ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * Tiago Salem Herrmann * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 HISTORY_TEXT_EVENT_ATTACHMENT_H #define HISTORY_TEXT_EVENT_ATTACHMENT_H #include #include #include "types.h" namespace History { class TextEventAttachmentPrivate; class ItemFactory; class TextEventAttachment { Q_DECLARE_PRIVATE(TextEventAttachment) friend class ItemFactory; public: explicit TextEventAttachment(); TextEventAttachment(const QString &accountId, const QString &threadId, const QString &eventId, const QString &attachmentId, const QString &contentType, const QString &filePath, const History::AttachmentFlags &status = History::AttachmentDownloaded); TextEventAttachment(const TextEventAttachment &other); virtual ~TextEventAttachment(); TextEventAttachment& operator=(const TextEventAttachment &other); QString accountId() const; QString threadId() const; QString eventId() const; QString attachmentId() const; QString contentType() const; QString filePath() const; History::AttachmentFlags status() const; virtual QVariantMap properties() const; static TextEventAttachment fromProperties(const QVariantMap &properties); bool isNull() const; bool operator==(const TextEventAttachment &other); protected: QSharedPointer d_ptr; }; typedef QList TextEventAttachments; } #endif // HISTORY_TEXT_EVENT_ATTACHMENT_H history-service-0.5/src/texteventattachment_p.h000066400000000000000000000033131455450246700221070ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * Tiago Salem Herrmann * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 HISTORY_TEXT_EVENT_ATTACHMENT_P_H #define HISTORY_TEXT_EVENT_ATTACHMENT_P_H #include #include "types.h" namespace History { class TextEventAttachment; class TextEventAttachmentPrivate { public: explicit TextEventAttachmentPrivate(); TextEventAttachmentPrivate(const QString &theAccountId, const QString &theThreadId, const QString &theEventId, const QString &theAttachmentId, const QString &theContentType, const QString &theFilePath, const History::AttachmentFlags &theStatus); virtual ~TextEventAttachmentPrivate(); QString accountId; QString threadId; QString eventId; QString attachmentId; QString contentType; QString filePath; History::AttachmentFlags status; }; } #endif // HISTORY_TEXT_EVENT_ATTACHMENT_P_H history-service-0.5/src/thread.cpp000066400000000000000000000211511455450246700172730ustar00rootroot00000000000000/* * Copyright (C) 2013-2016 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 "thread.h" #include "thread_p.h" #include "textevent.h" #include "voiceevent.h" #include #include Q_DECLARE_METATYPE(QList< QVariantMap >) namespace History { // ------------- ThreadPrivate ------------------------------------------------ ThreadPrivate::ThreadPrivate() { } ThreadPrivate::ThreadPrivate(const QString &theAccountId, const QString &theThreadId, EventType theType, const Participants &theParticipants, const QDateTime &theTimestamp, const Event &theLastEvent, int theCount, int theUnreadCount, const Threads &theGroupedThreads, ChatType theChatType, const QVariantMap &theChatRoomInfo) : accountId(theAccountId), threadId(theThreadId), type(theType), participants(theParticipants), timestamp(theTimestamp), lastEvent(theLastEvent), count(theCount), unreadCount(theUnreadCount), groupedThreads(theGroupedThreads), chatType(theChatType), chatRoomInfo(theChatRoomInfo) { } ThreadPrivate::~ThreadPrivate() { } // ------------- Thread ------------------------------------------------------ Thread::Thread() : d_ptr(new ThreadPrivate()) { } Thread::Thread(const QString &accountId, const QString &threadId, EventType type, const Participants &participants, const QDateTime ×tamp, const Event &lastEvent, int count, int unreadCount, const Threads &groupedThreads, ChatType chatType, const QVariantMap &chatRoomInfo) : d_ptr(new ThreadPrivate(accountId, threadId, type, participants, timestamp, lastEvent, count, unreadCount, groupedThreads, chatType, chatRoomInfo)) { qDBusRegisterMetaType >(); qRegisterMetaType >(); } Thread::Thread(const Thread &other) : d_ptr(new ThreadPrivate(*other.d_ptr)) { } Thread::~Thread() { } Thread &Thread::operator=(const Thread &other) { if (&other == this) { return *this; } d_ptr = QSharedPointer(new ThreadPrivate(*other.d_ptr)); return *this; } QString Thread::accountId() const { Q_D(const Thread); return d->accountId; } QString Thread::threadId() const { Q_D(const Thread); return d->threadId; } EventType Thread::type() const { Q_D(const Thread); return d->type; } Participants Thread::participants() const { Q_D(const Thread); return d->participants; } QDateTime Thread::timestamp() const { Q_D(const Thread); return d->timestamp; } Event Thread::lastEvent() const { Q_D(const Thread); return d->lastEvent; } int Thread::count() const { Q_D(const Thread); return d->count; } int Thread::unreadCount() const { Q_D(const Thread); return d->unreadCount; } History::Threads Thread::groupedThreads() const { Q_D(const Thread); return d->groupedThreads; } ChatType Thread::chatType() const { Q_D(const Thread); return d->chatType; } QVariantMap Thread::chatRoomInfo() const { Q_D(const Thread); return d->chatRoomInfo; } bool Thread::isNull() const { Q_D(const Thread); return d->accountId.isNull() && d->threadId.isNull() && d->participants.isEmpty(); } bool Thread::operator ==(const Thread &other) const { Q_D(const Thread); if (d->type != other.d_ptr->type) { return false; } if (d->accountId != other.d_ptr->accountId) { return false; } if (d->threadId != other.d_ptr->threadId) { return false; } return true; } bool Thread::operator<(const Thread &other) const { QString selfData = QString::number(type()) + accountId() + threadId(); QString otherData = QString::number(other.type()) + other.accountId() + other.threadId(); return selfData < otherData; } void Thread::removeParticipants(const Participants &participants) { Q_D(Thread); Q_FOREACH(const Participant &participant, participants) { d->participants.removeAll(participant); } } void Thread::addParticipants(const Participants &participants) { Q_D(Thread); Q_FOREACH(const Participant &participant, participants) { d->participants.append(participant); } } QVariantMap Thread::properties() const { Q_D(const Thread); if (d->accountId.isEmpty() || d->threadId.isEmpty()) { return QVariantMap(); } // include the properties from the last event QVariantMap map = lastEvent().properties(); // and add the thread ones too map[FieldAccountId] = d->accountId; map[FieldThreadId] = d->threadId; map[FieldType] = d->type; map[FieldChatType] = d->chatType; map[FieldParticipants] = d->participants.toVariantList(); map[FieldTimestamp] = d->timestamp; map[FieldCount] = d->count; map[FieldUnreadCount] = d->unreadCount; map[FieldLastEventId] = lastEvent().eventId(); map[FieldLastEventTimestamp] = d->timestamp; map[FieldChatRoomInfo] = d->chatRoomInfo; QList groupedThreads; Q_FOREACH(const Thread &thread, d->groupedThreads) { groupedThreads << thread.properties(); } if (!groupedThreads.isEmpty()) { map[FieldGroupedThreads] = QVariant::fromValue(groupedThreads); } return map; } Thread Thread::fromProperties(const QVariantMap &properties) { Thread thread; if (properties.isEmpty()) { return thread; } // FIXME: save the rest of the data QString accountId = properties[FieldAccountId].toString(); QString threadId = properties[FieldThreadId].toString(); EventType type = (EventType) properties[FieldType].toInt(); ChatType chatType = (ChatType) properties[FieldChatType].toInt(); Participants participants = Participants::fromVariant(properties[FieldParticipants]); QDateTime timestamp = QDateTime::fromString(properties[FieldTimestamp].toString(), Qt::ISODate); int count = properties[FieldCount].toInt(); int unreadCount = properties[FieldUnreadCount].toInt(); Threads groupedThreads; if (properties.contains(FieldGroupedThreads)) { QVariant variant = properties[FieldGroupedThreads]; if (variant.canConvert()) { Q_FOREACH(const QVariant& entry, variant.toList()) { groupedThreads << Thread::fromProperties(entry.toMap()); } } else if (variant.canConvert()) { QDBusArgument argument = variant.value(); argument >> groupedThreads; } } QVariantMap chatRoomInfo = qdbus_cast(properties[FieldChatRoomInfo]); // dbus_cast fails if the map was generated by a qml app, so we demarshal it by hand if (chatRoomInfo.isEmpty()) { chatRoomInfo = properties[FieldChatRoomInfo].toMap(); } Event event; switch (type) { case EventTypeText: event = TextEvent::fromProperties(properties); break; case EventTypeVoice: event = VoiceEvent::fromProperties(properties); break; case EventTypeNull: qWarning("Thread::fromProperties: Got EventTypeNull, using NULL event!"); break; } return Thread(accountId, threadId, type, participants, timestamp, event, count, unreadCount, groupedThreads, chatType, chatRoomInfo); } const QDBusArgument &operator>>(const QDBusArgument &argument, Threads &threads) { argument.beginArray(); while (!argument.atEnd()) { QVariantMap props; QVariant variant; argument >> variant; QDBusArgument innerArgument = variant.value(); if (!innerArgument.atEnd()) { innerArgument >> props; } threads << Thread::fromProperties(props); } argument.endArray(); return argument; } } history-service-0.5/src/thread.h000066400000000000000000000051421455450246700167420ustar00rootroot00000000000000/* * Copyright (C) 2013-2016 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 HISTORY_THREAD_H #define HISTORY_THREAD_H #include #include #include #include #include #include "types.h" #include "event.h" #include "participant.h" namespace History { class ThreadPrivate; class ItemFactory; class Thread; typedef QList Threads; class Thread { Q_DECLARE_PRIVATE(Thread) friend class ItemFactory; public: explicit Thread(); Thread(const QString &accountId, const QString &threadId, EventType type, const Participants &participants, const QDateTime ×tamp = QDateTime(), const Event &lastEvent = Event(), int count = 0, int unreadCount = 0, const Threads &groupedThreads = Threads(), ChatType chatType = ChatTypeNone, const QVariantMap &chatRoomInfo = QVariantMap()); Thread(const Thread &other); virtual ~Thread(); Thread& operator=(const Thread &other); QString accountId() const; QString threadId() const; EventType type() const; Participants participants() const; QDateTime timestamp() const; Event lastEvent() const; int count() const; int unreadCount() const; ChatType chatType() const; Threads groupedThreads() const; QVariantMap chatRoomInfo() const; void addParticipants(const History::Participants &participants); void removeParticipants(const History::Participants &participants); bool isNull() const; bool operator==(const Thread &other) const; bool operator<(const Thread &other) const; virtual QVariantMap properties() const; static Thread fromProperties(const QVariantMap &properties); protected: QSharedPointer d_ptr; }; const QDBusArgument &operator>>(const QDBusArgument &argument, Threads &threads); } #endif // HISTORY_THREAD_H history-service-0.5/src/thread_p.h000066400000000000000000000034521455450246700172630ustar00rootroot00000000000000/* * Copyright (C) 2013-2016 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 HISTORY_THREAD_P_H #define HISTORY_THREAD_P_H #include #include "types.h" namespace History { class Thread; class ThreadPrivate { public: explicit ThreadPrivate(); ThreadPrivate(const QString &theAccountId, const QString &theThreadId, EventType theType, const Participants &theParticipants, const QDateTime &theTimestamp, const Event &theLastEvent, int theCount, int theUnreadCount, const Threads &theGroupedThreads, ChatType chatType, const QVariantMap &chatRoomInfo); virtual ~ThreadPrivate(); QString accountId; QString threadId; EventType type; Participants participants; QDateTime timestamp; Event lastEvent; int count; int unreadCount; Threads groupedThreads; ChatType chatType; QVariantMap chatRoomInfo; }; } #endif // HISTORY_THREAD_P_H history-service-0.5/src/threadview.cpp000066400000000000000000000132321455450246700201670ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 "threadview.h" #include "threadview_p.h" #include "filter.h" #include "manager.h" #include "sort.h" #include "thread.h" #include #include #include namespace History { // ------------- ThreadViewPrivate ------------------------------------------------ ThreadViewPrivate::ThreadViewPrivate(History::EventType theType, const History::Sort &theSort, const History::Filter &theFilter) : type(theType), sort(theSort), filter(theFilter), valid(true), dbus(0) { } Threads ThreadViewPrivate::filteredThreads(const Threads &threads) { bool filterNull = filter.isNull(); Threads filtered; Q_FOREACH(const Thread &thread, threads) { if (thread.type() != type) { continue; } if (filterNull || filter.match(thread.properties())) { filtered << thread; } } return filtered; } void ThreadViewPrivate::_d_threadsAdded(const History::Threads &threads) { Q_Q(ThreadView); Threads filtered = filteredThreads(threads); if (!filtered.isEmpty()) { Q_EMIT q->threadsAdded(filtered); } } void ThreadViewPrivate::_d_threadsModified(const Threads &threads) { Q_Q(ThreadView); Threads filtered = filteredThreads(threads); if (!filtered.isEmpty()) { Q_EMIT q->threadsModified(filtered); } } void ThreadViewPrivate::_d_threadsRemoved(const Threads &threads) { Q_Q(ThreadView); Threads filtered = filteredThreads(threads); if (!filtered.isEmpty()) { Q_EMIT q->threadsRemoved(filtered); } } void ThreadViewPrivate::_d_threadParticipantsChanged(const History::Thread &thread, const History::Participants &added, const History::Participants &removed, const History::Participants &modified) { Q_Q(ThreadView); Threads filtered = filteredThreads(History::Threads() << thread); if (!filtered.isEmpty()) { Q_EMIT q->threadParticipantsChanged(filtered.first(), added, removed, modified); } } // ------------- ThreadView ------------------------------------------------------- ThreadView::ThreadView(History::EventType type, const History::Sort &sort, const Filter &filter, const QVariantMap &properties) : d_ptr(new ThreadViewPrivate(type, sort, filter)) { d_ptr->q_ptr = this; if (!Manager::instance()->isServiceRunning()) { Q_EMIT invalidated(); d_ptr->valid = false; return; } QDBusInterface interface(History::DBusService, History::DBusObjectPath, History::DBusInterface); QDBusReply reply = interface.call("QueryThreads", (int) type, sort.properties(), filter.properties(), properties); if (!reply.isValid()) { Q_EMIT invalidated(); d_ptr->valid = false; return; } d_ptr->objectPath = reply.value(); d_ptr->dbus = new QDBusInterface(History::DBusService, d_ptr->objectPath, History::ThreadViewInterface, QDBusConnection::sessionBus(), this); connect(Manager::instance(), SIGNAL(threadsAdded(History::Threads)), SLOT(_d_threadsAdded(History::Threads))); connect(Manager::instance(), SIGNAL(threadsModified(History::Threads)), SLOT(_d_threadsModified(History::Threads))); connect(Manager::instance(), SIGNAL(threadsRemoved(History::Threads)), SLOT(_d_threadsRemoved(History::Threads))); connect(Manager::instance(), SIGNAL(threadParticipantsChanged(History::Thread, History::Participants, History::Participants, History::Participants)), SLOT(_d_threadParticipantsChanged(History::Thread, History::Participants, History::Participants, History::Participants))); } ThreadView::~ThreadView() { Q_D(ThreadView); if (d->valid) { d->dbus->call("Destroy"); } } Threads ThreadView::nextPage() { Threads threads; Q_D(ThreadView); if (!d->valid) { return threads; } QDBusReply > reply = d->dbus->call("NextPage"); if (!reply.isValid()) { qDebug() << "Error:" << reply.error(); d->valid = false; Q_EMIT invalidated(); return threads; } QList threadsProperties = reply.value(); Q_FOREACH(const QVariantMap &properties, threadsProperties) { Thread thread = Thread::fromProperties(properties); if (!thread.isNull()) { threads << thread; } } return threads; } bool ThreadView::isValid() const { Q_D(const ThreadView); return d->valid; } } #include "moc_threadview.cpp" history-service-0.5/src/threadview.h000066400000000000000000000046231455450246700176400ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 HISTORY_THREADVIEW_H #define HISTORY_THREADVIEW_H #include "types.h" #include "filter.h" #include "sort.h" #include "thread.h" #include namespace History { class ThreadViewPrivate; class ThreadView : public QObject { Q_OBJECT Q_DECLARE_PRIVATE(ThreadView) public: ThreadView(History::EventType type, const History::Sort &sort, const History::Filter &filter, const QVariantMap &properties); ~ThreadView(); Threads nextPage(); bool isValid() const; Q_SIGNALS: void threadsAdded(const History::Threads &threads); void threadsModified(const History::Threads &threads); void threadsRemoved(const History::Threads &threads); void threadParticipantsChanged(const History::Thread &thread, const History::Participants &added, const History::Participants &removed, const History::Participants &modified); void invalidated(); private: Q_PRIVATE_SLOT(d_func(), void _d_threadsAdded(const History::Threads &threads)) Q_PRIVATE_SLOT(d_func(), void _d_threadsModified(const History::Threads &threads)) Q_PRIVATE_SLOT(d_func(), void _d_threadsRemoved(const History::Threads &threads)) Q_PRIVATE_SLOT(d_func(), void _d_threadParticipantsChanged(const History::Thread &thread, const History::Participants &added, const History::Participants &removed, const History::Participants &modified)) QScopedPointer d_ptr; }; } #endif // HISTORY_THREADVIEW_H history-service-0.5/src/threadview_p.h000066400000000000000000000036011455450246700201520ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 THREADVIEW_P_H #define THREADVIEW_P_H #include "types.h" #include namespace History { class ThreadView; class ThreadViewPrivate { Q_DECLARE_PUBLIC(ThreadView) public: ThreadViewPrivate(History::EventType theType, const History::Sort &theSort, const History::Filter &theFilter); EventType type; Sort sort; Filter filter; QString objectPath; bool valid; QDBusInterface *dbus; Threads filteredThreads(const Threads &threads); // private slots void _d_threadsAdded(const History::Threads &threads); void _d_threadsModified(const History::Threads &threads); void _d_threadsRemoved(const History::Threads &threads); void _d_threadParticipantsChanged(const History::Thread &thread, const History::Participants &added, const History::Participants &removed, const History::Participants &modified); ThreadView *q_ptr; }; } #endif // THREADVIEW_P_H history-service-0.5/src/types.h000066400000000000000000000172071455450246700166440ustar00rootroot00000000000000/* * Copyright (C) 2013-2016 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 TYPES_H #define TYPES_H #include #include #define DefineSharedPointer(type) class type; typedef QSharedPointer type##Ptr; typedef QWeakPointer type##WeakPtr; typedef QList type##s; namespace History { DefineSharedPointer(EventView) DefineSharedPointer(Plugin) DefineSharedPointer(ThreadView) // enums enum EventType { EventTypeText, EventTypeVoice, EventTypeNull }; enum FilterType { FilterTypeStandard, FilterTypeIntersection, FilterTypeUnion }; enum MatchFlag { MatchCaseSensitive = 0x01, MatchCaseInsensitive = 0x02, MatchContains = 0x04, MatchPhoneNumber = 0x08, MatchNotEquals = 0x10, MatchLess = 0x20, MatchGreater = 0x40, MatchLessOrEquals = 0x80, MatchGreaterOrEquals = 0x100 }; Q_DECLARE_FLAGS(MatchFlags, MatchFlag) enum MessageStatus { MessageStatusUnknown, MessageStatusDelivered, MessageStatusTemporarilyFailed, MessageStatusPermanentlyFailed, MessageStatusAccepted, MessageStatusRead, MessageStatusDeleted, MessageStatusPending, // pending attachment download MessageStatusDraft, _MessageStatusPadding }; enum MessageType { MessageTypeText = 0, MessageTypeMultiPart = 1, MessageTypeInformation = 2 }; enum InformationType { InformationTypeNone = 0, InformationTypeSimChange = 1, InformationTypeText = 2, InformationTypeSelfJoined = 3, InformationTypeJoined = 4, InformationTypeTitleChanged = 5, InformationTypeInvitationSent = 6, InformationTypeLeaving = 7, InformationTypeSelfLeaving = 8, InformationTypeAdminGranted = 9, InformationTypeAdminRemoved = 10, InformationTypeSelfAdminGranted = 11, InformationTypeSelfAdminRemoved = 12, InformationTypeSelfKicked = 13, InformationTypeGroupGone = 14 }; enum ChatType { ChatTypeNone = 0, ChatTypeContact = 1, ChatTypeRoom = 2, ChatTypeGroup = 3, ChatTypeList = 4, _ChatTypePadding = 5 }; // FIXME (boiko): I think this needs to be changed to a simple enum and not flags, // as the statuses are mutually exclusive enum AttachmentFlag { AttachmentDownloaded = 0x01, AttachmentPending = 0x02, AttachmentError = 0x04 }; Q_DECLARE_FLAGS(AttachmentFlags, AttachmentFlag) enum ParticipantState { ParticipantStateRegular = 0, ParticipantStateLocalPending = 1, ParticipantStateRemotePending = 2 }; enum ParticipantRoles { ParticipantRoleNone = 0, ParticipantRoleMember = 1, ParticipantRoleAdmin = 2 }; // Event writing results enum EventWriteResult { EventWriteCreated, EventWriteModified, EventWriteError, EventWriteNone }; // Since these might not get used in evey file that // includes this file, lets ignore the unused-variable // warning #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunused-variable" // dbus service, object path and interface static const char* DBusService = "com.lomiri.HistoryService"; static const char* DBusObjectPath = "/com/lomiri/HistoryService"; static const char* DBusInterface = "com.lomiri.HistoryService"; static const char* ThreadViewInterface = "com.lomiri.HistoryService.ThreadView"; static const char* EventViewInterface = "com.lomiri.HistoryService.EventView"; // fields static const char* FieldAccountId = "accountId"; static const char* FieldThreadId = "threadId"; static const char* FieldEventId = "eventId"; static const char* FieldType = "type"; static const char* FieldParticipants = "participants"; static const char* FieldParticipantIds = "participantIds"; static const char* FieldCount = "count"; static const char* FieldUnreadCount = "unreadCount"; static const char* FieldSenderId = "senderId"; static const char* FieldTimestamp = "timestamp"; static const char* FieldSentTime = "sentTime"; static const char* FieldDate = "date"; static const char* FieldNewEvent = "newEvent"; static const char* FieldChatType = "chatType"; static const char* FieldChatRoomInfo = "chatRoomInfo"; static const char* FieldChatRoomJoined = "joined"; static const char* FieldChatRoomSelfRoles = "selfRoles"; // Chat Room Info Fields static const char* FieldChatRoomName = "roomName"; static const char* FieldChatRoomServer = "server"; static const char* FieldChatRoomCreator = "creator"; static const char* FieldChatRoomCreationTimestamp = "creationTimestamp"; static const char* FieldChatRoomAnonymous = "anonymous"; static const char* FieldChatRoomInviteOnly = "inviteOnly"; static const char* FieldChatRoomParticipantLimit = "participantLimit"; static const char* FieldChatRoomModerated = "moderated"; static const char* FieldChatRoomTitle = "title"; static const char* FieldChatRoomDescription = "description"; static const char* FieldChatRoomPersistent = "persistent"; static const char* FieldChatRoomPrivate = "private"; static const char* FieldChatRoomPasswordProtected = "passwordProtected"; static const char* FieldChatRoomPassword = "password"; static const char* FieldChatRoomPasswordHint = "passwordHint"; static const char* FieldChatRoomCanUpdateConfiguration = "canUpdateConfiguration"; // thread fields static const char* FieldLastEventId = "lastEventId"; static const char* FieldLastEventTimestamp = "lastEventTimestamp"; static const char* FieldGroupedThreads = "groupedThreads"; // text event fields static const char* FieldMessage = "message"; static const char* FieldMessageType = "messageType"; static const char* FieldMessageStatus = "messageStatus"; static const char* FieldReadTimestamp = "readTimestamp"; static const char* FieldSubject = "subject"; static const char* FieldInformationType = "informationType"; static const char* FieldAttachments = "attachments"; // text attachment fields static const char* FieldAttachmentId = "attachmentId"; static const char* FieldContentType = "contentType"; static const char* FieldFilePath = "filePath"; static const char* FieldStatus = "status"; // voice event fields static const char* FieldMissed = "missed"; static const char* FieldDuration = "duration"; static const char* FieldRemoteParticipant = "remoteParticipant"; // sort stuff static const char* FieldSortField = "sortField"; static const char* FieldSortOrder = "sortOrder"; static const char* FieldCaseSensitivity = "caseSensitivity"; // filter stuff static const char* FieldFilterType = "filterType"; static const char* FieldFilterProperty = "filterProperty"; static const char* FieldFilterValue = "filterValue"; static const char* FieldMatchFlags = "matchFlags"; static const char* FieldFilters = "filters"; static const char* FieldGroupingProperty = "groupingProperty"; // contact matching stuff static const char* FieldContactId = "contactId"; static const char* FieldAlias = "alias"; static const char* FieldAvatar = "avatar"; static const char* FieldIdentifier = "identifier"; static const char* FieldDetailProperties = "detailProperties"; static const char* FieldParticipantState = "state"; static const char* FieldParticipantRoles = "roles"; #pragma GCC diagnostic pop } #endif // TYPES_H history-service-0.5/src/unionfilter.cpp000066400000000000000000000102321455450246700203600ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 "unionfilter.h" #include "unionfilter_p.h" #include #include #include namespace History { UnionFilterPrivate::UnionFilterPrivate() { } UnionFilterPrivate::~UnionFilterPrivate() { } bool UnionFilterPrivate::match(const QVariantMap properties) const { // if the filter list is empty, assume it matches if (filters.isEmpty()) { return true; } // return true if any of the filters match Q_FOREACH(const History::Filter &filter, filters) { if (filter.match(properties)) { return true; } } // if we reach this point it means none of the filters matched the properties return false; } bool UnionFilterPrivate::isValid() const { // FIXME: maybe we should check if at least one of the inner filters are valid? return !filters.isEmpty(); } QVariantMap UnionFilterPrivate::properties() const { QVariantMap map; if (!isValid()) { return map; } QVariantList filterList; Q_FOREACH(const Filter &filter, filters) { filterList << filter.properties(); } map[FieldFilters] = filterList; map[FieldFilterType] = (int) History::FilterTypeUnion; return map; } QString UnionFilterPrivate::toString(const QString &propertyPrefix) const { if (filters.isEmpty()) { return QString(); } else if (filters.count() == 1) { return filters.first().toString(); } QStringList output; // wrap each filter string around parenthesis Q_FOREACH(const Filter &filter, filters) { QString value = filter.toString(propertyPrefix); if (!value.isEmpty()) { output << QString("(%1)").arg(value); } } return QString("(%1)").arg(output.join(" OR ")); } HISTORY_FILTER_DEFINE_COPY(UnionFilter, FilterTypeUnion) UnionFilter::UnionFilter() : Filter(*new UnionFilterPrivate()) { } UnionFilter::~UnionFilter() { } void UnionFilter::setFilters(const Filters &filters) { Q_D(UnionFilter); d->filters = filters; } void UnionFilter::prepend(const Filter &filter) { Q_D(UnionFilter); d->filters.prepend(filter); } void UnionFilter::append(const Filter &filter) { Q_D(UnionFilter); d->filters.append(filter); } void UnionFilter::clear() { Q_D(UnionFilter); d->filters.clear(); } Filters UnionFilter::filters() const { Q_D(const UnionFilter); return d->filters; } Filter UnionFilter::fromProperties(const QVariantMap &properties) { UnionFilter filter; if (properties.isEmpty()) { return filter; } QVariant filters = properties[FieldFilters]; QVariantList filterList; // when the filter travels through DBus, it arrives marshalled into QDBusArguments. // cover that case too. if (filters.canConvert()) { QDBusArgument argument = filters.value(); QVariantList list; argument >> list; // and cast also the inner filters Q_FOREACH(const QVariant &var, list) { QDBusArgument arg = var.value(); QVariantMap map; arg >> map; filterList.append(map); } } else { filterList = filters.toList(); } Q_FOREACH(const QVariant &props, filterList) { Filter innerFilter = History::Filter::fromProperties(props.toMap()); if (innerFilter.isValid()) { filter.append(innerFilter); } } return filter; } } history-service-0.5/src/unionfilter.h000066400000000000000000000025711455450246700200340ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 HISTORY_UNIONFILTER_H #define HISTORY_UNIONFILTER_H #include "filter.h" #include "types.h" namespace History { class UnionFilterPrivate; // OR filter class UnionFilter : public Filter { Q_DECLARE_PRIVATE(UnionFilter) public: UnionFilter(); ~UnionFilter(); // copy related members UnionFilter(const Filter &other); UnionFilter& operator=(const Filter &other); void setFilters(const Filters &filters); void prepend(const Filter &filter); void append(const Filter &filter); void clear(); Filters filters() const; static Filter fromProperties(const QVariantMap &properties); }; } #endif history-service-0.5/src/unionfilter_p.h000066400000000000000000000025211455450246700203460ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 HISTORY_UNIONFILTER_P_H #define HISTORY_UNIONFILTER_P_H #include "filter_p.h" namespace History { class UnionFilter; class UnionFilterPrivate : public FilterPrivate { public: UnionFilterPrivate(); ~UnionFilterPrivate(); Filters filters; virtual FilterType type() const { return FilterTypeUnion; } QString toString(const QString &propertyPrefix = QString()) const; bool match(const QVariantMap properties) const; bool isValid() const; virtual QVariantMap properties() const; HISTORY_FILTER_DECLARE_CLONE(UnionFilter) }; } #endif // HISTORY_UNIONFILTER_P_H history-service-0.5/src/utils.cpp000066400000000000000000000141611455450246700171670ustar00rootroot00000000000000/* * Copyright (C) 2015-2016 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 #include "utils_p.h" #include "phoneutils_p.h" #include #include #include #include #include #include namespace History { Utils::Utils() { } // FIXME: find a better way to determine when accounts should be grouped bool Utils::shouldGroupThread(const Thread &thread) { if (protocolFromAccountId(thread.accountId()) == "multimedia") { return thread.chatType() != History::ChatTypeRoom; } return (matchFlagsForAccount(thread.accountId()) & MatchPhoneNumber); } MatchFlags Utils::matchFlagsForAccount(const QString &accountId) { static QMap protocolFlags; if (protocolFlags.isEmpty()) { protocolFlags["ofono"] = MatchPhoneNumber; protocolFlags["multimedia"] = MatchPhoneNumber; protocolFlags["sip"] = MatchPhoneNumber; } QString protocol = protocolFromAccountId(accountId); if (protocolFlags.contains(protocol)) { return protocolFlags[protocol]; } // default to phone number matching for now return History::MatchCaseSensitive; } QString Utils::protocolFromAccountId(const QString &accountId) { QStringList parsedId = accountId.split("/"); if (parsedId.count() < 3) { return QString(); } return parsedId[1]; } bool Utils::compareIds(const QString &accountId, const QString &id1, const QString &id2) { MatchFlags matchFlags = matchFlagsForAccount(accountId); if (matchFlags & MatchPhoneNumber) { return PhoneUtils::comparePhoneNumbers(id1, id2); } if (matchFlags & MatchCaseInsensitive) { return id1.toLower() == id2.toLower(); } return id1 == id2; } bool Utils::compareParticipants(const QStringList &participants1, const QStringList &participants2, MatchFlags flags) { // if list size is different, just return if (participants1.count() != participants2.count()) { return false; } if (flags & MatchPhoneNumber) { QStringList normalizedParticipants1; QStringList normalizedParticipants2; Q_FOREACH(const QString &participant, participants1) { normalizedParticipants1 << PhoneUtils::normalizePhoneNumber(participant); } Q_FOREACH(const QString &participant, participants2) { normalizedParticipants2 << PhoneUtils::normalizePhoneNumber(participant); } return compareNormalizedParticipants(normalizedParticipants1, normalizedParticipants2, flags); } return compareNormalizedParticipants(participants1, participants2, flags); } bool Utils::compareNormalizedParticipants(const QStringList &participants1, const QStringList &participants2, MatchFlags flags) { QStringList mutableParticipants2 = participants2; // if list size is different, just return if (participants1.count() != participants2.count()) { return false; } // and now compare the lists bool found = true; Q_FOREACH(const QString &participant, participants1) { if (flags & MatchPhoneNumber) { // we need to iterate the list and call the phone number comparing function for // each participant from the given thread bool inList = false; QStringList::iterator it = mutableParticipants2.begin(); while (it != mutableParticipants2.end()) { if (PhoneUtils::compareNormalizedPhoneNumbers(*it, participant)) { inList = true; mutableParticipants2.erase(it); break; } ++it; } if (!inList) { found = false; break; } } else if (!mutableParticipants2.contains(participant)) { found = false; break; } } return found; } QString Utils::normalizeId(const QString &accountId, const QString &id) { QString normalizedId = id; // for now we only normalize phone number IDs if (matchFlagsForAccount(accountId) & History::MatchPhoneNumber) { normalizedId = PhoneUtils::normalizePhoneNumber(id); } if (normalizedId.isEmpty()) { qWarning() << "Normalized phone number is empty:" << accountId << id; normalizedId = id; } return normalizedId; } QVariant Utils::getUserValue(const QString &interface, const QString &propName) { QString uid = QString::number(getuid()); QString activeUser = "/org/freedesktop/Accounts/User" + uid; QDBusInterface iface("org.freedesktop.Accounts", activeUser, "org.freedesktop.DBus.Properties", QDBusConnection::systemBus()); QDBusReply reply = iface.call("Get", interface, propName); if (reply.isValid()) { return reply.value(); } else { qWarning() << "Failed to get user property " << propName << " from AccountsService:" << reply.error().message(); } return QVariant(); } bool Utils::shouldIncludeParticipants(const Thread &thread) { return shouldIncludeParticipants(thread.accountId(), thread.chatType()); } bool Utils::shouldIncludeParticipants(const QString &accountId, const ChatType &type) { // FIXME: this is obviously incorrect. we have to query the protocol files as a final solution if (protocolFromAccountId(accountId) == "irc") { return type != ChatTypeRoom; } return true; } } history-service-0.5/src/utils_p.h000066400000000000000000000034111455450246700171470ustar00rootroot00000000000000/* * Copyright (C) 2015-2016 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 UTILS_P_H #define UTILS_P_H #include "types.h" #include "thread.h" namespace History { class Utils { public: static MatchFlags matchFlagsForAccount(const QString &accountId); static QString protocolFromAccountId(const QString &accountId); static bool compareIds(const QString &accountId, const QString &id1, const QString & id2); static bool compareParticipants(const QStringList &participants1, const QStringList &participants2, MatchFlags flags); static bool compareNormalizedParticipants(const QStringList &participants1, const QStringList &participants2, MatchFlags flags); static bool shouldGroupThread(const Thread &thread); static bool shouldIncludeParticipants(const Thread &thread); static bool shouldIncludeParticipants(const QString &accountId, const History::ChatType &type); static QString normalizeId(const QString &accountId, const QString &id); static QVariant getUserValue(const QString &interface, const QString &propName); private: Utils(); }; } #endif // UTILS_P_H history-service-0.5/src/voiceevent.cpp000066400000000000000000000103711455450246700201750ustar00rootroot00000000000000/* * Copyright (C) 2013-2015 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 "voiceevent.h" #include "voiceevent_p.h" namespace History { // ------------- VoiceEventPrivate ------------------------------------------------ VoiceEventPrivate::VoiceEventPrivate() { } VoiceEventPrivate::VoiceEventPrivate(const QString &theAccountId, const QString &theThreadId, const QString &theEventId, const QString &theSender, const QDateTime &theTimestamp, bool theNewEvent, bool theMissed, const QTime &theDuration, const QString &theRemoteParticipant, const Participants &theParticipants) : EventPrivate(theAccountId, theThreadId, theEventId, theSender, theTimestamp, theNewEvent, theParticipants), missed(theMissed), duration(theDuration), remoteParticipant(theRemoteParticipant) { } VoiceEventPrivate::~VoiceEventPrivate() { } EventType VoiceEventPrivate::type() const { return EventTypeVoice; } QVariantMap VoiceEventPrivate::properties() const { QVariantMap map = EventPrivate::properties(); map[FieldMissed] = missed; map[FieldDuration] = QTime(0,0,0,0).secsTo(duration); map[FieldRemoteParticipant] = remoteParticipant; return map; } // ------------- VoiceEvent ------------------------------------------------------- HISTORY_EVENT_DEFINE_COPY(VoiceEvent, EventTypeVoice) VoiceEvent::VoiceEvent() : Event(*new VoiceEventPrivate()) { } VoiceEvent::VoiceEvent(const QString &accountId, const QString &threadId, const QString &eventId, const QString &sender, const QDateTime ×tamp, bool newEvent, bool missed, const QTime &duration, const QString &remoteParticipant, const Participants &participants) : Event(*new VoiceEventPrivate(accountId, threadId, eventId, sender, timestamp, newEvent, missed, duration, remoteParticipant, participants)) { } VoiceEvent::~VoiceEvent() { } bool VoiceEvent::missed() const { Q_D(const VoiceEvent); return d->missed; } QTime VoiceEvent::duration() const { Q_D(const VoiceEvent); return d->duration; } QString VoiceEvent::remoteParticipant() const { Q_D(const VoiceEvent); return d->remoteParticipant; } Event VoiceEvent::fromProperties(const QVariantMap &properties) { Event event; if (properties.isEmpty()) { return event; } QString accountId = properties[FieldAccountId].toString(); QString threadId = properties[FieldThreadId].toString(); QString eventId = properties[FieldEventId].toString(); QString senderId = properties[FieldSenderId].toString(); QDateTime timestamp = QDateTime::fromString(properties[FieldTimestamp].toString(), Qt::ISODate); bool newEvent = properties[FieldNewEvent].toBool(); Participants participants = Participants::fromVariant(properties[FieldParticipants]); bool missed = properties[FieldMissed].toBool(); QTime duration = QTime(0,0,0).addSecs(properties[FieldDuration].toInt()); QString remoteParticipant = properties[FieldRemoteParticipant].toString(); event = VoiceEvent(accountId, threadId, eventId, senderId, timestamp, newEvent, missed, duration, remoteParticipant, participants); return event; } } history-service-0.5/src/voiceevent.h000066400000000000000000000035641455450246700176500ustar00rootroot00000000000000/* * Copyright (C) 2013-2015 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 HISTORY_VOICEEVENT_H #define HISTORY_VOICEEVENT_H #include "event.h" namespace History { class VoiceEventPrivate; class ItemFactory; class VoiceEvent : public Event { Q_DECLARE_PRIVATE(VoiceEvent) friend class ItemFactory; public: explicit VoiceEvent(); VoiceEvent(const QString &accountId, const QString &threadId, const QString &eventId, const QString &sender, const QDateTime ×tamp, bool newEvent, bool missed, const QTime &duration = QTime(), const QString &remoteParticipant = QString(), const Participants &participants = Participants()); ~VoiceEvent(); // copy related members VoiceEvent(const Event &other); VoiceEvent& operator=(const Event &other); bool missed() const; QTime duration() const; /*** * Returns the ID of the remote participant. * On incoming calls that's the same as the sender() property */ QString remoteParticipant() const; static Event fromProperties(const QVariantMap &properties); }; } #endif history-service-0.5/src/voiceevent_p.h000066400000000000000000000032031455450246700201550ustar00rootroot00000000000000/* * Copyright (C) 2013-2015 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 HISTORY_VOICEEVENT_P_H #define HISTORY_VOICEEVENT_P_H #include "event_p.h" namespace History { class VoiceEventPrivate : public EventPrivate { public: VoiceEventPrivate(); VoiceEventPrivate(const QString &theAccountId, const QString &theThreadId, const QString &theEventId, const QString &theSender, const QDateTime &theTimestamp, bool theNewEvent, bool theMissed, const QTime &theDuration, const QString &theRemoteParticipant, const Participants &theParticipants); ~VoiceEventPrivate(); bool missed; QTime duration; QString remoteParticipant; EventType type() const; QVariantMap properties() const; HISTORY_EVENT_DECLARE_CLONE(VoiceEvent) }; } #endif // HISTORY_VOICEEVENT_P_H history-service-0.5/tests/000077500000000000000000000000001455450246700156735ustar00rootroot00000000000000history-service-0.5/tests/CMakeLists.txt000066400000000000000000000002511455450246700204310ustar00rootroot00000000000000include (GenerateTest) add_subdirectory(common) add_subdirectory(daemon) add_subdirectory(libhistoryservice) add_subdirectory(plugins) add_subdirectory(Lomiri.History) history-service-0.5/tests/Lomiri.History/000077500000000000000000000000001455450246700205665ustar00rootroot00000000000000history-service-0.5/tests/Lomiri.History/CMakeLists.txt000066400000000000000000000052711455450246700233330ustar00rootroot00000000000000include_directories( ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_SOURCE_DIR}/Lomiri/History ${CMAKE_SOURCE_DIR}/src ${CMAKE_SOURCE_DIR}/tests/common ${TP_QT5_INCLUDE_DIRS} ) set(SOURCE_DIR ${CMAKE_SOURCE_DIR}/Lomiri/History) set(HistoryQml_SOURCES ${SOURCE_DIR}/historyeventmodel.cpp ${SOURCE_DIR}/historyeventmodel.h ${SOURCE_DIR}/historymanager.h ${SOURCE_DIR}/historymanager.cpp ${SOURCE_DIR}/historygroupedeventsmodel.cpp ${SOURCE_DIR}/historygroupedeventsmodel.h ${SOURCE_DIR}/historygroupedthreadsmodel.cpp ${SOURCE_DIR}/historygroupedthreadsmodel.h ${SOURCE_DIR}/historymodel.cpp ${SOURCE_DIR}/historymodel.h ${SOURCE_DIR}/historyqmlfilter.cpp ${SOURCE_DIR}/historyqmlfilter.h ${SOURCE_DIR}/historyqmlintersectionfilter.cpp ${SOURCE_DIR}/historyqmlintersectionfilter.h ${SOURCE_DIR}/historyqmlplugin.cpp ${SOURCE_DIR}/historyqmlplugin.h ${SOURCE_DIR}/historyqmlsort.cpp ${SOURCE_DIR}/historyqmlsort.h ${SOURCE_DIR}/historyqmltexteventattachment.cpp ${SOURCE_DIR}/historyqmltexteventattachment.h ${SOURCE_DIR}/historyqmlunionfilter.cpp ${SOURCE_DIR}/historyqmlunionfilter.h ${SOURCE_DIR}/historythreadmodel.cpp ${SOURCE_DIR}/historythreadmodel.h ) set(HistoryGroupedThreadsModelTest_SOURCES ${HistoryQml_SOURCES} HistoryGroupedThreadsModelTest.cpp ) generate_test(HistoryGroupedThreadsModelTest SOURCES ${HistoryGroupedThreadsModelTest_SOURCES} LIBRARIES historyservice QT5_MODULES Core Qml Test USE_DBUS USE_XVFB TASKS --task ${CMAKE_BINARY_DIR}/daemon/history-daemon --ignore-return --task-name history-daemon WAIT_FOR com.lomiri.HistoryService) set(HistoryManagerTest_SOURCES ${HistoryQml_SOURCES} HistoryManagerTest.cpp ) generate_test(HistoryManagerTest SOURCES ${HistoryManagerTest_SOURCES} LIBRARIES historyservice QT5_MODULES Core Qml Test USE_DBUS USE_XVFB TASKS --task ${CMAKE_BINARY_DIR}/daemon/history-daemon --ignore-return --task-name history-daemon WAIT_FOR com.lomiri.HistoryService) set(HistoryEventModelTest_SOURCES ${HistoryQml_SOURCES} HistoryEventModelTest.cpp ) generate_telepathy_test(HistoryEventModelTest SOURCES ${HistoryEventModelTest_SOURCES} LIBRARIES ${TP_QT5_LIBRARIES} mockcontroller telepathytest historyservice USE_XVFB TASKS --task ${CMAKE_BINARY_DIR}/daemon/history-daemon --ignore-return --task-name history-daemon WAIT_FOR com.lomiri.HistoryService) history-service-0.5/tests/Lomiri.History/HistoryEventModelTest.cpp000066400000000000000000000067151455450246700255670ustar00rootroot00000000000000/* * Copyright (C) 2016-2017 Canonical, Ltd. * * This file is part of history-service. * * telephony-service 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; version 3. * * telephony-service 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 #include "telepathytest.h" #include "manager.h" #include "textevent.h" #include "historyeventmodel.h" class HistoryEventModelTest : public TelepathyTest { Q_OBJECT private Q_SLOTS: void initTestCase(); void testTelepathyInitializedCorrectly(); private: History::Manager *mManager; }; void HistoryEventModelTest::initTestCase() { initialize(0); mManager = History::Manager::instance(); } void HistoryEventModelTest::testTelepathyInitializedCorrectly() { Tp::AccountPtr account = addAccount("mock", "ofono", "My Account"); QVERIFY(!account.isNull()); // we don't actually test anything on the model other than it having the event // but if telepathy is not properly initialized, this will crash. HistoryEventModel model; QString participant("textParticipant"); // create a temporary thread to populate the model History::Thread textThread = mManager->threadForParticipants(account->uniqueIdentifier(), History::EventTypeText, QStringList() << participant, History::MatchCaseSensitive, true); History::TextEvent event(textThread.accountId(), textThread.threadId(), "theEventId", participant, QDateTime::currentDateTime(), QDateTime::currentDateTime().addSecs(-10), true, "Hi there", History::MessageTypeText, History::MessageStatusRead, QDateTime::currentDateTime(), "The subject", History::InformationTypeNone, History::TextEventAttachments(), textThread.participants()); QVERIFY(mManager->writeEvents(History::Events() << event)); HistoryQmlFilter *filter = new HistoryQmlFilter(this); filter->setFilterProperty(History::FieldThreadId); filter->setFilterValue(textThread.threadId()); model.setFilter(filter); HistoryQmlSort *sort = new HistoryQmlSort(this); sort->setSortOrder(HistoryQmlSort::DescendingOrder); sort->setSortField("timestamp"); model.setSort(sort); QTRY_COMPARE(model.rowCount(), 1); // this will trigger the crash if tp-qt is not properly initialized qDebug() << model.index(0).data(HistoryEventModel::SenderRole); mManager->removeThreads(History::Threads() << textThread); QTRY_COMPARE(model.rowCount(), 0); } QTEST_MAIN(HistoryEventModelTest) #include "HistoryEventModelTest.moc" history-service-0.5/tests/Lomiri.History/HistoryGroupedThreadsModelTest.cpp000066400000000000000000000153771455450246700274320ustar00rootroot00000000000000/* * Copyright (C) 2015 Canonical, Ltd. * * This file is part of history-service. * * telephony-service 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; version 3. * * telephony-service 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 #include #include "manager.h" #include "historygroupedthreadsmodel.h" class HistoryGroupedThreadsModelTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void testCanFetchMore(); void testThreadsUpdated(); private: History::Manager *mManager; }; void HistoryGroupedThreadsModelTest::initTestCase() { mManager = History::Manager::instance(); } void HistoryGroupedThreadsModelTest::testCanFetchMore() { HistoryGroupedThreadsModel model; QSignalSpy fetchMoreChanged(&model, SIGNAL(canFetchMoreChanged())); // create a temporary thread to populate the model History::Thread textThread = mManager->threadForParticipants("accountId0", History::EventTypeText, QStringList() << QString("textParticipant"), History::MatchCaseSensitive, true); // must return false if there is no filter set QVERIFY(!model.canFetchMore()); HistoryQmlFilter *filter = new HistoryQmlFilter(this); model.setFilter(filter); model.setGroupingProperty(History::FieldParticipants); HistoryQmlSort *sort = new HistoryQmlSort(this); sort->setSortOrder(HistoryQmlSort::DescendingOrder); sort->setSortField("lastEventTimestamp"); model.setSort(sort); // force updateQuery() to be called model.componentComplete(); QVERIFY(model.canFetchMore()); model.fetchMore(); QTRY_VERIFY(fetchMoreChanged.count() >= 1); QVERIFY(!model.canFetchMore()); QTRY_COMPARE(model.rowCount(), 1); mManager->removeThreads(History::Threads() << textThread); QTRY_COMPARE(model.rowCount(), 0); } void HistoryGroupedThreadsModelTest::testThreadsUpdated() { HistoryGroupedThreadsModel model; QSignalSpy dataChanged(&model, SIGNAL(dataChanged(QModelIndex, QModelIndex))); QSignalSpy rowsRemoved(&model, SIGNAL(rowsRemoved(QModelIndex, int, int))); HistoryQmlFilter *filter = new HistoryQmlFilter(this); model.setFilter(filter); model.setGroupingProperty(History::FieldParticipants); HistoryQmlSort *sort = new HistoryQmlSort(this); sort->setSortOrder(HistoryQmlSort::DescendingOrder); sort->setSortField("lastEventTimestamp"); model.setSort(sort); // force updateQuery() to be called model.componentComplete(); // create first thread History::Thread textThread = mManager->threadForParticipants("ofono/ofono/account0", History::EventTypeText, QStringList() << QString("1234567"), History::MatchPhoneNumber, true); // insert one event History::TextEvent firstEvent = History::TextEvent(textThread.accountId(), textThread.threadId(), QString("eventId1%1").arg(QString::number(qrand() % 1024)), QString("1234567"), QDateTime::currentDateTime(), QDateTime::currentDateTime().addSecs(-10), false, "Random Message", History::MessageTypeText); mManager->writeEvents(History::Events() << firstEvent); QTRY_COMPARE(dataChanged.count(), 1); QModelIndex firstIndex = dataChanged.first().first().value(); QString lastEventMessage = model.data(firstIndex, HistoryThreadModel::LastEventTextMessageRole).toString(); QCOMPARE(QString("Random Message"), lastEventMessage); dataChanged.clear(); // create another thread to be grouped, but using another kind of account textThread = mManager->threadForParticipants("multimedia/multimedia/account1", History::EventTypeText, QStringList() << QString("1234567"), History::MatchPhoneNumber, true); QTRY_COMPARE(dataChanged.count(), 1); QModelIndex index = dataChanged.first().first().value(); QCOMPARE(firstIndex, index); dataChanged.clear(); // insert another event in second thread History::TextEvent secondEvent = History::TextEvent(textThread.accountId(), textThread.threadId(), QString("eventId2%1").arg(QString::number(qrand() % 1024)), QString("1234567"), QDateTime::currentDateTime().addSecs(1), QDateTime::currentDateTime().addSecs(-10), false, "Random Message2", History::MessageTypeText); mManager->writeEvents(History::Events() << secondEvent); QTRY_COMPARE(dataChanged.count(), 1); // make sure the index is the same, meaning both threads are grouped index = dataChanged.first().first().value(); QCOMPARE(firstIndex, index); // check if latest message is from the second event lastEventMessage = model.data(index, HistoryThreadModel::LastEventTextMessageRole).toString(); // check if count is correct given that we have two threads grouped with one message in each QCOMPARE(model.data(index, HistoryThreadModel::CountRole).toInt(), 2); QCOMPARE(QString("Random Message2"), lastEventMessage); dataChanged.clear(); // delete latest event and make sure the text displayed is from the first thread again mManager->removeEvents(History::Events() << secondEvent); QTRY_COMPARE(dataChanged.count(), 1); index = dataChanged.first().first().value(); QCOMPARE(firstIndex, index); lastEventMessage = model.data(index, HistoryThreadModel::LastEventTextMessageRole).toString(); QCOMPARE(QString("Random Message"), lastEventMessage); // check if count is correct given that we have only one thread now QCOMPARE(model.data(index, HistoryThreadModel::CountRole).toInt(), 1); // delete first event and make sure the model is cleared mManager->removeEvents(History::Events() << firstEvent); QTRY_COMPARE(rowsRemoved.count(), 1); QTRY_COMPARE(model.rowCount(), 0); } QTEST_MAIN(HistoryGroupedThreadsModelTest) #include "HistoryGroupedThreadsModelTest.moc" history-service-0.5/tests/Lomiri.History/HistoryManagerTest.cpp000066400000000000000000000103711455450246700250700ustar00rootroot00000000000000/* * Copyright (C) 2021 UBports Foundation * * Authors: * Lionel Duboeuf * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 #include #include #include "manager.h" #include "historymanager.h" #include "voiceevent.h" class HistoryManagerTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void testShouldNotTriggerOperation(); void testRemoveAll(); private: History::Manager *mManager; }; void HistoryManagerTest::initTestCase() { mManager = History::Manager::instance(); } void HistoryManagerTest::testShouldNotTriggerOperation() { HistoryManager historyManager; QJSEngine myEngine; myEngine.globalObject().setProperty("error",""); myEngine.globalObject().setProperty("deletedEventCount",""); QJSValue fun = myEngine.evaluate("(function( _deletedEventCount, _error) {" " error = _error; " " deletedEventCount = _deletedEventCount;" "})"); // no type historyManager.removeEvents(History::EventTypeNull, QDateTime::currentDateTime().toString("yyyy-MM-ddTHH:mm:ss.zzz"),fun); QCOMPARE(myEngine.globalObject().property("error").toInt(),HistoryManager::OPERATION_INVALID); // bad filter historyManager.removeEvents(HistoryModel::EventTypeVoice, "", fun); QCOMPARE(myEngine.globalObject().property("error").toInt(),HistoryManager::OPERATION_INVALID); //start remove no datas historyManager.removeEvents(HistoryModel::EventTypeVoice, QDateTime::currentDateTime().toString("yyyy-MM-ddTHH:mm:ss.zzz"),fun); QTest::qWait(100); QCOMPARE(myEngine.globalObject().property("error").toInt(),HistoryManager::NO_ERROR); QCOMPARE(myEngine.globalObject().property("deletedEventCount").toInt(),0); } void HistoryManagerTest::testRemoveAll() { HistoryManager historyManager; QJSEngine myEngine; myEngine.globalObject().setProperty("error",""); myEngine.globalObject().setProperty("deletedEventCount",-1); QJSValue fun = myEngine.evaluate("(function( _deletedEventCount, _error) {" " error = _error; " " deletedEventCount = _deletedEventCount;" "})"); QString voiceParticipant("voiceParticipant"); // create a temporary thread to populate the model History::Thread voiceThread = mManager->threadForParticipants("voiceAccountId", History::EventTypeVoice, QStringList()<< voiceParticipant, History::MatchCaseSensitive, true); History::VoiceEvent voiceEvent(voiceThread.accountId(), voiceThread.threadId(), QString("eventId1"), voiceParticipant, QDateTime::currentDateTime(), false, false, QTime(1,2,3)); QVERIFY(mManager->writeEvents(History::Events() << voiceEvent)); historyManager.removeEvents(HistoryModel::EventTypeVoice, QDateTime::currentDateTime().addDays(1).toString("yyyy-MM-ddTHH:mm:ss.zzz"), fun); QTest::qWait(100); QCOMPARE(myEngine.globalObject().property("error").toInt(),HistoryManager::NO_ERROR); QCOMPARE(myEngine.globalObject().property("deletedEventCount").toInt(),1); } QTEST_MAIN(HistoryManagerTest) #include "HistoryManagerTest.moc" history-service-0.5/tests/common/000077500000000000000000000000001455450246700171635ustar00rootroot00000000000000history-service-0.5/tests/common/CMakeLists.txt000066400000000000000000000007751455450246700217340ustar00rootroot00000000000000include_directories(${TP_QT5_INCLUDE_DIRS} ${CMAKE_SOURCE_DIR}/src) configure_file(dbus-session.conf.in ${CMAKE_CURRENT_BINARY_DIR}/dbus-session.conf) add_library(mockcontroller STATIC mockcontroller.cpp mockcontroller.h) qt5_use_modules(mockcontroller Core DBus) add_library(telepathytest STATIC telepathytest.cpp telepathytest.h) qt5_use_modules(telepathytest Core DBus) target_link_libraries(telepathytest ${TP_QT5_LIBRARIES}) add_subdirectory(mock) add_subdirectory(dbus-services) history-service-0.5/tests/common/dbus-services/000077500000000000000000000000001455450246700217415ustar00rootroot00000000000000history-service-0.5/tests/common/dbus-services/CMakeLists.txt000066400000000000000000000007411455450246700245030ustar00rootroot00000000000000# copy the services we want to use set(DBUS_SERVICES_DIR ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATAROOTDIR}/dbus-1/services) file(COPY ${DBUS_SERVICES_DIR}/org.freedesktop.Telepathy.MissionControl5.service DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) file(COPY ${DBUS_SERVICES_DIR}/org.freedesktop.Telepathy.AccountManager.service DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) file(COPY ${DBUS_SERVICES_DIR}/ca.desrt.dconf.service DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) history-service-0.5/tests/common/dbus-session.conf.in000066400000000000000000000032161455450246700230570ustar00rootroot00000000000000 session unix:tmpdir=/tmp @CMAKE_CURRENT_BINARY_DIR@/dbus-services 60000 1000000000 1000000000 1000000000 120000 240000 100000 10000 100000 10000 50000 50000 50000 300000 history-service-0.5/tests/common/dbustypes.h000066400000000000000000000022751455450246700213640ustar00rootroot00000000000000/** * Copyright (C) 2013 Canonical, Ltd. * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU General Public License version 3, as published by * the Free Software Foundation. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, * SATISFACTORY QUALITY, 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 . * * Authors: Tiago Salem Herrmann */ #ifndef DBUSTYPES #define DBUSTYPES struct MessageStruct { QDBusObjectPath path; QVariantMap properties; }; struct AttachmentStruct { QString id; QString contentType; QString filePath; quint64 offset; quint64 length; }; typedef QList AttachmentList; Q_DECLARE_METATYPE(AttachmentStruct) Q_DECLARE_METATYPE(AttachmentList) typedef QList MessageList; Q_DECLARE_METATYPE(MessageStruct) Q_DECLARE_METATYPE(MessageList) #endif history-service-0.5/tests/common/mock/000077500000000000000000000000001455450246700201145ustar00rootroot00000000000000history-service-0.5/tests/common/mock/CMakeLists.txt000066400000000000000000000015171455450246700226600ustar00rootroot00000000000000include_directories( ${TP_QT5_INCLUDE_DIRS} ${Qt5Core_INCLUDE_DIRS} ${Qt5DBus_INCLUDE_DIRS} ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_SOURCE_DIR}/src ) find_library(TELEPATHY_QT5_SERVICE_LIBRARIES telepathy-qt5-service) set(mock_SRCS main.cpp protocol.cpp connection.cpp textchannel.cpp callchannel.cpp conferencecallchannel.cpp mockconnectiondbus.cpp speakeriface.cpp emergencymodeiface.cpp ussdiface.cpp voicemailiface.cpp) qt5_add_dbus_adaptor(mock_SRCS MockConnection.xml mockconnectiondbus.h MockConnectionDBus) add_executable(telepathy-mock ${mock_SRCS}) qt5_use_modules(telepathy-mock Core DBus) target_link_libraries(telepathy-mock ${TP_QT5_LIBRARIES} ${TELEPATHY_QT5_SERVICE_LIBRARIES} ${OFONO_QT_LIBRARIES} ${PULSEAUDIO_LIBRARIES} ${Qt5Network_LIBRARIES}) history-service-0.5/tests/common/mock/MockConnection.xml000066400000000000000000000133411455450246700235510ustar00rootroot00000000000000 An interface to the fake connection manager history-service-0.5/tests/common/mock/callchannel.cpp000066400000000000000000000204011455450246700230610ustar00rootroot00000000000000/** * Copyright (C) 2013 Canonical, Ltd. * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU General Public License version 3, as published by * the Free Software Foundation. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, * SATISFACTORY QUALITY, 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 . * * Authors: Tiago Salem Herrmann * Gustavo Pichorim Boiko */ #include "callchannel.h" MockCallChannel::MockCallChannel(MockConnection *conn, QString phoneNumber, QString state, uint targetHandle, QObject *parent): QObject(parent), mIncoming(false), mRequestedHangup(false), mConnection(conn), mPhoneNumber(phoneNumber), mTargetHandle(targetHandle), mState(state) { Tp::BaseChannelPtr baseChannel = Tp::BaseChannel::create(mConnection, TP_QT_IFACE_CHANNEL_TYPE_CALL, Tp::HandleTypeContact, targetHandle); Tp::BaseChannelCallTypePtr callType = Tp::BaseChannelCallType::create(baseChannel.data(), true, Tp::StreamTransportTypeUnknown, true, false, "",""); baseChannel->plugInterface(Tp::AbstractChannelInterfacePtr::dynamicCast(callType)); mHoldIface = Tp::BaseChannelHoldInterface::create(); mHoldIface->setSetHoldStateCallback(Tp::memFun(this,&MockCallChannel::onHoldStateChanged)); mMuteIface = Tp::BaseCallMuteInterface::create(); mMuteIface->setSetMuteStateCallback(Tp::memFun(this,&MockCallChannel::onMuteStateChanged)); mSplittableIface = Tp::BaseChannelSplittableInterface::create(); mSplittableIface->setSplitCallback(Tp::memFun(this,&MockCallChannel::onSplit)); baseChannel->plugInterface(Tp::AbstractChannelInterfacePtr::dynamicCast(mHoldIface)); baseChannel->plugInterface(Tp::AbstractChannelInterfacePtr::dynamicCast(mMuteIface)); baseChannel->plugInterface(Tp::AbstractChannelInterfacePtr::dynamicCast(mSplittableIface)); mBaseChannel = baseChannel; mCallChannel = Tp::BaseChannelCallTypePtr::dynamicCast(mBaseChannel->interface(TP_QT_IFACE_CHANNEL_TYPE_CALL)); mCallChannel->setHangupCallback(Tp::memFun(this,&MockCallChannel::onHangup)); mCallChannel->setAcceptCallback(Tp::memFun(this,&MockCallChannel::onAccept)); // init must be called after initialization, otherwise we will have no object path registered. QTimer::singleShot(0, this, SLOT(init())); } void MockCallChannel::onHangup(uint /* reason */, const QString& /* detailedReason */, const QString& /* message */, Tp::DBusError* /* error */) { // TODO: use the parameters sent by telepathy mRequestedHangup = true; setCallState("disconnected"); } void MockCallChannel::onAccept(Tp::DBusError*) { setCallState("active"); } void MockCallChannel::init() { Tp::CallMemberMap memberFlags; Tp::HandleIdentifierMap identifiers; QVariantMap stateDetails; Tp::CallStateReason reason; mIncoming = mState == "incoming" || mState == "waiting"; identifiers[mTargetHandle] = mPhoneNumber; reason.actor = 0; reason.reason = Tp::CallStateChangeReasonProgressMade; reason.message = ""; reason.DBusReason = ""; if (mIncoming) { memberFlags[mTargetHandle] = 0; } else { memberFlags[mTargetHandle] = Tp::CallMemberFlagRinging; } mCallChannel->setCallState(Tp::CallStateInitialising, 0, reason, stateDetails); mCallContent = Tp::BaseCallContent::create(baseChannel()->dbusConnection(), baseChannel().data(), "audio", Tp::MediaStreamTypeAudio, Tp::MediaStreamDirectionNone); mDTMFIface = Tp::BaseCallContentDTMFInterface::create(); mCallContent->plugInterface(Tp::AbstractCallContentInterfacePtr::dynamicCast(mDTMFIface)); mCallChannel->addContent(mCallContent); mDTMFIface->setStartToneCallback(Tp::memFun(this,&MockCallChannel::onDTMFStartTone)); mDTMFIface->setStopToneCallback(Tp::memFun(this,&MockCallChannel::onDTMFStopTone)); mCallChannel->setMembersFlags(memberFlags, identifiers, Tp::UIntList(), reason); mCallChannel->setCallState(Tp::CallStateInitialised, 0, reason, stateDetails); QObject::connect(mBaseChannel.data(), SIGNAL(closed()), this, SLOT(deleteLater())); } void MockCallChannel::onOfonoMuteChanged(bool mute) { Tp::LocalMuteState state = mute ? Tp::LocalMuteStateMuted : Tp::LocalMuteStateUnmuted; mMuteIface->setMuteState(state); } void MockCallChannel::onHoldStateChanged(const Tp::LocalHoldState &state, const Tp::LocalHoldStateReason& /* reason */, Tp::DBusError* /* error */) { if (state == Tp::LocalHoldStateHeld && mState == "active") { setCallState("held"); } else if (state == Tp::LocalHoldStateUnheld && mState == "held") { setCallState("active"); } } void MockCallChannel::onMuteStateChanged(const Tp::LocalMuteState &state, Tp::DBusError* /* error */) { #if 0 FIXME: reimplement if (state == Tp::LocalMuteStateMuted) { mConnection->callVolume()->setMuted(true); } else if (state == Tp::LocalMuteStateUnmuted) { mConnection->callVolume()->setMuted(false); } #else Q_UNUSED(state); #endif } void MockCallChannel::onDTMFStartTone(uchar event, Tp::DBusError* /* error */) { QString finalString; if (event == 10) { finalString = "*"; } else if (event == 11) { finalString = "#"; } else { finalString = QString::number(event); } qDebug() << "start tone" << finalString; } void MockCallChannel::onDTMFStopTone(Tp::DBusError* /* error */) { } void MockCallChannel::onSplit(Tp::DBusError* /* error */) { Q_EMIT splitted(); } QString MockCallChannel::objectPath() const { return mBaseChannel->objectPath(); } Tp::CallState MockCallChannel::callState() const { return (Tp::CallState) mCallChannel->callState(); } MockCallChannel::~MockCallChannel() { qDebug() << "call channel closed"; // TODO - for some reason the object is not being removed mConnection->dbusConnection().unregisterObject(mBaseChannel->objectPath(), QDBusConnection::UnregisterTree); } Tp::BaseChannelPtr MockCallChannel::baseChannel() { return mBaseChannel; } void MockCallChannel::setCallState(const QString &state) { Tp::CallStateReason reason; QVariantMap stateDetails; reason.actor = 0; reason.reason = Tp::CallStateChangeReasonUserRequested; reason.message = ""; reason.DBusReason = ""; if (state == "disconnected") { qDebug() << "disconnected"; if (mIncoming && mState == "incoming" && !mRequestedHangup) { reason.reason = Tp::CallStateChangeReasonNoAnswer; } mCallChannel->setCallState(Tp::CallStateEnded, 0, reason, stateDetails); // leave the channel opened for a bit longer so that the call state gets propagated correctly. QTimer::singleShot(10, mBaseChannel.data(), &Tp::BaseChannel::close); } else if (state == "active") { qDebug() << "active"; mHoldIface->setHoldState(Tp::LocalHoldStateUnheld, Tp::LocalHoldStateReasonNone); if (mState == "dialing" || mState == "alerting" || mState == "incoming") { mCallChannel->setCallState(Tp::CallStateAccepted, 0, reason, stateDetails); } mCallChannel->setCallState(Tp::CallStateActive, 0, reason, stateDetails); } else if (state == "held") { mHoldIface->setHoldState(Tp::LocalHoldStateHeld, Tp::LocalHoldStateReasonNone); qDebug() << "held"; } else if (state == "dialing") { qDebug() << "dialing"; } else if (state == "alerting") { qDebug() << "alerting"; } else if (state == "incoming") { mIncoming = true; qDebug() << "incoming"; } else if (state == "waiting") { mIncoming = true; qDebug() << "waiting"; } mState = state; Q_EMIT callStateChanged(this, state); } history-service-0.5/tests/common/mock/callchannel.h000066400000000000000000000051031455450246700225300ustar00rootroot00000000000000/** * Copyright (C) 2013 Canonical, Ltd. * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU General Public License version 3, as published by * the Free Software Foundation. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, * SATISFACTORY QUALITY, 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 . * * Authors: Tiago Salem Herrmann Gustavo Pichorim Boiko */ #ifndef MOCKCALLCHANNEL_H #define MOCKCALLCHANNEL_H #include #include #include #include #include #include "connection.h" class MockConnection; class MockCallChannel : public QObject { Q_OBJECT public: MockCallChannel(MockConnection *conn, QString phoneNumber, QString state, uint targetHandle, QObject *parent = 0); ~MockCallChannel(); Tp::BaseChannelPtr baseChannel(); void onHangup(uint reason, const QString &detailedReason, const QString &message, Tp::DBusError* error); void onAccept(Tp::DBusError*); void onMuteStateChanged(const Tp::LocalMuteState &state, Tp::DBusError *error); void onHoldStateChanged(const Tp::LocalHoldState &state, const Tp::LocalHoldStateReason &reason, Tp::DBusError *error); void onDTMFStartTone(uchar event, Tp::DBusError *error); void onDTMFStopTone(Tp::DBusError *error); void onSplit(Tp::DBusError *error); QString objectPath() const; Tp::CallState callState() const; public Q_SLOTS: void setCallState(const QString &state); void init(); void onOfonoMuteChanged(bool mute); Q_SIGNALS: void callStateChanged(MockCallChannel *channel, const QString &state); void splitted(); private: bool mIncoming; bool mRequestedHangup; MockConnection *mConnection; QString mPhoneNumber; uint mTargetHandle; QString mState; QString mObjPath; Tp::BaseChannelPtr mBaseChannel; Tp::BaseChannelHoldInterfacePtr mHoldIface; Tp::BaseCallMuteInterfacePtr mMuteIface; Tp::BaseChannelSplittableInterfacePtr mSplittableIface; Tp::BaseChannelCallTypePtr mCallChannel; Tp::BaseCallContentDTMFInterfacePtr mDTMFIface; Tp::BaseCallContentPtr mCallContent; }; #endif // MOCKCALLCHANNEL_H history-service-0.5/tests/common/mock/conferencecallchannel.cpp000066400000000000000000000237701455450246700251250ustar00rootroot00000000000000/** * Copyright (C) 2013 Canonical, Ltd. * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License version 3, as published by * the Free Software Foundation. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . * * Authors: * Tiago Salem Herrmann * Gustavo Pichorim Boiko */ #include "conferencecallchannel.h" #include "callchannel.h" MockConferenceCallChannel::MockConferenceCallChannel(MockConnection *conn, QList callChannels, QObject* /* parent */): mRequestedHangup(false), mConnection(conn), mDtmfLock(false), mCallChannels(callChannels) { Q_FOREACH(MockCallChannel *channel, mConnection->callChannels().values()) { if (channel->callState() == Tp::CallStateActive) { QDBusObjectPath path(channel->baseChannel()->objectPath()); mCallChannels << path; } } Tp::BaseChannelPtr baseChannel = Tp::BaseChannel::create(mConnection, TP_QT_IFACE_CHANNEL_TYPE_CALL, Tp::HandleTypeNone, 0); Tp::BaseChannelCallTypePtr callType = Tp::BaseChannelCallType::create(baseChannel.data(), true, Tp::StreamTransportTypeUnknown, true, false, "",""); baseChannel->plugInterface(Tp::AbstractChannelInterfacePtr::dynamicCast(callType)); mHoldIface = Tp::BaseChannelHoldInterface::create(); mHoldIface->setSetHoldStateCallback(Tp::memFun(this,&MockConferenceCallChannel::onHoldStateChanged)); mMuteIface = Tp::BaseCallMuteInterface::create(); mMuteIface->setSetMuteStateCallback(Tp::memFun(this,&MockConferenceCallChannel::onMuteStateChanged)); mSpeakerIface = BaseChannelSpeakerInterface::create(); mSpeakerIface->setTurnOnSpeakerCallback(Tp::memFun(this,&MockConferenceCallChannel::onTurnOnSpeaker)); mConferenceIface = Tp::BaseChannelConferenceInterface::create(mCallChannels); mMergeableIface = Tp::BaseChannelMergeableConferenceInterface::create(); mMergeableIface->setMergeCallback(Tp::memFun(this,&MockConferenceCallChannel::onMerge)); baseChannel->plugInterface(Tp::AbstractChannelInterfacePtr::dynamicCast(mHoldIface)); baseChannel->plugInterface(Tp::AbstractChannelInterfacePtr::dynamicCast(mMuteIface)); baseChannel->plugInterface(Tp::AbstractChannelInterfacePtr::dynamicCast(mSpeakerIface)); baseChannel->plugInterface(Tp::AbstractChannelInterfacePtr::dynamicCast(mConferenceIface)); baseChannel->plugInterface(Tp::AbstractChannelInterfacePtr::dynamicCast(mMergeableIface)); mBaseChannel = baseChannel; mCallChannel = Tp::BaseChannelCallTypePtr::dynamicCast(mBaseChannel->interface(TP_QT_IFACE_CHANNEL_TYPE_CALL)); mCallChannel->setHangupCallback(Tp::memFun(this,&MockConferenceCallChannel::onHangup)); Tp::CallStateReason reason; QVariantMap stateDetails; reason.actor = 0; reason.reason = Tp::CallStateChangeReasonUserRequested; reason.message = ""; reason.DBusReason = ""; mCallChannel->setCallState(Tp::CallStateActive, 0, reason, stateDetails); QObject::connect(mConnection, SIGNAL(channelSplitted(QDBusObjectPath)), SLOT(onChannelSplitted(QDBusObjectPath))); // init must be called after initialization, otherwise we will have no object path registered. QTimer::singleShot(0, this, SLOT(init())); } Tp::BaseChannelPtr MockConferenceCallChannel::baseChannel() { return mBaseChannel; } void MockConferenceCallChannel::onMerge(const QDBusObjectPath &channel, Tp::DBusError* /* error */) { if (!mCallChannels.contains(channel)) { mCallChannels << channel; mConferenceIface->mergeChannel(channel, 0, QVariantMap()); Q_EMIT channelMerged(channel.path()); } } void MockConferenceCallChannel::onChannelSplitted(const QDBusObjectPath &path) { if (mCallChannels.contains(path)) { mCallChannels.removeAll(path); mConferenceIface->removeChannel(path, QVariantMap()); } if (mCallChannels.size() == 1) { // remove the call channel from the conference before closing it. mConferenceIface->removeChannel(mCallChannels.takeFirst(), QVariantMap()); Tp::CallStateReason reason; QVariantMap stateDetails; reason.actor = 0; reason.reason = Tp::CallStateChangeReasonUserRequested; reason.message = ""; reason.DBusReason = ""; mCallChannel->setCallState(Tp::CallStateEnded, 0, reason, stateDetails); mBaseChannel->close(); } } void MockConferenceCallChannel::onTurnOnSpeaker(bool /* active */, Tp::DBusError* /* error */) { //mConnection->setSpeakerMode(active); // FIXME: reimplement } void MockConferenceCallChannel::onHangup(uint reason, const QString& /* detailedReason */, const QString &message, Tp::DBusError* /* error */) { //FIXME: reimplement Tp::CallStateReason theReason; QVariantMap stateDetails; theReason.actor = 0; theReason.reason = reason; theReason.message = message; theReason.DBusReason = ""; mCallChannel->setCallState(Tp::CallStateEnded, 0, theReason, stateDetails); mBaseChannel->close(); } void MockConferenceCallChannel::init() { QVariantMap stateDetails; Tp::CallStateReason reason; mObjPath = mBaseChannel->objectPath(); reason.actor = 0; reason.reason = Tp::CallStateChangeReasonProgressMade; reason.message = ""; reason.DBusReason = ""; mCallChannel->setCallState(Tp::CallStateActive, 0, reason, stateDetails); mDTMFIface = Tp::BaseCallContentDTMFInterface::create(); mDTMFIface->setStartToneCallback(Tp::memFun(this,&MockConferenceCallChannel::onDTMFStartTone)); mDTMFIface->setStopToneCallback(Tp::memFun(this,&MockConferenceCallChannel::onDTMFStopTone)); QObject::connect(mBaseChannel.data(), SIGNAL(closed()), this, SLOT(deleteLater())); //QObject::connect(mConnection->callVolume(), SIGNAL(mutedChanged(bool)), SLOT(onOfonoMuteChanged(bool))); QObject::connect(mConnection, SIGNAL(speakerModeChanged(bool)), mSpeakerIface.data(), SLOT(setSpeakerMode(bool))); //QObject::connect(mConnection->voiceCallManager(), SIGNAL(sendTonesComplete(bool)), SLOT(onDtmfComplete(bool))); //mSpeakerIface->setSpeakerMode(mConnection->speakerMode()); QObject::connect(mConnection, SIGNAL(channelSplitted(const QDBusObjectPath&)), this, SLOT(onChannelSplitted(const QDBusObjectPath&))); QObject::connect(mConnection, SIGNAL(channelHangup(const QDBusObjectPath&)), this, SLOT(onChannelSplitted(const QDBusObjectPath&))); Q_EMIT initialized(); } void MockConferenceCallChannel::onOfonoMuteChanged(bool mute) { Tp::LocalMuteState state = mute ? Tp::LocalMuteStateMuted : Tp::LocalMuteStateUnmuted; mMuteIface->setMuteState(state); } void MockConferenceCallChannel::setConferenceActive(bool active) { if (active) { mHoldIface->setHoldState(Tp::LocalHoldStateUnheld, Tp::LocalHoldStateReasonNone); } else { mHoldIface->setHoldState(Tp::LocalHoldStateHeld, Tp::LocalHoldStateReasonNone); } } void MockConferenceCallChannel::onHoldStateChanged(const Tp::LocalHoldState& /* state */, const Tp::LocalHoldStateReason& /* reason */, Tp::DBusError* /* error */) { /*if (state == Tp::LocalHoldStateHeld && mHoldIface->getHoldState() == Tp::LocalHoldStateUnheld) { mConnection->voiceCallManager()->swapCalls(); } else if (state == Tp::LocalHoldStateUnheld && mHoldIface->getHoldState() == Tp::LocalHoldStateHeld) { mConnection->voiceCallManager()->swapCalls(); }*/ // FIXME: reimplement } void MockConferenceCallChannel::onMuteStateChanged(const Tp::LocalMuteState& /* state */, Tp::DBusError* /* error */) { /*if (state == Tp::LocalMuteStateMuted) { mConnection->callVolume()->setMuted(true); } else if (state == Tp::LocalMuteStateUnmuted) { mConnection->callVolume()->setMuted(false); }*/ // FIXME: reimplement } void MockConferenceCallChannel::sendNextDtmf() { /*if (mDtmfLock) { return; } if (!mDtmfPendingStrings.isEmpty()) { mDtmfLock = true; mConnection->voiceCallManager()->sendTones(mDtmfPendingStrings.front()); }*/ // FIXME: reimplement } void MockConferenceCallChannel::onDtmfComplete(bool success) { mDtmfLock = false; if (success) { mDtmfPendingStrings.removeFirst(); if (mDtmfPendingStrings.isEmpty()) { return; } sendNextDtmf(); } else { QTimer::singleShot(1000, this, SLOT(sendNextDtmf())); } } void MockConferenceCallChannel::onDTMFStartTone(uchar event, Tp::DBusError* /* error */) { QString finalString; if (event == 10) { finalString = "*"; } else if (event == 11) { finalString = "#"; } else { finalString = QString::number(event); } qDebug() << "start tone" << finalString; // we can't append to the first item in the queue as it is being sent and // we dont know yet if it will succeed or not. if (mDtmfPendingStrings.count() > 1) { mDtmfPendingStrings[1] += finalString; } else { mDtmfPendingStrings << finalString; } sendNextDtmf(); } void MockConferenceCallChannel::onDTMFStopTone(Tp::DBusError* /* error */) { } MockConferenceCallChannel::~MockConferenceCallChannel() { qDebug() << "conference call channel closed"; // TODO - for some reason the object is not being removed mConnection->dbusConnection().unregisterObject(mObjPath, QDBusConnection::UnregisterTree); } history-service-0.5/tests/common/mock/conferencecallchannel.h000066400000000000000000000055701455450246700245700ustar00rootroot00000000000000/** * Copyright (C) 2013 Canonical, Ltd. * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License version 3, as published by * the Free Software Foundation. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . * * Authors: * Tiago Salem Herrmann * Gustavo Pichorim Boiko */ #ifndef MOCKCONFERENCECALLCHANNEL_H #define MOCKCONFERENCECALLCHANNEL_H #include #include #include #include #include #include "connection.h" #include "speakeriface.h" class MockConnection; class MockConferenceCallChannel : public QObject { Q_OBJECT public: MockConferenceCallChannel(MockConnection *conn, QList callChannels, QObject *parent = 0); ~MockConferenceCallChannel(); void onHangup(uint reason, const QString &detailedReason, const QString &message, Tp::DBusError* error); void onMuteStateChanged(const Tp::LocalMuteState &state, Tp::DBusError *error); void onHoldStateChanged(const Tp::LocalHoldState &state, const Tp::LocalHoldStateReason &reason, Tp::DBusError *error); void onDTMFStartTone(uchar event, Tp::DBusError *error); void onDTMFStopTone(Tp::DBusError *error); void onTurnOnSpeaker(bool active, Tp::DBusError *error); void onMerge(const QDBusObjectPath &channel, Tp::DBusError *error); Tp::BaseChannelPtr baseChannel(); void setConferenceActive(bool active); Q_SIGNALS: void channelMerged(const QString &objectPath); void initialized(); private Q_SLOTS: void onDtmfComplete(bool success); void sendNextDtmf(); void init(); void onOfonoMuteChanged(bool mute); void onChannelSplitted(const QDBusObjectPath &path); private: bool mRequestedHangup; MockConnection *mConnection; bool mDtmfLock; QList mCallChannels; QString mObjPath; QString mPreviousState; bool mIncoming; Tp::BaseChannelPtr mBaseChannel; Tp::BaseChannelHoldInterfacePtr mHoldIface; Tp::BaseChannelConferenceInterfacePtr mConferenceIface; Tp::BaseChannelMergeableConferenceInterfacePtr mMergeableIface; Tp::BaseCallMuteInterfacePtr mMuteIface; BaseChannelSpeakerInterfacePtr mSpeakerIface; Tp::BaseChannelCallTypePtr mCallChannel; Tp::BaseCallContentDTMFInterfacePtr mDTMFIface; QStringList mDtmfPendingStrings; }; #endif // MOCKCONFERENCECALLCHANNEL_H history-service-0.5/tests/common/mock/connection.cpp000066400000000000000000000633311455450246700227650ustar00rootroot00000000000000/** * Copyright (C) 2013 Canonical, Ltd. * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU General Public License version 3, as published by * the Free Software Foundation. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, * SATISFACTORY QUALITY, 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 . * * Authors: Tiago Salem Herrmann * Gustavo Pichorim Boiko */ #include #include #include #include // telepathy-mock #include "connection.h" #include "phoneutils_p.h" #include "protocol.h" #include "conferencecallchannel.h" #include "mockconnectiondbus.h" MockConnection::MockConnection(const QDBusConnection &dbusConnection, const QString &cmName, const QString &protocolName, const QVariantMap ¶meters) : Tp::BaseConnection(dbusConnection, cmName, protocolName, parameters), mConferenceCall(0), mVoicemailIndicator(false), mVoicemailCount(0) { setSelfHandle(newHandle("")); setConnectCallback(Tp::memFun(this,&MockConnection::connect)); setInspectHandlesCallback(Tp::memFun(this,&MockConnection::inspectHandles)); setRequestHandlesCallback(Tp::memFun(this,&MockConnection::requestHandles)); setCreateChannelCallback(Tp::memFun(this,&MockConnection::createChannel)); // initialise requests interface (Connection.Interface.Requests) requestsIface = Tp::BaseConnectionRequestsInterface::create(this); // set requestable text channel properties Tp::RequestableChannelClass text; text.fixedProperties[TP_QT_IFACE_CHANNEL+".ChannelType"] = TP_QT_IFACE_CHANNEL_TYPE_TEXT; text.fixedProperties[TP_QT_IFACE_CHANNEL+".TargetHandleType"] = Tp::HandleTypeContact; text.allowedProperties.append(TP_QT_IFACE_CHANNEL+".TargetHandle"); text.allowedProperties.append(TP_QT_IFACE_CHANNEL+".TargetID"); text.allowedProperties.append(TP_QT_IFACE_CHANNEL_INTERFACE_CONFERENCE + QLatin1String(".InitialInviteeHandles")); // set requestable call channel properties Tp::RequestableChannelClass call; call.fixedProperties[TP_QT_IFACE_CHANNEL+".ChannelType"] = TP_QT_IFACE_CHANNEL_TYPE_CALL; call.fixedProperties[TP_QT_IFACE_CHANNEL+".TargetHandleType"] = Tp::HandleTypeContact; call.fixedProperties[TP_QT_IFACE_CHANNEL_TYPE_CALL+".InitialAudio"] = true; call.allowedProperties.append(TP_QT_IFACE_CHANNEL+".TargetHandle"); call.allowedProperties.append(TP_QT_IFACE_CHANNEL+".TargetID"); call.allowedProperties.append(TP_QT_IFACE_CHANNEL_TYPE_CALL+".InitialAudio"); call.allowedProperties.append(TP_QT_IFACE_CHANNEL_TYPE_CALL+".InitialVideo"); call.allowedProperties.append(TP_QT_IFACE_CHANNEL_TYPE_CALL+".InitialAudioName"); call.allowedProperties.append(TP_QT_IFACE_CHANNEL_TYPE_CALL+".InitialVideoName"); call.allowedProperties.append(TP_QT_IFACE_CHANNEL_TYPE_CALL+".InitialTransport"); call.allowedProperties.append(TP_QT_IFACE_CHANNEL_TYPE_CALL+".HardwareStreaming"); call.allowedProperties.append(TP_QT_IFACE_CHANNEL_INTERFACE_CONFERENCE + QLatin1String(".InitialChannels")); requestsIface->requestableChannelClasses << text << call; plugInterface(Tp::AbstractConnectionInterfacePtr::dynamicCast(requestsIface)); // init presence interface simplePresenceIface = Tp::BaseConnectionSimplePresenceInterface::create(); simplePresenceIface->setSetPresenceCallback(Tp::memFun(this,&MockConnection::setPresence)); simplePresenceIface->setMaximumStatusMessageLength(255); plugInterface(Tp::AbstractConnectionInterfacePtr::dynamicCast(simplePresenceIface)); // Set Presence Tp::SimpleStatusSpec presenceOnline; presenceOnline.type = Tp::ConnectionPresenceTypeAvailable; presenceOnline.maySetOnSelf = true; presenceOnline.canHaveMessage = true; Tp::SimpleStatusSpec presenceOffline; presenceOffline.type = Tp::ConnectionPresenceTypeOffline; presenceOffline.maySetOnSelf = true; presenceOffline.canHaveMessage = true; Tp::SimpleStatusSpec presenceAway; presenceAway.type = Tp::ConnectionPresenceTypeAway; presenceAway.maySetOnSelf = true; presenceAway.canHaveMessage = true; mStatuses.insert(QLatin1String("available"), presenceOnline); mStatuses.insert(QLatin1String("offline"), presenceOffline); mStatuses.insert(QLatin1String("away"), presenceAway); mStatuses.insert(QLatin1String("simlocked"), presenceAway); mStatuses.insert(QLatin1String("flightmode"), presenceOffline); mStatuses.insert(QLatin1String("nosim"), presenceOffline); mStatuses.insert(QLatin1String("nomodem"), presenceOffline); mStatuses.insert(QLatin1String("registered"), presenceOnline); mStatuses.insert(QLatin1String("roaming"), presenceOnline); mStatuses.insert(QLatin1String("unregistered"), presenceAway); mStatuses.insert(QLatin1String("denied"), presenceAway); mStatuses.insert(QLatin1String("unknown"), presenceAway); mStatuses.insert(QLatin1String("searching"), presenceAway); simplePresenceIface->setStatuses(mStatuses); mSelfPresence.type = Tp::ConnectionPresenceTypeOffline; contactsIface = Tp::BaseConnectionContactsInterface::create(); contactsIface->setGetContactAttributesCallback(Tp::memFun(this,&MockConnection::getContactAttributes)); contactsIface->setContactAttributeInterfaces(QStringList() << TP_QT_IFACE_CONNECTION << TP_QT_IFACE_CONNECTION_INTERFACE_SIMPLE_PRESENCE); plugInterface(Tp::AbstractConnectionInterfacePtr::dynamicCast(contactsIface)); // init custom emergency mode interface (not provided by telepathy emergencyModeIface = BaseConnectionEmergencyModeInterface::create(); emergencyModeIface->setEmergencyNumbersCallback(Tp::memFun(this,&MockConnection::emergencyNumbers)); plugInterface(Tp::AbstractConnectionInterfacePtr::dynamicCast(emergencyModeIface)); mEmergencyNumbers << "123" << "456" << "789"; emergencyModeIface->setEmergencyNumbers(mEmergencyNumbers); // init custom voicemail interface (not provided by telepathy) voicemailIface = BaseConnectionVoicemailInterface::create(); voicemailIface->setVoicemailCountCallback(Tp::memFun(this,&MockConnection::voicemailCount)); voicemailIface->setVoicemailIndicatorCallback(Tp::memFun(this,&MockConnection::voicemailIndicator)); voicemailIface->setVoicemailNumberCallback(Tp::memFun(this,&MockConnection::voicemailNumber)); voicemailIface->setVoicemailNumber(mVoicemailNumber); plugInterface(Tp::AbstractConnectionInterfacePtr::dynamicCast(voicemailIface)); voicemailIface->setVoicemailCount(mVoicemailCount); voicemailIface->setVoicemailIndicator(mVoicemailIndicator); mVoicemailNumber = "555"; supplementaryServicesIface = BaseConnectionUSSDInterface::create(); supplementaryServicesIface->setInitiateCallback(Tp::memFun(this,&MockConnection::USSDInitiate)); supplementaryServicesIface->setRespondCallback(Tp::memFun(this,&MockConnection::USSDRespond)); supplementaryServicesIface->setCancelCallback(Tp::memFun(this,&MockConnection::USSDCancel)); static int serial = 0; serial++; supplementaryServicesIface->setSerial(QString("accountserial%1").arg(QString::number(serial))); plugInterface(Tp::AbstractConnectionInterfacePtr::dynamicCast(supplementaryServicesIface)); mDBus = new MockConnectionDBus(this); } MockConnection::~MockConnection() { } void MockConnection::addMMSToService(const QString &path, const QVariantMap &properties, const QString &servicePath) { qDebug() << "addMMSToService " << path << properties << servicePath; #if 0 FIXME: re-implement MMSDMessage *msg = new MMSDMessage(path, properties); QObject::connect(msg, SIGNAL(propertyChanged(QString,QVariant)), SLOT(onMMSPropertyChanged(QString,QVariant))); mServiceMMSList[servicePath].append(msg); if (properties["Status"] == "received") { const QString normalizedNumber = PhoneUtils::normalizePhoneNumber(properties["Sender"].toString()); // check if there is an open channel for this number and use it Q_FOREACH(const QString &phoneNumber, mTextChannels.keys()) { if (PhoneUtils::comparePhoneNumbers(normalizedNumber, phoneNumber)) { qDebug() << "existing channel" << mTextChannels[phoneNumber]; mTextChannels[phoneNumber]->mmsReceived(path, properties); return; } } Tp::DBusError error; bool yours; qDebug() << "new handle" << normalizedNumber; uint handle = newHandle(normalizedNumber); ensureChannel(TP_QT_IFACE_CHANNEL_TYPE_TEXT,Tp::HandleTypeContact, handle, yours, handle, false, &error); if(error.isValid()) { qCritical() << "Error creating channel for incoming message " << error.name() << error.message(); return; } mTextChannels[normalizedNumber]->mmsReceived(path, properties); } #endif } MockTextChannel *MockConnection::textChannelForRecipients(const QStringList &recipients) { Q_FOREACH(MockTextChannel *channel, mTextChannels) { QStringList channelRecipients = channel->recipients(); if (channelRecipients.length() != recipients.length()) { continue; } bool ok = true; Q_FOREACH(const QString &recipient, recipients) { if (!channelRecipients.contains(recipient)) { ok = false; break; } } if (ok) { return channel; } } return 0; } uint MockConnection::setPresence(const QString& status, const QString& statusMessage, Tp::DBusError *error) { qDebug() << "setPresence" << status << statusMessage; Tp::SimpleContactPresences presences; if (!mStatuses.contains(status) || !mStatuses[status].maySetOnSelf) { error->set(TP_QT_ERROR_INVALID_ARGUMENT, "Status not supported or cannot be set"); return selfHandle(); } Tp::SimpleStatusSpec spec = mStatuses[status]; mSelfPresence.status = status; mSelfPresence.type = spec.type; mSelfPresence.statusMessage = spec.canHaveMessage ? statusMessage : ""; presences[selfHandle()] = mSelfPresence; simplePresenceIface->setPresences(presences); return selfHandle(); } Tp::ContactAttributesMap MockConnection::getContactAttributes(const Tp::UIntList &handles, const QStringList &ifaces, Tp::DBusError *error) { qDebug() << "getContactAttributes" << handles << ifaces; Tp::ContactAttributesMap attributesMap; QVariantMap attributes; Q_FOREACH(uint handle, handles) { attributes[TP_QT_IFACE_CONNECTION+"/contact-id"] = inspectHandles(Tp::HandleTypeContact, Tp::UIntList() << handle, error).at(0); if (ifaces.contains(TP_QT_IFACE_CONNECTION_INTERFACE_SIMPLE_PRESENCE)) { attributes[TP_QT_IFACE_CONNECTION_INTERFACE_SIMPLE_PRESENCE+"/presence"] = QVariant::fromValue(mSelfPresence); } attributesMap[handle] = attributes; } return attributesMap; } void MockConnection::setOnline(bool online) { qDebug() << "setOnline" << online; Tp::SimpleContactPresences presences; if (online) { mSelfPresence.status = "available"; mSelfPresence.statusMessage = ""; mSelfPresence.type = Tp::ConnectionPresenceTypeAvailable; } else { mSelfPresence.status = "offline"; mSelfPresence.statusMessage = ""; mSelfPresence.type = Tp::ConnectionPresenceTypeOffline; } presences[selfHandle()] = mSelfPresence; simplePresenceIface->setPresences(presences); } uint MockConnection::newHandle(const QString &identifier) { static int handleCount = 0; mHandles[++handleCount] = identifier; return handleCount; } QMap MockConnection::callChannels() { return mCallChannels; } QStringList MockConnection::inspectHandles(uint handleType, const Tp::UIntList& handles, Tp::DBusError *error) { QStringList identifiers; if( handleType != Tp::HandleTypeContact ) { error->set(TP_QT_ERROR_INVALID_ARGUMENT,"Not supported"); return QStringList(); } qDebug() << "MockConnection::inspectHandles " << handles; Q_FOREACH( uint handle, handles) { if (mHandles.keys().contains(handle)) { identifiers.append(mHandles.value(handle)); } else { error->set(TP_QT_ERROR_INVALID_HANDLE, "Handle not found"); return QStringList(); } } qDebug() << "MockConnection::inspectHandles " << identifiers; return identifiers; } void MockConnection::connect(Tp::DBusError* /* error */) { qDebug() << "MockConnection::connect"; setStatus(Tp::ConnectionStatusConnected, Tp::ConnectionStatusReasonRequested); } Tp::UIntList MockConnection::requestHandles(uint handleType, const QStringList& identifiers, Tp::DBusError* error) { qDebug() << "requestHandles"; Tp::UIntList handles; if( handleType != Tp::HandleTypeContact ) { error->set(TP_QT_ERROR_INVALID_ARGUMENT, "Not supported"); return Tp::UIntList(); } Q_FOREACH( const QString& identifier, identifiers) { if (mHandles.values().contains(identifier)) { handles.append(mHandles.key(identifier)); } else { handles.append(newHandle(identifier)); } } qDebug() << "requestHandles" << handles; return handles; } Tp::BaseChannelPtr MockConnection::createTextChannel(uint targetHandleType, uint targetHandle, const QVariantMap &hints, Tp::DBusError *error) { Q_UNUSED(targetHandleType); Q_UNUSED(error); if (hints.contains(TP_QT_IFACE_CHANNEL_INTERFACE_CONFERENCE + QLatin1String(".InitialInviteeHandles")) && targetHandleType == Tp::HandleTypeNone && targetHandle == 0) { } QStringList recipients; bool flash; if (hints.contains(TP_QT_IFACE_CHANNEL_INTERFACE_CONFERENCE + QLatin1String(".InitialInviteeHandles"))) { recipients << inspectHandles(Tp::HandleTypeContact, qdbus_cast(hints[TP_QT_IFACE_CHANNEL_INTERFACE_CONFERENCE + QLatin1String(".InitialInviteeHandles")]), error); } else { recipients << mHandles.value(targetHandle); } if (hints.contains(TP_QT_IFACE_CHANNEL_INTERFACE_SMS + QLatin1String(".Flash"))) { flash = hints[TP_QT_IFACE_CHANNEL_INTERFACE_SMS + QLatin1String(".Flash")].toBool(); } // FIXME: test flash messages Q_UNUSED(flash); MockTextChannel *channel = new MockTextChannel(this, recipients, targetHandle); QObject::connect(channel, SIGNAL(messageRead(QString)), SLOT(onMessageRead(QString))); QObject::connect(channel, SIGNAL(destroyed()), SLOT(onTextChannelClosed())); QObject::connect(channel, SIGNAL(messageSent(QString,QVariantMap)), SIGNAL(messageSent(QString,QVariantMap))); qDebug() << channel; mTextChannels << channel; return channel->baseChannel(); } void MockConnection::onMessageRead(const QString &id) { // FIXME: check what else to do Q_EMIT messageRead(id); } void MockConnection::onConferenceCallChannelClosed() { if (mConferenceCall) { mConferenceCall = NULL; } } void MockConnection::onCallChannelSplitted() { MockCallChannel *channel = qobject_cast(sender()); Q_EMIT channelSplitted(channel->baseChannel()->objectPath()); Q_EMIT channelSplitted(QDBusObjectPath(channel->baseChannel()->objectPath())); } Tp::BaseChannelPtr MockConnection::createCallChannel(uint targetHandleType, uint targetHandle, const QVariantMap &hints, Tp::DBusError *error) { Q_UNUSED(targetHandleType); QString requestedId = mHandles.value(targetHandle); if (hints.contains(TP_QT_IFACE_CHANNEL_INTERFACE_CONFERENCE + QLatin1String(".InitialChannels")) && targetHandleType == Tp::HandleTypeNone && targetHandle == 0) { // conference call request if (mConferenceCall) { error->set(TP_QT_ERROR_NOT_AVAILABLE, "Conference call already exists"); return Tp::BaseChannelPtr(); } QDBusArgument arg = hints[TP_QT_IFACE_CHANNEL_INTERFACE_CONFERENCE + QLatin1String(".InitialChannels")].value(); QList channels; arg >> channels; if (!channels.isEmpty()) { mConferenceCall = new MockConferenceCallChannel(this, channels); QObject::connect(mConferenceCall, SIGNAL(destroyed()), SLOT(onConferenceCallChannelClosed())); QObject::connect(mConferenceCall, SIGNAL(channelMerged(QString)), SIGNAL(channelMerged(QString))); // the object path is only availabe after we return to the event loop, so emit the conferenceCreated signal // only after that. QObject::connect(mConferenceCall, &MockConferenceCallChannel::initialized, [this]() { Q_EMIT conferenceCreated(mConferenceCall->baseChannel()->objectPath()); }); return mConferenceCall->baseChannel(); } error->set(TP_QT_ERROR_NOT_AVAILABLE, "Impossible to merge calls"); return Tp::BaseChannelPtr(); } Q_FOREACH(const QString &id, mCallChannels.keys()) { if (id == requestedId) { return mCallChannels[id]->baseChannel(); } } QString state = "dialing"; if (mInitialCallStatus.contains(requestedId)) { state = mInitialCallStatus.take(requestedId); } mCallChannels[requestedId] = new MockCallChannel(this, requestedId, state, targetHandle); QObject::connect(mCallChannels[requestedId], SIGNAL(destroyed()), SLOT(onCallChannelClosed())); QObject::connect(mCallChannels[requestedId], SIGNAL(callStateChanged(MockCallChannel*,QString)), SLOT(onCallStateChanged(MockCallChannel*,QString))); QObject::connect(mCallChannels[requestedId], SIGNAL(splitted()), SLOT(onCallChannelSplitted())); qDebug() << mCallChannels[requestedId]; if (!mIncomingCalls.contains(requestedId)) { Q_EMIT callReceived(requestedId); } return mCallChannels[requestedId]->baseChannel(); } Tp::BaseChannelPtr MockConnection::createChannel(const QVariantMap &request, Tp::DBusError *error) { const QString channelType = request.value(TP_QT_IFACE_CHANNEL + QLatin1String(".ChannelType")).toString(); uint targetHandleType = request.value(TP_QT_IFACE_CHANNEL + QLatin1String(".TargetHandleType")).toUInt(); uint targetHandle = request.value(TP_QT_IFACE_CHANNEL + QLatin1String(".TargetHandle")).toUInt(); qDebug() << "MockConnection::createChannel" << targetHandle; if (mSelfPresence.type != Tp::ConnectionPresenceTypeAvailable) { error->set(TP_QT_ERROR_NETWORK_ERROR, "No network available"); return Tp::BaseChannelPtr(); } if (channelType == TP_QT_IFACE_CHANNEL_TYPE_TEXT) { return createTextChannel(targetHandleType, targetHandle, request, error); } else if (channelType == TP_QT_IFACE_CHANNEL_TYPE_CALL) { return createCallChannel(targetHandleType, targetHandle, request, error); } else { error->set(TP_QT_ERROR_NOT_IMPLEMENTED, "Channel type not available"); } return Tp::BaseChannelPtr(); } void MockConnection::placeIncomingMessage(const QString &message, const QVariantMap &info) { QString sender = info["Sender"].toString(); QStringList recipients = info["Recipients"].toStringList(); MockTextChannel *channel = textChannelForRecipients(recipients); if (!channel) { // request the channel Tp::DBusError error; QVariantMap request; bool yours; uint handle = newHandle(sender); request[TP_QT_IFACE_CHANNEL + QLatin1String(".ChannelType")] = TP_QT_IFACE_CHANNEL_TYPE_TEXT; request[TP_QT_IFACE_CHANNEL + QLatin1String(".InitiatorHandle")] = handle; request[TP_QT_IFACE_CHANNEL + QLatin1String(".TargetHandleType")] = Tp::HandleTypeContact; request[TP_QT_IFACE_CHANNEL + QLatin1String(".TargetHandle")] = handle; ensureChannel(request, yours, false, &error); if(error.isValid()) { qWarning() << "Error creating channel for incoming message" << error.name() << error.message(); return; } channel = textChannelForRecipients(recipients); if (!channel) { return; } } if (info.contains("Attachments")){ channel->mmsReceived(message, info); } else { channel->messageReceived(message, info); } } void MockConnection::placeDeliveryReport(const QStringList &recipients, const QString &messageId, const QString &status) { MockTextChannel *channel = textChannelForRecipients(recipients); if (!channel) { qWarning() << "Text channel for recipients" << recipients << "not found while placing a delivery report"; return; } channel->placeDeliveryReport(messageId, status); } void MockConnection::onTextChannelClosed() { MockTextChannel *channel = static_cast(sender()); if (channel) { qDebug() << "text channel closed for recipients " << channel->recipients(); mTextChannels.removeAll(channel); } } void MockConnection::onCallChannelClosed() { qDebug() << "onCallChannelClosed()"; MockCallChannel *channel = static_cast(sender()); if (channel) { QString key = mCallChannels.key(channel); qDebug() << "call channel closed for number " << key; mCallChannels.remove(key); mIncomingCalls.removeAll(key); Q_EMIT callEnded(key); } } void MockConnection::onCallChannelDestroyed() { // FIXME: implement } void MockConnection::onCallStateChanged(MockCallChannel *channel, const QString &state) { const QString key = mCallChannels.key(channel); if (key.isEmpty()) { return; } Q_EMIT callStateChanged(key, channel->objectPath(), state); } uint MockConnection::ensureHandle(const QString &id) { if (mHandles.values().contains(id)) { return mHandles.key(id); } return newHandle(id); } QString MockConnection::placeCall(const QVariantMap &properties) { qDebug() << "new call" << properties; bool yours; Tp::DBusError error; QString callerId = properties["Caller"].toString(); QString state = properties["State"].toString(); if (mCallChannels.contains(callerId)) { return mCallChannels[callerId]->objectPath(); } uint handle = ensureHandle(callerId); uint initiatorHandle = 0; if (state == "incoming" || state == "waiting") { initiatorHandle = handle; } else { initiatorHandle = selfHandle(); } qDebug() << "initiatorHandle " <objectPath(); } QStringList MockConnection::emergencyNumbers(Tp::DBusError* /* error */) { return mEmergencyNumbers; } void MockConnection::setEmergencyNumbers(const QStringList &emergencyNumbers) { mEmergencyNumbers = emergencyNumbers; emergencyModeIface->setEmergencyNumbers(emergencyNumbers); } bool MockConnection::voicemailIndicator(Tp::DBusError* /* error */) { return mVoicemailIndicator; } void MockConnection::setVoicemailIndicator(bool visible) { mVoicemailIndicator = visible; voicemailIface->setVoicemailIndicator(visible); } QString MockConnection::voicemailNumber(Tp::DBusError* /* error */) { return mVoicemailNumber; } void MockConnection::setVoicemailNumber(const QString &number) { mVoicemailNumber = number; voicemailIface->setVoicemailNumber(mVoicemailNumber); } uint MockConnection::voicemailCount(Tp::DBusError* /* error */) { return mVoicemailCount; } void MockConnection::setVoicemailCount(int count) { mVoicemailCount = count; voicemailIface->setVoicemailCount(mVoicemailCount); } void MockConnection::USSDInitiate(const QString& /* command */, Tp::DBusError* /* error */) { // FIXME: implement } void MockConnection::USSDRespond(const QString& /* reply */, Tp::DBusError* /* error */) { // FIXME: implement } void MockConnection::USSDCancel(Tp::DBusError* /* error */) { // FIXME: implement } QString MockConnection::serial() { return supplementaryServicesIface->serial(); } QString MockConnection::uniqueName() const { static int count = 0; return QString("connection%1%2").arg((quintptr) this, 0, 16).arg(count++, 0, 16); } void MockConnection::hangupCall(const QString &callerId) { if (!mCallChannels.contains(callerId)) { return; } mCallChannels[callerId]->setCallState("disconnected"); mIncomingCalls.removeAll(callerId); } void MockConnection::setCallState(const QString &phoneNumber, const QString &state) { if (!mCallChannels.contains(phoneNumber)) { return; } mCallChannels[phoneNumber]->setCallState(state); } history-service-0.5/tests/common/mock/connection.h000066400000000000000000000140101455450246700224200ustar00rootroot00000000000000/** * Copyright (C) 2013 Canonical, Ltd. * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU General Public License version 3, as published by * the Free Software Foundation. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, * SATISFACTORY QUALITY, 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 . * * Authors: Tiago Salem Herrmann * Gustavo Pichorim Boiko */ #ifndef MOCKCONNECTION_H #define MOCKCONNECTION_H // qt #include // telepathy-qt #include #include #include #include #include // mock-cm #include "textchannel.h" #include "callchannel.h" #include "dbustypes.h" #include "emergencymodeiface.h" #include "ussdiface.h" #include "voicemailiface.h" class MockTextChannel; class MockCallChannel; class MockConnectionDBus; class MockConferenceCallChannel; class MockConnection : public Tp::BaseConnection { Q_OBJECT Q_DISABLE_COPY(MockConnection) public: MockConnection(const QDBusConnection &dbusConnection, const QString &cmName, const QString &protocolName, const QVariantMap ¶meters); QStringList inspectHandles(uint handleType, const Tp::UIntList& handles, Tp::DBusError *error); Tp::UIntList requestHandles(uint handleType, const QStringList& identifiers, Tp::DBusError* error); Tp::BaseChannelPtr createChannel(const QVariantMap& request, Tp::DBusError *error); Tp::ContactAttributesMap getContactAttributes(const Tp::UIntList &handles, const QStringList &ifaces, Tp::DBusError *error); uint setPresence(const QString& status, const QString& statusMessage, Tp::DBusError *error); void connect(Tp::DBusError *error); void setOnline(bool online); Tp::BaseConnectionRequestsInterfacePtr requestsIface; Tp::BaseConnectionSimplePresenceInterfacePtr simplePresenceIface; Tp::BaseConnectionContactsInterfacePtr contactsIface; uint newHandle(const QString &identifier); QMap callChannels(); // phone custom interfaces BaseConnectionEmergencyModeInterfacePtr emergencyModeIface; BaseConnectionVoicemailInterfacePtr voicemailIface; BaseConnectionUSSDInterfacePtr supplementaryServicesIface; uint ensureHandle(const QString &id); Tp::BaseChannelPtr createTextChannel(uint targetHandleType, uint targetHandle, const QVariantMap &hints, Tp::DBusError *error); Tp::BaseChannelPtr createCallChannel(uint targetHandleType, uint targetHandle, const QVariantMap &hints, Tp::DBusError *error); ~MockConnection(); QString placeCall(const QVariantMap &properties); QStringList emergencyNumbers(Tp::DBusError *error); void setEmergencyNumbers(const QStringList &emergencyNumbers); bool voicemailIndicator(Tp::DBusError *error); void setVoicemailIndicator(bool visible); QString voicemailNumber(Tp::DBusError *error); void setVoicemailNumber(const QString &number); uint voicemailCount(Tp::DBusError *error); void setVoicemailCount(int count); void USSDInitiate(const QString &command, Tp::DBusError *error); void USSDRespond(const QString &reply, Tp::DBusError *error); void USSDCancel(Tp::DBusError *error); QString serial(); // FIXME: there is a problem in telepathy-qt that connection object paths and services are not properly unregistered // and thus if we gain the same memory address (and thus the same pointer) every time we reconnect, there might be some problems QString uniqueName() const; Q_SIGNALS: void messageRead(const QString &messageId); void messageSent(const QString &message, const QVariantMap &info); void callReceived(const QString &callerId); void callEnded(const QString &callerId); void callStateChanged(const QString &callerId, const QString &objectPath, const QString &state); void conferenceCreated(const QString &objectPath); void channelMerged(const QString &objectPath); void channelSplitted(const QString &objectPath); void channelSplitted(const QDBusObjectPath &objectPath); public Q_SLOTS: void placeIncomingMessage(const QString &message, const QVariantMap &info); void placeDeliveryReport(const QStringList &recipients, const QString &messageId, const QString &status); void hangupCall(const QString &callerId); void setCallState(const QString &phoneNumber, const QString &state); void onTextChannelClosed(); void onCallChannelClosed(); void onCallChannelDestroyed(); void onCallStateChanged(MockCallChannel *channel, const QString &state); void onMessageRead(const QString &id); void onConferenceCallChannelClosed(); void onCallChannelSplitted(); private: void addMMSToService(const QString &path, const QVariantMap &properties, const QString &servicePath); MockTextChannel *textChannelForRecipients(const QStringList &recipients); QMap mHandles; Tp::SimpleStatusSpecMap mStatuses; QList mTextChannels; QMap mCallChannels; QMap mInitialCallStatus; QStringList mModems; Tp::SimplePresence mSelfPresence; MockConnectionDBus *mDBus; QStringList mIncomingCalls; MockConferenceCallChannel *mConferenceCall; QStringList mEmergencyNumbers; bool mVoicemailIndicator; int mVoicemailCount; QString mVoicemailNumber; }; #endif history-service-0.5/tests/common/mock/dbustypes.h000066400000000000000000000022751455450246700223150ustar00rootroot00000000000000/** * Copyright (C) 2013 Canonical, Ltd. * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU General Public License version 3, as published by * the Free Software Foundation. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, * SATISFACTORY QUALITY, 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 . * * Authors: Tiago Salem Herrmann */ #ifndef DBUSTYPES #define DBUSTYPES struct MessageStruct { QDBusObjectPath path; QVariantMap properties; }; struct AttachmentStruct { QString id; QString contentType; QString filePath; quint64 offset; quint64 length; }; typedef QList AttachmentList; Q_DECLARE_METATYPE(AttachmentStruct) Q_DECLARE_METATYPE(AttachmentList) typedef QList MessageList; Q_DECLARE_METATYPE(MessageStruct) Q_DECLARE_METATYPE(MessageList) #endif history-service-0.5/tests/common/mock/emergencymodeiface.cpp000066400000000000000000000110611455450246700244320ustar00rootroot00000000000000/** * Copyright (C) 2013-2015 Canonical, Ltd. * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU General Public License version 3, as published by * the Free Software Foundation. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, * SATISFACTORY QUALITY, 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 . * * Authors: Tiago Salem Herrmann Gustavo Pichorim Boiko */ #include #include #include #include "emergencymodeiface.h" BaseConnectionEmergencyModeInterface::Adaptee::Adaptee(BaseConnectionEmergencyModeInterface *interface) : QObject(interface), mInterface(interface) { } struct TP_QT_NO_EXPORT BaseConnectionEmergencyModeInterface::Private { Private(BaseConnectionEmergencyModeInterface *parent) : adaptee(new BaseConnectionEmergencyModeInterface::Adaptee(parent)) { } EmergencyNumbersCallback emergencyNumbersCB; BaseConnectionEmergencyModeInterface::Adaptee *adaptee; QString fakeEmergencyNumber; }; BaseConnectionEmergencyModeInterface::Adaptee::~Adaptee() { } void BaseConnectionEmergencyModeInterface::Adaptee::emergencyNumbers(const ConnectionInterfaceEmergencyModeAdaptor::EmergencyNumbersContextPtr &context) { if (!mInterface->mPriv->emergencyNumbersCB.isValid()) { context->setFinishedWithError(TP_QT_ERROR_NOT_IMPLEMENTED, QLatin1String("Not implemented")); return; } Tp::DBusError error; QStringList numbers = mInterface->mPriv->emergencyNumbersCB(&error); if (error.isValid()) { context->setFinishedWithError(error.name(), error.message()); return; } if (mInterface->mPriv->fakeEmergencyNumber.isEmpty()) { context->setFinished(numbers); } else { context->setFinished(QStringList() << numbers << mInterface->mPriv->fakeEmergencyNumber); } } BaseConnectionEmergencyModeInterface::BaseConnectionEmergencyModeInterface() : AbstractConnectionInterface(TP_QT_IFACE_CONNECTION_EMERGENCYMODE), mPriv(new Private(this)) { } BaseConnectionEmergencyModeInterface::~BaseConnectionEmergencyModeInterface() { delete mPriv; } void BaseConnectionEmergencyModeInterface::setEmergencyNumbersCallback(const EmergencyNumbersCallback &cb) { mPriv->emergencyNumbersCB = cb; } void BaseConnectionEmergencyModeInterface::setEmergencyNumbers(const QStringList &numbers) { QStringList finalEmergencyList(numbers); if (!mPriv->fakeEmergencyNumber.isEmpty()) { finalEmergencyList << mPriv->fakeEmergencyNumber; } Q_EMIT mPriv->adaptee->emergencyNumbersChanged(finalEmergencyList); } void BaseConnectionEmergencyModeInterface::setFakeEmergencyNumber(const QString &fakeEmergencyNumber) { mPriv->fakeEmergencyNumber = fakeEmergencyNumber; } QVariantMap BaseConnectionEmergencyModeInterface::immutableProperties() const { QVariantMap map; return map; } void BaseConnectionEmergencyModeInterface::createAdaptor() { (void) new ConnectionInterfaceEmergencyModeAdaptor(dbusObject()->dbusConnection(), mPriv->adaptee, dbusObject()); } ConnectionInterfaceEmergencyModeAdaptor::ConnectionInterfaceEmergencyModeAdaptor(const QDBusConnection& bus, QObject* adaptee, QObject* parent) : Tp::AbstractAdaptor(bus, adaptee, parent) { connect(adaptee, SIGNAL(emergencyNumbersChanged(QStringList)), SIGNAL(EmergencyNumbersChanged(QStringList))); } ConnectionInterfaceEmergencyModeAdaptor::~ConnectionInterfaceEmergencyModeAdaptor() { } QStringList ConnectionInterfaceEmergencyModeAdaptor::EmergencyNumbers(const QDBusMessage& dbusMessage) { if (!adaptee()->metaObject()->indexOfMethod("emergencyNumbers(ConnectionInterfaceEmergencyModeAdaptor::EmergencyNumbersContextPtr)")) { dbusConnection().send(dbusMessage.createErrorReply(TP_QT_ERROR_NOT_IMPLEMENTED, QLatin1String("Not implemented"))); return QStringList(); } EmergencyNumbersContextPtr ctx = EmergencyNumbersContextPtr( new Tp::MethodInvocationContext< QStringList >(dbusConnection(), dbusMessage)); QMetaObject::invokeMethod(adaptee(), "emergencyNumbers", Q_ARG(ConnectionInterfaceEmergencyModeAdaptor::EmergencyNumbersContextPtr, ctx)); return QStringList(); } history-service-0.5/tests/common/mock/emergencymodeiface.h000066400000000000000000000076571455450246700241170ustar00rootroot00000000000000/** * Copyright (C) 2013-2015 Canonical, Ltd. * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU General Public License version 3, as published by * the Free Software Foundation. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, * SATISFACTORY QUALITY, 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 . * * Authors: Tiago Salem Herrmann Gustavo Pichorim Boiko */ #ifndef OFONOEMERGENCYMODEIFACE_H #define OFONOEMERGENCYMODEIFACE_H // telepathy-qt #include #include #include #include #include class BaseConnectionEmergencyModeInterface; typedef Tp::SharedPtr BaseConnectionEmergencyModeInterfacePtr; #define TP_QT_IFACE_CONNECTION_EMERGENCYMODE "com.lomiri.Telephony.EmergencyMode" class TP_QT_EXPORT BaseConnectionEmergencyModeInterface : public Tp::AbstractConnectionInterface { Q_OBJECT Q_DISABLE_COPY(BaseConnectionEmergencyModeInterface) public: static BaseConnectionEmergencyModeInterfacePtr create() { return BaseConnectionEmergencyModeInterfacePtr(new BaseConnectionEmergencyModeInterface()); } template static Tp::SharedPtr create() { return Tp::SharedPtr( new BaseConnectionEmergencyModeInterfaceSubclass()); } QVariantMap immutableProperties() const; virtual ~BaseConnectionEmergencyModeInterface(); typedef Tp::Callback1 EmergencyNumbersCallback; void setEmergencyNumbersCallback(const EmergencyNumbersCallback &cb); void setFakeEmergencyNumber(const QString &fakeEmergencyNumber); public Q_SLOTS: void setEmergencyNumbers(const QStringList &numbers); protected: BaseConnectionEmergencyModeInterface(); private: void createAdaptor(); class Adaptee; friend class Adaptee; struct Private; friend struct Private; Private *mPriv; }; class TP_QT_EXPORT ConnectionInterfaceEmergencyModeAdaptor : public Tp::AbstractAdaptor { Q_OBJECT Q_CLASSINFO("D-Bus Interface", TP_QT_IFACE_CONNECTION_EMERGENCYMODE) Q_CLASSINFO("D-Bus Introspection", "" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" "") public: ConnectionInterfaceEmergencyModeAdaptor(const QDBusConnection& dbusConnection, QObject* adaptee, QObject* parent); virtual ~ConnectionInterfaceEmergencyModeAdaptor(); typedef Tp::MethodInvocationContextPtr< QStringList > EmergencyNumbersContextPtr; public Q_SLOTS: // METHODS QStringList EmergencyNumbers(const QDBusMessage& dbusMessage); Q_SIGNALS: // SIGNALS void EmergencyNumbersChanged(const QStringList &numbers); }; class TP_QT_NO_EXPORT BaseConnectionEmergencyModeInterface::Adaptee : public QObject { Q_OBJECT public: Adaptee(BaseConnectionEmergencyModeInterface *interface); ~Adaptee(); private Q_SLOTS: void emergencyNumbers(const ConnectionInterfaceEmergencyModeAdaptor::EmergencyNumbersContextPtr &context); Q_SIGNALS: void emergencyNumbersChanged(const QStringList &numbers); public: BaseConnectionEmergencyModeInterface *mInterface; }; #endif history-service-0.5/tests/common/mock/main.cpp000066400000000000000000000031041455450246700215420ustar00rootroot00000000000000/** * Copyright (C) 2013 Canonical, Ltd. * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU General Public License version 3, as published by * the Free Software Foundation. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, * SATISFACTORY QUALITY, 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 . * * Authors: Tiago Salem Herrmann */ #include #include #include #include "protocol.h" int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); Tp::registerTypes(); Tp::enableDebug(true); Tp::enableWarnings(true); // create a standard protocol Tp::BaseProtocolPtr proto = Tp::BaseProtocol::create( QDBusConnection::sessionBus(), QLatin1String("mock")); // create a phone protocol Tp::BaseProtocolPtr phoneProto = Tp::BaseProtocol::create( QDBusConnection::sessionBus(), QLatin1String("ofono")); Tp::BaseConnectionManagerPtr cm = Tp::BaseConnectionManager::create( QDBusConnection::sessionBus(), QLatin1String("mock")); cm->addProtocol(phoneProto); cm->addProtocol(proto); cm->registerObject(); return a.exec(); } history-service-0.5/tests/common/mock/mockconnectiondbus.cpp000066400000000000000000000103351455450246700245110ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 "connection.h" #include "mockconnectiondbus.h" #include "mockconnectionadaptor.h" Q_DECLARE_METATYPE(QList< QVariantMap >) MockConnectionDBus::MockConnectionDBus(MockConnection *parent) : QObject(parent), mAdaptor(0), mConnection(parent) { connect(mConnection, SIGNAL(messageRead(QString)), SIGNAL(MessageRead(QString))); connect(mConnection, SIGNAL(messageSent(QString,QVariantMap)), SIGNAL(MessageSent(QString,QVariantMap))); connect(mConnection, SIGNAL(callReceived(QString)), SIGNAL(CallReceived(QString))); connect(mConnection, SIGNAL(callEnded(QString)), SIGNAL(CallEnded(QString))); connect(mConnection, SIGNAL(callStateChanged(QString,QString,QString)), SIGNAL(CallStateChanged(QString,QString,QString))); connect(mConnection, SIGNAL(conferenceCreated(QString)), SIGNAL(ConferenceCreated(QString))); connect(mConnection, SIGNAL(channelMerged(QString)), SIGNAL(ChannelMerged(QString))); connect(mConnection, SIGNAL(channelSplitted(QString)), SIGNAL(ChannelSplitted(QString))); connect(mConnection, SIGNAL(disconnected()), SIGNAL(Disconnected())); connect(mConnection, SIGNAL(destroyed()), SIGNAL(Destroyed())); qDBusRegisterMetaType >(); mObjectPath = "/com/lomiri/MockConnection/" + mConnection->protocolName(); connectToBus(); } MockConnectionDBus::~MockConnectionDBus() { QDBusConnection::sessionBus().unregisterObject(mObjectPath, QDBusConnection::UnregisterTree); } bool MockConnectionDBus::connectToBus() { if (!mAdaptor) { mAdaptor = new MockConnectionAdaptor(this); } if (!QDBusConnection::sessionBus().registerObject(mObjectPath, this)) { return false; } return QDBusConnection::sessionBus().registerService("com.lomiri.MockConnection"); } void MockConnectionDBus::PlaceIncomingMessage(const QString &message, const QVariantMap &properties) { mConnection->placeIncomingMessage(message, properties); } void MockConnectionDBus::PlaceDeliveryReport(const QStringList &recipients, const QString &messageId, const QString &status) { mConnection->placeDeliveryReport(recipients, messageId, status); } QString MockConnectionDBus::PlaceCall(const QVariantMap &properties) { return mConnection->placeCall(properties); } void MockConnectionDBus::HangupCall(const QString &callerId) { mConnection->hangupCall(callerId); } void MockConnectionDBus::SetCallState(const QString &phoneNumber, const QString &state) { mConnection->setCallState(phoneNumber, state); } void MockConnectionDBus::SetOnline(bool online) { mConnection->setOnline(online); } void MockConnectionDBus::SetPresence(const QString &status, const QString &statusMessage) { Tp::DBusError error; mConnection->setPresence(status, statusMessage, &error); } void MockConnectionDBus::SetVoicemailIndicator(bool active) { mConnection->setVoicemailIndicator(active); } void MockConnectionDBus::SetVoicemailNumber(const QString &number) { mConnection->setVoicemailNumber(number); } void MockConnectionDBus::SetVoicemailCount(int count) { mConnection->setVoicemailCount(count); } void MockConnectionDBus::SetEmergencyNumbers(const QStringList &numbers) { mConnection->setEmergencyNumbers(numbers); } QString MockConnectionDBus::Serial() { return mConnection->serial(); } history-service-0.5/tests/common/mock/mockconnectiondbus.h000066400000000000000000000050131455450246700241530ustar00rootroot00000000000000/** * Copyright (C) 2013 Canonical, Ltd. * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU General Public License version 3, as published by * the Free Software Foundation. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, * SATISFACTORY QUALITY, 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 . * * Authors: Gustavo Pichorim Boiko */ #ifndef MOCKCONNECTIONDBUS_H #define MOCKCONNECTIONDBUS_H #include #include class MockConnectionAdaptor; class MockConnection; class MockConnectionDBus : public QObject, public QDBusContext { Q_OBJECT public: explicit MockConnectionDBus(MockConnection *parent); ~MockConnectionDBus(); bool connectToBus(); void PlaceIncomingMessage(const QString &message, const QVariantMap &properties); void PlaceDeliveryReport(const QStringList &recipients, const QString &messageId, const QString &status); QString PlaceCall(const QVariantMap &properties); void HangupCall(const QString &callerId); void SetCallState(const QString &phoneNumber, const QString &state); void SetOnline(bool online); void SetPresence(const QString &status, const QString &statusMessage); // voicemail stuff void SetVoicemailIndicator(bool active); void SetVoicemailNumber(const QString &number); void SetVoicemailCount(int count); // emergency numbers stuff void SetEmergencyNumbers(const QStringList &numbers); // USSD stuff QString Serial(); Q_SIGNALS: // signals that will be relayed into the bus void MessageRead(const QString &messageId); void MessageSent(const QString &mesasge, const QVariantMap &properties); void CallReceived(const QString &callerId); void CallEnded(const QString &callerId); void CallStateChanged(const QString &callerId, const QString &objectPath, const QString &state); void ConferenceCreated(const QString &objectPath); void ChannelMerged(const QString &objectPath); void ChannelSplitted(const QString &objectPath); void Disconnected(); void Destroyed(); private: MockConnectionAdaptor *mAdaptor; MockConnection *mConnection; QString mObjectPath; }; #endif // MOCKCONNECTIONDBUS_H history-service-0.5/tests/common/mock/protocol.cpp000066400000000000000000000037511455450246700224670ustar00rootroot00000000000000/** * Copyright (C) 2013 Canonical, Ltd. * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU General Public License version 3, as published by * the Free Software Foundation. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, * SATISFACTORY QUALITY, 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 . * * Authors: Tiago Salem Herrmann */ #include "protocol.h" #include "connection.h" #include #include Protocol::Protocol(const QDBusConnection &dbusConnection, const QString &name) : Tp::BaseProtocol(dbusConnection, name) { setRequestableChannelClasses(Tp::RequestableChannelClassSpecList() << Tp::RequestableChannelClassSpec::textChat() << Tp::RequestableChannelClassSpec::audioCall()); setCreateConnectionCallback(memFun(this, &Protocol::createConnection)); Tp::ProtocolParameterList parameters; Tp::ProtocolParameter parameter("modem-objpath", "s", 0); parameters << parameter; setParameters(parameters); addressingIface = Tp::BaseProtocolAddressingInterface::create(); if (name == "ofono") { addressingIface->setAddressableVCardFields(QStringList() << "tel"); } else { addressingIface->setAddressableVCardFields(QStringList() << "x-mock-im" << "x-sip"); } plugInterface(addressingIface); } Tp::BaseConnectionPtr Protocol::createConnection(const QVariantMap ¶meters, Tp::DBusError *error) { Q_UNUSED(error); return Tp::BaseConnection::create("mock", name().toLatin1(), parameters); } history-service-0.5/tests/common/mock/protocol.h000066400000000000000000000022551455450246700221320ustar00rootroot00000000000000/** * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Tiago Salem Herrmann * * This file is part of telepathy-ofono. * * telepathy-ofono is free software; you can redistribute it and/or modify * it under the terms of the GNU LESSER General Public License as published by * the Free Software Foundation; version 3. * * telepathy-ofono 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 LESSER General Public License * along with this program. If not, see . */ #ifndef MOCKPROTOCOL_H #define MOCKPROTOCOL_H #include class Protocol : public Tp::BaseProtocol { Q_OBJECT Q_DISABLE_COPY(Protocol) public: Protocol(const QDBusConnection &dbusConnection, const QString &name); Tp::BaseProtocolAddressingInterfacePtr addressingIface; private: Tp::BaseConnectionPtr createConnection(const QVariantMap ¶meters, Tp::DBusError *error); }; #endif history-service-0.5/tests/common/mock/speakeriface.cpp000066400000000000000000000076761455450246700232620ustar00rootroot00000000000000/** * Copyright (C) 2013 Canonical, Ltd. * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License version 3, as published by * the Free Software Foundation. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . * * Authors: Tiago Salem Herrmann */ #include #include #include #include "speakeriface.h" // Chan.I.Speaker BaseChannelSpeakerInterface::Adaptee::Adaptee(BaseChannelSpeakerInterface *interface) : QObject(interface), mInterface(interface) { } struct TP_QT_NO_EXPORT BaseChannelSpeakerInterface::Private { Private(BaseChannelSpeakerInterface *parent) : speakerMode(false), adaptee(new BaseChannelSpeakerInterface::Adaptee(parent)) { } bool speakerMode; turnOnSpeakerCallback turnOnSpeakerCB; BaseChannelSpeakerInterface::Adaptee *adaptee; }; BaseChannelSpeakerInterface::Adaptee::~Adaptee() { } void BaseChannelSpeakerInterface::Adaptee::turnOnSpeaker(bool active, const ChannelInterfaceSpeakerAdaptor::turnOnSpeakerContextPtr &context) { if (!mInterface->mPriv->turnOnSpeakerCB.isValid()) { context->setFinishedWithError(TP_QT_ERROR_NOT_IMPLEMENTED, QLatin1String("Not implemented")); return; } Tp::DBusError error; mInterface->mPriv->turnOnSpeakerCB(active, &error); if (error.isValid()) { context->setFinishedWithError(error.name(), error.message()); return; } context->setFinished(); } BaseChannelSpeakerInterface::BaseChannelSpeakerInterface() : AbstractChannelInterface(TP_QT_IFACE_CHANNEL_SPEAKER), mPriv(new Private(this)) { } BaseChannelSpeakerInterface::~BaseChannelSpeakerInterface() { delete mPriv; } bool BaseChannelSpeakerInterface::speakerMode() const { return mPriv->speakerMode; } void BaseChannelSpeakerInterface::setTurnOnSpeakerCallback(const turnOnSpeakerCallback &cb) { mPriv->turnOnSpeakerCB = cb; } void BaseChannelSpeakerInterface::setSpeakerMode(bool active) { mPriv->speakerMode = active; Q_EMIT mPriv->adaptee->speakerChanged(active); } QVariantMap BaseChannelSpeakerInterface::immutableProperties() const { QVariantMap map; return map; } void BaseChannelSpeakerInterface::createAdaptor() { (void) new ChannelInterfaceSpeakerAdaptor(dbusObject()->dbusConnection(), mPriv->adaptee, dbusObject()); } ChannelInterfaceSpeakerAdaptor::ChannelInterfaceSpeakerAdaptor(const QDBusConnection& bus, QObject* adaptee, QObject* parent) : Tp::AbstractAdaptor(bus, adaptee, parent) { connect(adaptee, SIGNAL(speakerChanged(bool)), SIGNAL(SpeakerChanged(bool))); } ChannelInterfaceSpeakerAdaptor::~ChannelInterfaceSpeakerAdaptor() { } void ChannelInterfaceSpeakerAdaptor::turnOnSpeaker(bool active, const QDBusMessage& dbusMessage) { if (!adaptee()->metaObject()->indexOfMethod("turnOnSpeaker(bool,ChannelInterfaceSpeakerAdaptor::turnOnSpeakerContextPtr)")) { dbusConnection().send(dbusMessage.createErrorReply(TP_QT_ERROR_NOT_IMPLEMENTED, QLatin1String("Not implemented"))); return; } turnOnSpeakerContextPtr ctx = turnOnSpeakerContextPtr( new Tp::MethodInvocationContext< bool >(dbusConnection(), dbusMessage)); QMetaObject::invokeMethod(adaptee(), "turnOnSpeaker", Q_ARG(bool, active), Q_ARG(ChannelInterfaceSpeakerAdaptor::turnOnSpeakerContextPtr, ctx)); return; } bool ChannelInterfaceSpeakerAdaptor::SpeakerMode() const { return qvariant_cast< bool >(adaptee()->property("speakerMode")); } history-service-0.5/tests/common/mock/speakeriface.h000066400000000000000000000075411455450246700227160ustar00rootroot00000000000000/** * Copyright (C) 2013-2015 Canonical, Ltd. * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU General Public License version 3, as published by * the Free Software Foundation. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, * SATISFACTORY QUALITY, 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 . * * Authors: Tiago Salem Herrmann Gustavo Pichorim Boiko */ #ifndef OFONOSPEAKERIFACE_H #define OFONOSPEAKERIFACE_H // telepathy-qt #include #include #include #include #include #define TP_QT_IFACE_CHANNEL_SPEAKER "com.lomiri.Telephony.Speaker" class BaseChannelSpeakerInterface; typedef Tp::SharedPtr BaseChannelSpeakerInterfacePtr; class TP_QT_EXPORT BaseChannelSpeakerInterface : public Tp::AbstractChannelInterface { Q_OBJECT Q_DISABLE_COPY(BaseChannelSpeakerInterface) public: static BaseChannelSpeakerInterfacePtr create() { return BaseChannelSpeakerInterfacePtr(new BaseChannelSpeakerInterface()); } template static Tp::SharedPtr create() { return Tp::SharedPtr( new BaseChannelSpeakerInterfaceSubclass()); } QVariantMap immutableProperties() const; virtual ~BaseChannelSpeakerInterface(); bool speakerMode() const; typedef Tp::Callback2 turnOnSpeakerCallback; void setTurnOnSpeakerCallback(const turnOnSpeakerCallback &cb); public Q_SLOTS: void setSpeakerMode(bool active); protected: BaseChannelSpeakerInterface(); private: void createAdaptor(); class Adaptee; friend class Adaptee; struct Private; friend struct Private; Private *mPriv; }; class TP_QT_EXPORT ChannelInterfaceSpeakerAdaptor : public Tp::AbstractAdaptor { Q_OBJECT Q_CLASSINFO("D-Bus Interface", TP_QT_IFACE_CHANNEL_SPEAKER) Q_CLASSINFO("D-Bus Introspection", "" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" "") Q_PROPERTY(bool SpeakerMode READ SpeakerMode) public: ChannelInterfaceSpeakerAdaptor(const QDBusConnection& dbusConnection, QObject* adaptee, QObject* parent); virtual ~ChannelInterfaceSpeakerAdaptor(); typedef Tp::MethodInvocationContextPtr< bool > turnOnSpeakerContextPtr; public: // PROPERTIES bool SpeakerMode() const; public Q_SLOTS: // METHODS void turnOnSpeaker(bool active, const QDBusMessage& dbusMessage); Q_SIGNALS: // SIGNALS void SpeakerChanged(bool active); }; class TP_QT_NO_EXPORT BaseChannelSpeakerInterface::Adaptee : public QObject { Q_OBJECT Q_PROPERTY(bool speakerMode READ speakerMode) public: Adaptee(BaseChannelSpeakerInterface *interface); ~Adaptee(); bool speakerMode() const { return mInterface->speakerMode(); } private Q_SLOTS: void turnOnSpeaker(bool active, const ChannelInterfaceSpeakerAdaptor::turnOnSpeakerContextPtr &context); Q_SIGNALS: void speakerChanged(bool active); public: BaseChannelSpeakerInterface *mInterface; }; #endif history-service-0.5/tests/common/mock/textchannel.cpp000066400000000000000000000245311455450246700231420ustar00rootroot00000000000000/** * Copyright (C) 2013 Canonical, Ltd. * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU General Public License version 3, as published by * the Free Software Foundation. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, * SATISFACTORY QUALITY, 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 . * * Authors: Tiago Salem Herrmann * Gustavo Pichorim Boiko */ // telepathy-ofono #include "textchannel.h" QDBusArgument &operator<<(QDBusArgument &argument, const AttachmentStruct &attachment) { argument.beginStructure(); argument << attachment.id << attachment.contentType << attachment.filePath << attachment.offset << attachment.length; argument.endStructure(); return argument; } const QDBusArgument &operator>>(const QDBusArgument &argument, AttachmentStruct &attachment) { argument.beginStructure(); argument >> attachment.id >> attachment.contentType >> attachment.filePath >> attachment.offset >> attachment.length; argument.endStructure(); return argument; } MockTextChannel::MockTextChannel(MockConnection *conn, QStringList recipients, uint targetHandle, QObject *parent): QObject(parent), mConnection(conn), mRecipients(recipients), mTargetHandle(targetHandle), mMessageCounter(1) { qDBusRegisterMetaType(); qDBusRegisterMetaType(); Tp::HandleType type = recipients.count() > 1 ? Tp::HandleTypeNone : Tp::HandleTypeContact; Tp::BaseChannelPtr baseChannel = Tp::BaseChannel::create(mConnection, TP_QT_IFACE_CHANNEL_TYPE_TEXT, type, targetHandle); mBaseChannel = baseChannel; Tp::BaseChannelTextTypePtr textType = Tp::BaseChannelTextType::create(baseChannel.data()); baseChannel->plugInterface(Tp::AbstractChannelInterfacePtr::dynamicCast(textType)); QStringList supportedContentTypes = QStringList() << "text/plain"; Tp::UIntList messageTypes = Tp::UIntList() << Tp::ChannelTextMessageTypeNormal << Tp::ChannelTextMessageTypeDeliveryReport; uint messagePartSupportFlags = 0; uint deliveryReportingSupport = Tp::DeliveryReportingSupportFlagReceiveSuccesses; mMessagesIface = Tp::BaseChannelMessagesInterface::create(textType.data(), supportedContentTypes, messageTypes, messagePartSupportFlags, deliveryReportingSupport); mMessagesIface->setSendMessageCallback(Tp::memFun(this,&MockTextChannel::sendMessage)); baseChannel->plugInterface(Tp::AbstractChannelInterfacePtr::dynamicCast(mMessagesIface)); // group stuff mGroupIface = Tp::BaseChannelGroupInterface::create(); mGroupIface->setGroupFlags(Tp::ChannelGroupFlagCanAdd); mGroupIface->setSelfHandle(conn->selfHandle()); mGroupIface->setAddMembersCallback(Tp::memFun(this,&MockTextChannel::onAddMembers)); mGroupIface->setRemoveMembersCallback(Tp::memFun(this,&MockTextChannel::onRemoveMembers)); baseChannel->plugInterface(Tp::AbstractChannelInterfacePtr::dynamicCast(mGroupIface)); addMembers(recipients); mTextChannel = Tp::BaseChannelTextTypePtr::dynamicCast(mBaseChannel->interface(TP_QT_IFACE_CHANNEL_TYPE_TEXT)); mTextChannel->setMessageAcknowledgedCallback(Tp::memFun(this,&MockTextChannel::messageAcknowledged)); QObject::connect(mBaseChannel.data(), SIGNAL(closed()), this, SLOT(deleteLater())); } MockTextChannel::~MockTextChannel() { } Tp::BaseChannelPtr MockTextChannel::baseChannel() { return mBaseChannel; } void MockTextChannel::messageAcknowledged(const QString &id) { Q_EMIT messageRead(id); } QString MockTextChannel::sendMessage(const Tp::MessagePartList& message, uint /* flags */, Tp::DBusError* /* error */) { Tp::MessagePart header = message.at(0); Tp::MessagePart body = message.at(1); static int serial = 0; // FIXME: check what other data we need to emit in the signal QString id; if (!header["no-event-id"].variant().toBool()) { id = QString("sentmessage%1").arg(serial++); } QString messageText = body["content"].variant().toString(); QVariantMap properties; properties["SentTime"] = QDateTime::currentDateTime().toString(Qt::ISODate); properties["Recipients"] = mRecipients; properties["Id"] = id; Q_EMIT messageSent(messageText, properties); return id; } void MockTextChannel::placeDeliveryReport(const QString &messageId, const QString &status) { Tp::DeliveryStatus delivery_status; if (status == "sent" || status == "delivered") { delivery_status = Tp::DeliveryStatusDelivered; } else if(status == "failed" || status == "permanently_failed") { delivery_status = Tp::DeliveryStatusPermanentlyFailed; } else if (status == "temporarily_failed") { delivery_status = Tp::DeliveryStatusTemporarilyFailed; } else if(status == "pending" || status == "accepted") { delivery_status = Tp::DeliveryStatusAccepted; } else if (status == "read") { delivery_status = Tp::DeliveryStatusRead; } else if (status == "deleted") { delivery_status = Tp::DeliveryStatusDeleted; } else { delivery_status = Tp::DeliveryStatusUnknown; } Tp::MessagePartList partList; Tp::MessagePart header; header["message-sender"] = QDBusVariant(mTargetHandle); // FIXME: fix it header["message-sender-id"] = QDBusVariant(mRecipients.first()); header["message-type"] = QDBusVariant(Tp::ChannelTextMessageTypeDeliveryReport); header["delivery-status"] = QDBusVariant(delivery_status); header["delivery-token"] = QDBusVariant(messageId); partList << header; mTextChannel->addReceivedMessage(partList); } void MockTextChannel::messageReceived(const QString &message, const QVariantMap &info) { Tp::MessagePartList partList; Tp::MessagePart body; body["content-type"] = QDBusVariant("text/plain"); body["content"] = QDBusVariant(message); Tp::MessagePart header; header["message-token"] = QDBusVariant(info["SentTime"].toString() +"-" + QString::number(mMessageCounter++)); header["message-received"] = QDBusVariant(QDateTime::currentDateTime().toTime_t()); header["message-sent"] = QDBusVariant(QDateTime::fromString(info["SentTime"].toString(), Qt::ISODate).toTime_t()); header["message-sender"] = QDBusVariant(mTargetHandle); header["message-sender-id"] = QDBusVariant(mRecipients.first()); header["message-type"] = QDBusVariant(Tp::ChannelTextMessageTypeNormal); partList << header << body; mTextChannel->addReceivedMessage(partList); } void MockTextChannel::mmsReceived(const QString &id, const QVariantMap &properties) { Tp::MessagePartList message; QString subject = properties["Subject"].toString(); QString smil = properties["Smil"].toString(); Tp::MessagePart header; header["message-token"] = QDBusVariant(id); header["message-sender"] = QDBusVariant(mTargetHandle); header["message-received"] = QDBusVariant(QDateTime::fromString(properties["Date"].toString(), Qt::ISODate).toTime_t()); header["message-type"] = QDBusVariant(Tp::DeliveryStatusDelivered); if (!subject.isEmpty()) { header["subject"] = QDBusVariant(subject); } message << header; AttachmentList mmsdAttachments = qdbus_cast(properties["Attachments"]); Q_FOREACH(const AttachmentStruct &attachment, mmsdAttachments) { QFile attachmentFile(attachment.filePath); if (!attachmentFile.open(QIODevice::ReadOnly)) { qWarning() << "fail to load attachment" << attachmentFile.errorString() << attachment.filePath; continue; } // FIXME check if we managed to read the total attachment file attachmentFile.seek(attachment.offset); QByteArray fileData = attachmentFile.read(attachment.length); Tp::MessagePart part; part["content-type"] = QDBusVariant(attachment.contentType); part["identifier"] = QDBusVariant(attachment.id); part["content"] = QDBusVariant(fileData); part["size"] = QDBusVariant(attachment.length); message << part; } if (!smil.isEmpty()) { Tp::MessagePart part; part["content-type"] = QDBusVariant(QString("application/smil")); part["identifier"] = QDBusVariant(QString("smil")); part["content"] = QDBusVariant(smil); part["size"] = QDBusVariant(smil.size()); message << part; } mTextChannel->addReceivedMessage(message); } void MockTextChannel::addMembers(QStringList recipients) { Tp::UIntList handles; Q_FOREACH(const QString &recipient, recipients) { uint handle = mConnection->ensureHandle(recipient); handles << handle; if (!mRecipients.contains(recipient)) { mRecipients << recipient; } if (!mMembers.contains(handle)) { mMembers << handle; } } mGroupIface->setMembers(mMembers, QVariantMap()); } QStringList MockTextChannel::recipients() const { return mRecipients; } Tp::UIntList MockTextChannel::members() { return mMembers; } void MockTextChannel::onAddMembers(const Tp::UIntList &handles, const QString& /* message */, Tp::DBusError *error) { addMembers(mConnection->inspectHandles(Tp::HandleTypeContact, handles, error)); } void MockTextChannel::onRemoveMembers(const Tp::UIntList &handles, const QString& /* message */, uint /* reason */, Tp::DBusError *error) { Q_FOREACH(uint handle, handles) { Q_FOREACH(const QString &recipient, mConnection->inspectHandles(Tp::HandleTypeContact, Tp::UIntList() << handle, error)) { mRecipients.removeAll(recipient); } mMembers.removeAll(handle); } mGroupIface->setMembers(mMembers, QVariantMap()); } history-service-0.5/tests/common/mock/textchannel.h000066400000000000000000000046531455450246700226120ustar00rootroot00000000000000/** * Copyright (C) 2013 Canonical, Ltd. * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU General Public License version 3, as published by * the Free Software Foundation. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, * SATISFACTORY QUALITY, 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 . * * Authors: Tiago Salem Herrmann * Gustavo Pichorim Boiko */ #ifndef MOCKTEXTCHANNEL_H #define MOCKTEXTCHANNEL_H #include #include #include #include #include #include "connection.h" class MockConnection; class MockTextChannel : public QObject { Q_OBJECT public: MockTextChannel(MockConnection *conn, QStringList recipients, uint targetHandle, QObject *parent = 0); QString sendMessage(const Tp::MessagePartList& message, uint flags, Tp::DBusError* error); void messageReceived(const QString & message, const QVariantMap &info); Tp::BaseChannelPtr baseChannel(); void messageAcknowledged(const QString &id); void mmsReceived(const QString &id, const QVariantMap &properties); void addMembers(QStringList recipients); QStringList recipients() const; Tp::UIntList members(); void onRemoveMembers(const Tp::UIntList& handles, const QString& message, uint reason, Tp::DBusError* error); void onAddMembers(const Tp::UIntList& handles, const QString& message, Tp::DBusError* error); public Q_SLOTS: void placeDeliveryReport(const QString &messageId, const QString &status); Q_SIGNALS: void messageRead(const QString &id); void messageSent(const QString &message, const QVariantMap &info); private: ~MockTextChannel(); MockConnection *mConnection; QStringList mRecipients; uint mTargetHandle; uint mMessageCounter; Tp::BaseChannelPtr mBaseChannel; Tp::BaseChannelMessagesInterfacePtr mMessagesIface; Tp::BaseChannelGroupInterfacePtr mGroupIface; Tp::BaseChannelTextTypePtr mTextChannel; Tp::UIntList mMembers; }; #endif // MOCKTEXTCHANNEL_H history-service-0.5/tests/common/mock/ussdiface.cpp000066400000000000000000000247511455450246700225770ustar00rootroot00000000000000/** * Copyright (C) 2013-2015 Canonical, Ltd. * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU General Public License version 3, as published by * the Free Software Foundation. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, * SATISFACTORY QUALITY, 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 . * * Authors: Tiago Salem Herrmann Gustavo Pichorim Boiko */ #include #include #include #include "ussdiface.h" // Conn.I.USSD BaseConnectionUSSDInterface::Adaptee::Adaptee(BaseConnectionUSSDInterface *interface) : QObject(interface), mInterface(interface) { } struct TP_QT_NO_EXPORT BaseConnectionUSSDInterface::Private { Private(BaseConnectionUSSDInterface *parent) : adaptee(new BaseConnectionUSSDInterface::Adaptee(parent)) { } QString state; QString serial; InitiateCallback initiateCB; RespondCallback respondCB; CancelCallback cancelCB; BaseConnectionUSSDInterface::Adaptee *adaptee; }; BaseConnectionUSSDInterface::Adaptee::~Adaptee() { } void BaseConnectionUSSDInterface::Adaptee::initiate(const QString &command, const ConnectionInterfaceUSSDAdaptor::InitiateContextPtr &context) { if (!mInterface->mPriv->initiateCB.isValid()) { context->setFinishedWithError(TP_QT_ERROR_NOT_IMPLEMENTED, QLatin1String("Not implemented")); return; } Tp::DBusError error; mInterface->mPriv->initiateCB(command, &error); if (error.isValid()) { context->setFinishedWithError(error.name(), error.message()); return; } context->setFinished(); } void BaseConnectionUSSDInterface::Adaptee::respond(const QString &reply, const ConnectionInterfaceUSSDAdaptor::RespondContextPtr &context) { if (!mInterface->mPriv->respondCB.isValid()) { context->setFinishedWithError(TP_QT_ERROR_NOT_IMPLEMENTED, QLatin1String("Not implemented")); return; } Tp::DBusError error; mInterface->mPriv->respondCB(reply, &error); if (error.isValid()) { context->setFinishedWithError(error.name(), error.message()); return; } context->setFinished(); } void BaseConnectionUSSDInterface::Adaptee::cancel(const ConnectionInterfaceUSSDAdaptor::CancelContextPtr &context) { if (!mInterface->mPriv->cancelCB.isValid()) { context->setFinishedWithError(TP_QT_ERROR_NOT_IMPLEMENTED, QLatin1String("Not implemented")); return; } Tp::DBusError error; mInterface->mPriv->cancelCB(&error); if (error.isValid()) { context->setFinishedWithError(error.name(), error.message()); return; } context->setFinished(); } BaseConnectionUSSDInterface::BaseConnectionUSSDInterface() : AbstractConnectionInterface(TP_QT_IFACE_CONNECTION_USSD), mPriv(new Private(this)) { } BaseConnectionUSSDInterface::~BaseConnectionUSSDInterface() { delete mPriv; } void BaseConnectionUSSDInterface::setInitiateCallback(const InitiateCallback &cb) { mPriv->initiateCB = cb; } void BaseConnectionUSSDInterface::setRespondCallback(const RespondCallback &cb) { mPriv->respondCB = cb; } void BaseConnectionUSSDInterface::setCancelCallback(const CancelCallback &cb) { mPriv->cancelCB = cb; } QString BaseConnectionUSSDInterface::state() const { return mPriv->state; } void BaseConnectionUSSDInterface::setSerial(const QString &serial) const { mPriv->serial = serial; } QString BaseConnectionUSSDInterface::serial() const { return mPriv->serial; } void BaseConnectionUSSDInterface::StateChanged(const QString &state) { mPriv->state = state; Q_EMIT mPriv->adaptee->stateChanged(state); } void BaseConnectionUSSDInterface::InitiateUSSDComplete(const QString &ussdResp) { Q_EMIT mPriv->adaptee->initiateUSSDComplete(ussdResp); } void BaseConnectionUSSDInterface::RespondComplete(bool success, const QString &ussdResp) { Q_EMIT mPriv->adaptee->respondComplete(success, ussdResp); } void BaseConnectionUSSDInterface::BarringComplete(const QString &ssOp, const QString &cbService, const QVariantMap &cbMap) { Q_EMIT mPriv->adaptee->barringComplete(ssOp, cbService, cbMap); } void BaseConnectionUSSDInterface::ForwardingComplete(const QString &ssOp, const QString &cfService, const QVariantMap &cfMap) { Q_EMIT mPriv->adaptee->forwardingComplete(ssOp, cfService, cfMap); } void BaseConnectionUSSDInterface::WaitingComplete(const QString &ssOp, const QVariantMap &cwMap) { Q_EMIT mPriv->adaptee->waitingComplete(ssOp, cwMap); } void BaseConnectionUSSDInterface::CallingLinePresentationComplete(const QString &ssOp, const QString &status) { Q_EMIT mPriv->adaptee->callingLinePresentationComplete(ssOp, status); } void BaseConnectionUSSDInterface::ConnectedLinePresentationComplete(const QString &ssOp, const QString &status) { Q_EMIT mPriv->adaptee->connectedLinePresentationComplete(ssOp, status); } void BaseConnectionUSSDInterface::CallingLineRestrictionComplete(const QString &ssOp, const QString &status) { Q_EMIT mPriv->adaptee->callingLineRestrictionComplete(ssOp, status); } void BaseConnectionUSSDInterface::ConnectedLineRestrictionComplete(const QString &ssOp, const QString &status) { Q_EMIT mPriv->adaptee->connectedLineRestrictionComplete(ssOp, status); } void BaseConnectionUSSDInterface::InitiateFailed() { Q_EMIT mPriv->adaptee->initiateFailed(); } void BaseConnectionUSSDInterface::NotificationReceived(const QString &message) { Q_EMIT mPriv->adaptee->notificationReceived(message); } void BaseConnectionUSSDInterface::RequestReceived(const QString &message) { Q_EMIT mPriv->adaptee->requestReceived(message); } QVariantMap BaseConnectionUSSDInterface::immutableProperties() const { QVariantMap map; return map; } void BaseConnectionUSSDInterface::createAdaptor() { (void) new ConnectionInterfaceUSSDAdaptor(dbusObject()->dbusConnection(), mPriv->adaptee, dbusObject()); } ConnectionInterfaceUSSDAdaptor::ConnectionInterfaceUSSDAdaptor(const QDBusConnection& bus, QObject* adaptee, QObject* parent) : Tp::AbstractAdaptor(bus, adaptee, parent) { connect(adaptee, SIGNAL(notificationReceived(const QString &)), SIGNAL(NotificationReceived(const QString &))); connect(adaptee, SIGNAL(requestReceived(const QString &)), SIGNAL(RequestReceived(const QString &))); connect(adaptee, SIGNAL(initiateUSSDComplete(const QString &)), SIGNAL(InitiateUSSDComplete(const QString &))); connect(adaptee, SIGNAL(barringComplete(const QString &, const QString &, const QVariantMap &)), SIGNAL(BarringComplete(const QString &, const QString &, const QVariantMap &))); connect(adaptee, SIGNAL(forwardingComplete(const QString &, const QString &, const QVariantMap &)), SIGNAL(ForwardingComplete(const QString &, const QString &, const QVariantMap &))); connect(adaptee, SIGNAL(waitingComplete(const QString &, const QVariantMap &)), SIGNAL(WaitingComplete(const QString &, const QVariantMap &))); connect(adaptee, SIGNAL(callingLinePresentationComplete(const QString &, const QString &)), SIGNAL(CallingLinePresentationComplete(const QString &, const QString &))); connect(adaptee, SIGNAL(connectedLinePresentationComplete(const QString &, const QString &)), SIGNAL(ConnectedLinePresentationComplete(const QString &, const QString &))); connect(adaptee, SIGNAL(callingLineRestrictionComplete(const QString &, const QString &)), SIGNAL(CallingLineRestrictionComplete(const QString &, const QString &))); connect(adaptee, SIGNAL(connectedLineRestrictionComplete(const QString &, const QString &)), SIGNAL(ConnectedLineRestrictionComplete(const QString &, const QString &))); connect(adaptee, SIGNAL(initiateFailed()), SIGNAL(InitiateFailed())); connect(adaptee, SIGNAL(stateChanged(const QString&)), SIGNAL(StateChanged(const QString&))); connect(adaptee, SIGNAL(respondComplete(bool, const QString &)), SIGNAL(RespondComplete(bool, const QString &))); } ConnectionInterfaceUSSDAdaptor::~ConnectionInterfaceUSSDAdaptor() { } void ConnectionInterfaceUSSDAdaptor::Initiate(const QString &command, const QDBusMessage& dbusMessage) { if (!adaptee()->metaObject()->indexOfMethod("initiate(const QString &,ConnectionInterfaceUSSDAdaptor::InitiateContextPtr)")) { dbusConnection().send(dbusMessage.createErrorReply(TP_QT_ERROR_NOT_IMPLEMENTED, QLatin1String("Not implemented"))); return; } InitiateContextPtr ctx = InitiateContextPtr( new Tp::MethodInvocationContext< >(dbusConnection(), dbusMessage)); QMetaObject::invokeMethod(adaptee(), "initiate", Q_ARG(QString, command), Q_ARG(ConnectionInterfaceUSSDAdaptor::InitiateContextPtr, ctx)); return; } void ConnectionInterfaceUSSDAdaptor::Respond(const QString &reply, const QDBusMessage& dbusMessage) { if (!adaptee()->metaObject()->indexOfMethod("respond(QConnectionInterfaceUSSDAdaptor::RespondContextPtr)")) { dbusConnection().send(dbusMessage.createErrorReply(TP_QT_ERROR_NOT_IMPLEMENTED, QLatin1String("Not implemented"))); return; } RespondContextPtr ctx = RespondContextPtr( new Tp::MethodInvocationContext< >(dbusConnection(), dbusMessage)); QMetaObject::invokeMethod(adaptee(), "respond", Q_ARG(QString, reply), Q_ARG(ConnectionInterfaceUSSDAdaptor::RespondContextPtr, ctx)); return; } void ConnectionInterfaceUSSDAdaptor::Cancel(const QDBusMessage& dbusMessage) { if (!adaptee()->metaObject()->indexOfMethod("cancel(ConnectionInterfaceUSSDAdaptor::CancelContextPtr)")) { dbusConnection().send(dbusMessage.createErrorReply(TP_QT_ERROR_NOT_IMPLEMENTED, QLatin1String("Not implemented"))); return; } CancelContextPtr ctx = CancelContextPtr( new Tp::MethodInvocationContext< >(dbusConnection(), dbusMessage)); QMetaObject::invokeMethod(adaptee(), "cancel", Q_ARG(ConnectionInterfaceUSSDAdaptor::CancelContextPtr, ctx)); return; } QString ConnectionInterfaceUSSDAdaptor::Serial() const { return qvariant_cast< QString >(adaptee()->property("serial")); } QString ConnectionInterfaceUSSDAdaptor::State() const { return qvariant_cast< QString >(adaptee()->property("state")); } history-service-0.5/tests/common/mock/ussdiface.h000066400000000000000000000222611455450246700222360ustar00rootroot00000000000000/** * Copyright (C) 2013-2015 Canonical, Ltd. * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU General Public License version 3, as published by * the Free Software Foundation. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, * SATISFACTORY QUALITY, 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 . * * Authors: Tiago Salem Herrmann Gustavo Pichorim Boiko */ #ifndef OFONOUSSDIFACE_H #define OFONOUSSDIFACE_H // telepathy-qt #include #include #include #include #include class BaseConnectionUSSDInterface; typedef Tp::SharedPtr BaseConnectionUSSDInterfacePtr; #define TP_QT_IFACE_CONNECTION_USSD "com.lomiri.Telephony.USSD" class TP_QT_EXPORT BaseConnectionUSSDInterface : public Tp::AbstractConnectionInterface { Q_OBJECT Q_DISABLE_COPY(BaseConnectionUSSDInterface) public: static BaseConnectionUSSDInterfacePtr create() { return BaseConnectionUSSDInterfacePtr(new BaseConnectionUSSDInterface()); } template static Tp::SharedPtr create() { return Tp::SharedPtr( new BaseConnectionUSSDInterfaceSubclass()); } QVariantMap immutableProperties() const; virtual ~BaseConnectionUSSDInterface(); typedef Tp::Callback2 InitiateCallback; void setInitiateCallback(const InitiateCallback &cb); typedef Tp::Callback2 RespondCallback; void setRespondCallback(const RespondCallback &cb); typedef Tp::Callback1 CancelCallback; void setCancelCallback(const CancelCallback &cb); QString state() const; QString serial() const; public Q_SLOTS: void NotificationReceived(const QString &message); void RequestReceived(const QString &message); void InitiateUSSDComplete(const QString &ussdResp); void RespondComplete(bool success, const QString &ussdResp); void BarringComplete(const QString &ssOp, const QString &cbService, const QVariantMap &cbMap); void ForwardingComplete(const QString &ssOp, const QString &cfService, const QVariantMap &cfMap); void WaitingComplete(const QString &ssOp, const QVariantMap &cwMap); void CallingLinePresentationComplete(const QString &ssOp, const QString &status); void ConnectedLinePresentationComplete(const QString &ssOp, const QString &status); void CallingLineRestrictionComplete(const QString &ssOp, const QString &status); void ConnectedLineRestrictionComplete(const QString &ssOp, const QString &status); void InitiateFailed(); void StateChanged(const QString &state); void setSerial(const QString& serial) const; protected: BaseConnectionUSSDInterface(); private: void createAdaptor(); class Adaptee; friend class Adaptee; struct Private; friend struct Private; Private *mPriv; }; class TP_QT_EXPORT ConnectionInterfaceUSSDAdaptor : public Tp::AbstractAdaptor { Q_OBJECT Q_CLASSINFO("D-Bus Interface", TP_QT_IFACE_CONNECTION_USSD) Q_CLASSINFO("D-Bus Introspection", "" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" "") Q_PROPERTY(QString State READ State) Q_PROPERTY(QString Serial READ Serial) public: ConnectionInterfaceUSSDAdaptor(const QDBusConnection& dbusConnection, QObject* adaptee, QObject* parent); virtual ~ConnectionInterfaceUSSDAdaptor(); typedef Tp::MethodInvocationContextPtr< > InitiateContextPtr; typedef Tp::MethodInvocationContextPtr< > RespondContextPtr; typedef Tp::MethodInvocationContextPtr< > CancelContextPtr; public Q_SLOTS: // METHODS void Initiate(const QString &command, const QDBusMessage& dbusMessage); void Respond(const QString &reply, const QDBusMessage& dbusMessage); void Cancel(const QDBusMessage& dbusMessage); QString State() const; QString Serial() const; Q_SIGNALS: // SIGNALS void NotificationReceived(const QString &message); void RequestReceived(const QString &message); void InitiateUSSDComplete(const QString &ussdResp); void RespondComplete(bool success, const QString &ussdResp); void BarringComplete(const QString &ssOp, const QString &cbService, const QVariantMap &cbMap); void ForwardingComplete(const QString &ssOp, const QString &cfService, const QVariantMap &cfMap); void WaitingComplete(const QString &ssOp, const QVariantMap &cwMap); void CallingLinePresentationComplete(const QString &ssOp, const QString &status); void ConnectedLinePresentationComplete(const QString &ssOp, const QString &status); void CallingLineRestrictionComplete(const QString &ssOp, const QString &status); void ConnectedLineRestrictionComplete(const QString &ssOp, const QString &status); void InitiateFailed(); void StateChanged(const QString &state); }; class TP_QT_NO_EXPORT BaseConnectionUSSDInterface::Adaptee : public QObject { Q_OBJECT Q_PROPERTY(QString state READ state) Q_PROPERTY(QString serial READ serial) public: Adaptee(BaseConnectionUSSDInterface *interface); ~Adaptee(); QString state() const { return mInterface->state(); } QString serial() const { return mInterface->serial(); } private Q_SLOTS: void initiate(const QString &command, const ConnectionInterfaceUSSDAdaptor::InitiateContextPtr &context); void respond(const QString &reply, const ConnectionInterfaceUSSDAdaptor::RespondContextPtr &context); void cancel(const ConnectionInterfaceUSSDAdaptor::CancelContextPtr &context); Q_SIGNALS: void notificationReceived(const QString &message); void requestReceived(const QString &message); void initiateUSSDComplete(const QString &ussdResp); void barringComplete(const QString &ssOp, const QString &cbService, const QVariantMap &cbMap); void forwardingComplete(const QString &ssOp, const QString &cfService, const QVariantMap &cfMap); void waitingComplete(const QString &ssOp, const QVariantMap &cwMap); void callingLinePresentationComplete(const QString &ssOp, const QString &status); void connectedLinePresentationComplete(const QString &ssOp, const QString &status); void callingLineRestrictionComplete(const QString &ssOp, const QString &status); void connectedLineRestrictionComplete(const QString &ssOp, const QString &status); void initiateFailed(); void respondComplete(bool success, const QString &response); void stateChanged(const QString &state); public: BaseConnectionUSSDInterface *mInterface; }; #endif history-service-0.5/tests/common/mock/voicemailiface.cpp000066400000000000000000000163571455450246700235740ustar00rootroot00000000000000/** * Copyright (C) 2013-2015 Canonical, Ltd. * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU General Public License version 3, as published by * the Free Software Foundation. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, * SATISFACTORY QUALITY, 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 . * * Authors: Tiago Salem Herrmann Gustavo Pichorim Boiko */ #include #include #include #include "voicemailiface.h" // Conn.I.Voicemail BaseConnectionVoicemailInterface::Adaptee::Adaptee(BaseConnectionVoicemailInterface *interface) : QObject(interface), mInterface(interface) { } struct TP_QT_NO_EXPORT BaseConnectionVoicemailInterface::Private { Private(BaseConnectionVoicemailInterface *parent) : adaptee(new BaseConnectionVoicemailInterface::Adaptee(parent)) { } VoicemailCountCallback voicemailCountCB; VoicemailNumberCallback voicemailNumberCB; VoicemailIndicatorCallback voicemailIndicatorCB; BaseConnectionVoicemailInterface::Adaptee *adaptee; }; BaseConnectionVoicemailInterface::Adaptee::~Adaptee() { } void BaseConnectionVoicemailInterface::Adaptee::voicemailIndicator(const ConnectionInterfaceVoicemailAdaptor::VoicemailIndicatorContextPtr &context) { if (!mInterface->mPriv->voicemailIndicatorCB.isValid()) { context->setFinishedWithError(TP_QT_ERROR_NOT_IMPLEMENTED, QLatin1String("Not implemented")); return; } Tp::DBusError error; bool active = mInterface->mPriv->voicemailIndicatorCB(&error); if (error.isValid()) { context->setFinishedWithError(error.name(), error.message()); return; } context->setFinished(active); } void BaseConnectionVoicemailInterface::Adaptee::voicemailNumber(const ConnectionInterfaceVoicemailAdaptor::VoicemailNumberContextPtr &context) { if (!mInterface->mPriv->voicemailNumberCB.isValid()) { context->setFinishedWithError(TP_QT_ERROR_NOT_IMPLEMENTED, QLatin1String("Not implemented")); return; } Tp::DBusError error; QString number = mInterface->mPriv->voicemailNumberCB(&error); if (error.isValid()) { context->setFinishedWithError(error.name(), error.message()); return; } context->setFinished(number); } void BaseConnectionVoicemailInterface::Adaptee::voicemailCount(const ConnectionInterfaceVoicemailAdaptor::VoicemailCountContextPtr &context) { if (!mInterface->mPriv->voicemailCountCB.isValid()) { context->setFinishedWithError(TP_QT_ERROR_NOT_IMPLEMENTED, QLatin1String("Not implemented")); return; } Tp::DBusError error; uint count = mInterface->mPriv->voicemailCountCB(&error); if (error.isValid()) { context->setFinishedWithError(error.name(), error.message()); return; } context->setFinished(count); } BaseConnectionVoicemailInterface::BaseConnectionVoicemailInterface() : AbstractConnectionInterface(TP_QT_IFACE_CONNECTION_VOICEMAIL), mPriv(new Private(this)) { } BaseConnectionVoicemailInterface::~BaseConnectionVoicemailInterface() { delete mPriv; } void BaseConnectionVoicemailInterface::setVoicemailIndicatorCallback(const VoicemailIndicatorCallback &cb) { mPriv->voicemailIndicatorCB = cb; } void BaseConnectionVoicemailInterface::setVoicemailNumberCallback(const VoicemailNumberCallback &cb) { mPriv->voicemailNumberCB = cb; } void BaseConnectionVoicemailInterface::setVoicemailCountCallback(const VoicemailCountCallback &cb) { mPriv->voicemailCountCB = cb; } void BaseConnectionVoicemailInterface::setVoicemailCount(int count) { Q_EMIT mPriv->adaptee->voicemailCountChanged(uint(count)); } void BaseConnectionVoicemailInterface::setVoicemailNumber(const QString &voicemailNumber) { Q_EMIT mPriv->adaptee->voicemailNumberChanged(voicemailNumber); } void BaseConnectionVoicemailInterface::setVoicemailIndicator(bool active) { Q_EMIT mPriv->adaptee->voicemailIndicatorChanged(active); } QVariantMap BaseConnectionVoicemailInterface::immutableProperties() const { QVariantMap map; return map; } void BaseConnectionVoicemailInterface::createAdaptor() { (void) new ConnectionInterfaceVoicemailAdaptor(dbusObject()->dbusConnection(), mPriv->adaptee, dbusObject()); } ConnectionInterfaceVoicemailAdaptor::ConnectionInterfaceVoicemailAdaptor(const QDBusConnection& bus, QObject* adaptee, QObject* parent) : Tp::AbstractAdaptor(bus, adaptee, parent) { connect(adaptee, SIGNAL(voicemailCountChanged(uint)), SIGNAL(VoicemailCountChanged(uint))); connect(adaptee, SIGNAL(voicemailIndicatorChanged(bool)), SIGNAL(VoicemailIndicatorChanged(bool))); connect(adaptee, SIGNAL(voicemailNumberChanged(QString)), SIGNAL(VoicemailNumberChanged(QString))); } ConnectionInterfaceVoicemailAdaptor::~ConnectionInterfaceVoicemailAdaptor() { } bool ConnectionInterfaceVoicemailAdaptor::VoicemailIndicator(const QDBusMessage& dbusMessage) { if (!adaptee()->metaObject()->indexOfMethod("voicemailIndicator(ConnectionInterfaceVoicemailAdaptor::VoicemailIndicatorContextPtr)")) { dbusConnection().send(dbusMessage.createErrorReply(TP_QT_ERROR_NOT_IMPLEMENTED, QLatin1String("Not implemented"))); return bool(); } VoicemailIndicatorContextPtr ctx = VoicemailIndicatorContextPtr( new Tp::MethodInvocationContext< bool >(dbusConnection(), dbusMessage)); QMetaObject::invokeMethod(adaptee(), "voicemailIndicator", Q_ARG(ConnectionInterfaceVoicemailAdaptor::VoicemailIndicatorContextPtr, ctx)); return bool(); } QString ConnectionInterfaceVoicemailAdaptor::VoicemailNumber(const QDBusMessage& dbusMessage) { if (!adaptee()->metaObject()->indexOfMethod("voicemailNumber(ConnectionInterfaceVoicemailAdaptor::VoicemailNumberContextPtr)")) { dbusConnection().send(dbusMessage.createErrorReply(TP_QT_ERROR_NOT_IMPLEMENTED, QLatin1String("Not implemented"))); return QString(); } VoicemailNumberContextPtr ctx = VoicemailNumberContextPtr( new Tp::MethodInvocationContext< QString >(dbusConnection(), dbusMessage)); QMetaObject::invokeMethod(adaptee(), "voicemailNumber", Q_ARG(ConnectionInterfaceVoicemailAdaptor::VoicemailNumberContextPtr, ctx)); return QString(); } uint ConnectionInterfaceVoicemailAdaptor::VoicemailCount(const QDBusMessage& dbusMessage) { if (!adaptee()->metaObject()->indexOfMethod("voicemailCount(ConnectionInterfaceVoicemailAdaptor::VoicemailCountContextPtr)")) { dbusConnection().send(dbusMessage.createErrorReply(TP_QT_ERROR_NOT_IMPLEMENTED, QLatin1String("Not implemented"))); return uint(); } VoicemailCountContextPtr ctx = VoicemailCountContextPtr( new Tp::MethodInvocationContext< uint >(dbusConnection(), dbusMessage)); QMetaObject::invokeMethod(adaptee(), "voicemailCount", Q_ARG(ConnectionInterfaceVoicemailAdaptor::VoicemailCountContextPtr, ctx)); return uint(); } history-service-0.5/tests/common/mock/voicemailiface.h000066400000000000000000000124041455450246700232260ustar00rootroot00000000000000/** * Copyright (C) 2013-2015 Canonical, Ltd. * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU General Public License version 3, as published by * the Free Software Foundation. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, * SATISFACTORY QUALITY, 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 . * * Authors: Tiago Salem Herrmann Gustavo Pichorim Boiko */ #ifndef OFONOVOICEMAILIFACE_H #define OFONOVOICEMAILIFACE_H // telepathy-qt #include #include #include #include #include class BaseConnectionVoicemailInterface; typedef Tp::SharedPtr BaseConnectionVoicemailInterfacePtr; #define TP_QT_IFACE_CONNECTION_VOICEMAIL "com.lomiri.Telephony.Voicemail" class TP_QT_EXPORT BaseConnectionVoicemailInterface : public Tp::AbstractConnectionInterface { Q_OBJECT Q_DISABLE_COPY(BaseConnectionVoicemailInterface) public: static BaseConnectionVoicemailInterfacePtr create() { return BaseConnectionVoicemailInterfacePtr(new BaseConnectionVoicemailInterface()); } template static Tp::SharedPtr create() { return Tp::SharedPtr( new BaseConnectionVoicemailInterfaceSubclass()); } QVariantMap immutableProperties() const; virtual ~BaseConnectionVoicemailInterface(); typedef Tp::Callback1 VoicemailCountCallback; void setVoicemailCountCallback(const VoicemailCountCallback &cb); typedef Tp::Callback1 VoicemailIndicatorCallback; void setVoicemailIndicatorCallback(const VoicemailIndicatorCallback &cb); typedef Tp::Callback1 VoicemailNumberCallback; void setVoicemailNumberCallback(const VoicemailNumberCallback &cb); public Q_SLOTS: void setVoicemailCount(int count); void setVoicemailIndicator(bool active); void setVoicemailNumber(const QString &voicemailNumber); protected: BaseConnectionVoicemailInterface(); private: void createAdaptor(); class Adaptee; friend class Adaptee; struct Private; friend struct Private; Private *mPriv; }; class TP_QT_EXPORT ConnectionInterfaceVoicemailAdaptor : public Tp::AbstractAdaptor { Q_OBJECT Q_CLASSINFO("D-Bus Interface", TP_QT_IFACE_CONNECTION_VOICEMAIL) Q_CLASSINFO("D-Bus Introspection", "" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" "") public: ConnectionInterfaceVoicemailAdaptor(const QDBusConnection& dbusConnection, QObject* adaptee, QObject* parent); virtual ~ConnectionInterfaceVoicemailAdaptor(); typedef Tp::MethodInvocationContextPtr< bool > VoicemailIndicatorContextPtr; typedef Tp::MethodInvocationContextPtr< QString > VoicemailNumberContextPtr; typedef Tp::MethodInvocationContextPtr< uint > VoicemailCountContextPtr; public Q_SLOTS: // METHODS bool VoicemailIndicator(const QDBusMessage& dbusMessage); QString VoicemailNumber(const QDBusMessage& dbusMessage); uint VoicemailCount(const QDBusMessage& dbusMessage); Q_SIGNALS: // SIGNALS void VoicemailCountChanged(uint count); void VoicemailIndicatorChanged(bool active); void VoicemailNumberChanged(const QString &voicemailNumber); }; class TP_QT_NO_EXPORT BaseConnectionVoicemailInterface::Adaptee : public QObject { Q_OBJECT public: Adaptee(BaseConnectionVoicemailInterface *interface); ~Adaptee(); private Q_SLOTS: void voicemailIndicator(const ConnectionInterfaceVoicemailAdaptor::VoicemailIndicatorContextPtr &context); void voicemailNumber(const ConnectionInterfaceVoicemailAdaptor::VoicemailNumberContextPtr &context); void voicemailCount(const ConnectionInterfaceVoicemailAdaptor::VoicemailCountContextPtr &context); Q_SIGNALS: void voicemailCountChanged(uint count); void voicemailIndicatorChanged(bool active); void voicemailNumberChanged(const QString &voicemailNumber); public: BaseConnectionVoicemailInterface *mInterface; }; #endif history-service-0.5/tests/common/mockcontroller.cpp000066400000000000000000000100531455450246700227230ustar00rootroot00000000000000/** * Copyright (C) 2013 Canonical, Ltd. * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU General Public License version 3, as published by * the Free Software Foundation. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, * SATISFACTORY QUALITY, 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 . * * Authors: * Tiago Salem Herrmann * Gustavo Pichorim Boiko */ #include "mockcontroller.h" #include static const QString mockService("com.lomiri.MockConnection"); static const QString mockObject("/com/lomiri/MockConnection/%1"); static const QString mockInterface("com.lomiri.MockConnection"); MockController::MockController(const QString &protocol, QObject *parent) : QObject(parent), mProtocol(protocol), mMockObject(mockObject.arg(protocol)), mMockInterface(mockService, mockObject.arg(protocol), mockInterface) { connect(&mMockInterface, SIGNAL(MessageRead(QString)), this, SIGNAL(messageRead(QString))); connect(&mMockInterface, SIGNAL(MessageSent(QString, QVariantMap)), this, SIGNAL(messageSent(QString, QVariantMap))); connect(&mMockInterface, SIGNAL(CallReceived(QString)), this, SIGNAL(callReceived(QString))); connect(&mMockInterface, SIGNAL(CallEnded(QString)), this, SIGNAL(callEnded(QString))); connect(&mMockInterface, SIGNAL(CallStateChanged(QString, QString, QString)), this, SIGNAL(callStateChanged(QString,QString,QString))); connect(&mMockInterface, SIGNAL(ConferenceCreated(QString)), this, SIGNAL(conferenceCreated(QString))); connect(&mMockInterface, SIGNAL(ChannelMerged(QString)), this, SIGNAL(channelMerged(QString))); connect(&mMockInterface, SIGNAL(ChannelSplitted(QString)), this, SIGNAL(channelSplitted(QString))); connect(&mMockInterface, SIGNAL(Disconnected()), this, SIGNAL(disconnected())); connect(&mMockInterface, SIGNAL(Destroyed()), this, SIGNAL(connectionDestroyed())); } void MockController::placeIncomingMessage(const QString &message, const QVariantMap &properties) { mMockInterface.call("PlaceIncomingMessage", message, properties); } void MockController::placeDeliveryReport(const QStringList &recipients, const QString &messageId, const QString &status) { mMockInterface.call("PlaceDeliveryReport", recipients, messageId, status); } QString MockController::placeCall(const QVariantMap &properties) { QDBusReply reply = mMockInterface.call("PlaceCall", properties); return reply; } void MockController::hangupCall(const QString &callerId) { mMockInterface.call("HangupCall", callerId); } void MockController::setCallState(const QString &phoneNumber, const QString &state) { mMockInterface.call("SetCallState", phoneNumber, state); } void MockController::setOnline(bool online) { mMockInterface.call("SetOnline", online); } void MockController::setPresence(const QString &status, const QString &statusMessage) { mMockInterface.call("SetPresence", status, statusMessage); } void MockController::setVoicemailNumber(const QString &number) { mMockInterface.call("SetVoicemailNumber", number); } void MockController::setVoicemailIndicator(bool active) { mMockInterface.call("SetVoicemailIndicator", active); } void MockController::setVoicemailCount(int count) { mMockInterface.call("SetVoicemailCount", count); } void MockController::setEmergencyNumbers(const QStringList &numbers) { mMockInterface.call("SetEmergencyNumbers", numbers); } QString MockController::serial() { QDBusReply reply = mMockInterface.call("Serial"); if (!reply.isValid()) { return QString(); } return reply.value(); } history-service-0.5/tests/common/mockcontroller.h000066400000000000000000000046521455450246700224000ustar00rootroot00000000000000/** * Copyright (C) 2013 Canonical, Ltd. * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU General Public License version 3, as published by * the Free Software Foundation. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, * SATISFACTORY QUALITY, 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 . * * Authors: * Tiago Salem Herrmann * Gustavo Pichorim Boiko */ #ifndef MOCKCONTROLLER_H #define MOCKCONTROLLER_H #include #include class MockController : public QObject { Q_OBJECT public: explicit MockController(const QString &protocol, QObject *parent = 0); Q_SIGNALS: void messageRead(const QString &messageId); void messageSent(const QString &message, const QVariantMap &properties); void callReceived(const QString &callerId); void callEnded(const QString &callerId); void callStateChanged(const QString &callerId, const QString &objectPath, const QString &state); void conferenceCreated(const QString &objectPath); void channelMerged(const QString &objectPath); void channelSplitted(const QString &objectPath); void disconnected(); void connectionDestroyed(); public Q_SLOTS: void placeIncomingMessage(const QString &message, const QVariantMap &properties); void placeDeliveryReport(const QStringList &recipients, const QString &messageId, const QString &status); QString placeCall(const QVariantMap &properties); void hangupCall(const QString &callerId); void setCallState(const QString &phoneNumber, const QString &state); void setOnline(bool online); void setPresence(const QString &status, const QString &statusMessage); // voicemail stuff void setVoicemailNumber(const QString &number); void setVoicemailIndicator(bool active); void setVoicemailCount(int count); // emergency numbers stuff void setEmergencyNumbers(const QStringList &numbers); // USSD stuff QString serial(); private: QString mProtocol; QString mMockObject; QDBusInterface mMockInterface; }; #endif // MOCKCONTROLLER_H history-service-0.5/tests/common/telepathytest.cpp000066400000000000000000000111731455450246700225710ustar00rootroot00000000000000/* * Copyright (C) 2015 Canonical, Ltd. * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 #include #include #include #include #include "telepathytest.h" #include "telepathyhelper_p.h" void TelepathyTest::initialize(int waitTime) { QSignalSpy spy(History::TelepathyHelper::instance(), SIGNAL(setupReady())); QTRY_COMPARE_WITH_TIMEOUT(spy.count(), 1, DEFAULT_TIMEOUT); // just in case, remove any existing account that might be a leftover from // previous test runs Q_FOREACH(const Tp::AccountPtr &account, History::TelepathyHelper::instance()->accounts()) { QVERIFY(removeAccount(account)); } // create an account manager instance to help testing Tp::Features accountFeatures; accountFeatures << Tp::Account::FeatureCore << Tp::Account::FeatureProtocolInfo; Tp::Features contactFeatures; contactFeatures << Tp::Contact::FeatureAlias << Tp::Contact::FeatureAvatarData << Tp::Contact::FeatureAvatarToken << Tp::Contact::FeatureCapabilities << Tp::Contact::FeatureSimplePresence; Tp::Features connectionFeatures; connectionFeatures << Tp::Connection::FeatureCore << Tp::Connection::FeatureSelfContact << Tp::Connection::FeatureSimplePresence; Tp::ChannelFactoryPtr channelFactory = Tp::ChannelFactory::create(QDBusConnection::sessionBus()); channelFactory->addCommonFeatures(Tp::Channel::FeatureCore); mAccountManager = Tp::AccountManager::create( Tp::AccountFactory::create(QDBusConnection::sessionBus(), accountFeatures), Tp::ConnectionFactory::create(QDBusConnection::sessionBus(), connectionFeatures), channelFactory, Tp::ContactFactory::create(contactFeatures)); mReady = false; connect(mAccountManager->becomeReady(Tp::AccountManager::FeatureCore), &Tp::PendingOperation::finished, [=]{ mReady = true; }); QTRY_VERIFY(mReady); // give some time for telepathy stuff to settle QTest::qWait(waitTime); } void TelepathyTest::doCleanup() { // remove all accounts on every test to prevent garbage to go from one test to another Q_FOREACH(const Tp::AccountPtr &account, mAccounts) { QVERIFY(removeAccount(account)); } QVERIFY(mAccounts.isEmpty()); } Tp::AccountPtr TelepathyTest::addAccount(const QString &manager, const QString &protocol, const QString &displayName, const QVariantMap ¶meters) { bool finished = false; Tp::AccountPtr account; connect(mAccountManager->createAccount(manager, protocol, displayName, parameters), &Tp::PendingOperation::finished, [&](Tp::PendingOperation *op) { Tp::PendingAccount *pa = qobject_cast(op); if (op->isError() || !pa) { qCritical() << "Failed to create account:" << op->errorName() << op->errorMessage(); finished = true; return; } account = pa->account(); connect(account->setEnabled(true), &Tp::PendingOperation::finished, [&] { Tp::Presence presence(Tp::ConnectionPresenceTypeAvailable, "online", "online"); account->setRequestedPresence(presence); finished = true; }); }); while (!finished) { QTest::qWait(100); } mAccounts << account; return account; } bool TelepathyTest::removeAccount(const Tp::AccountPtr &account) { bool success = false; bool finished = false; connect(account->remove(), &Tp::PendingOperation::finished, [&](Tp::PendingOperation *op) { success = !op->isError(); finished = true; }); while (!finished) { QTest::qWait(100); } if (success) { mAccounts.removeAll(account); } return success; } QList TelepathyTest::accounts() const { return mAccounts; } void TelepathyTest::cleanup() { doCleanup(); } history-service-0.5/tests/common/telepathytest.h000066400000000000000000000030561455450246700222370ustar00rootroot00000000000000/* * Copyright (C) 2015 Canonical, Ltd. * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 TELEPATHYTEST_H #define TELEPATHYTEST_H #include #include #include #define DEFAULT_TIMEOUT 15000 class TelepathyTest : public QObject { Q_OBJECT protected: void initialize(int waitTime = 1000); void doCleanup(); // helper slots void onAccountManagerReady(Tp::PendingOperation *op); Tp::AccountPtr addAccount(const QString &manager, const QString &protocol, const QString &displayName, const QVariantMap ¶meters = QVariantMap()); bool removeAccount(const Tp::AccountPtr &account); QList accounts() const; private Q_SLOTS: void cleanup(); private: Tp::AccountManagerPtr mAccountManager; bool mReady; QList mAccounts; }; #endif //TELEPATHYTEST_H history-service-0.5/tests/daemon/000077500000000000000000000000001455450246700171365ustar00rootroot00000000000000history-service-0.5/tests/daemon/CMakeLists.txt000066400000000000000000000006711455450246700217020ustar00rootroot00000000000000include_directories( ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_SOURCE_DIR}/src ${CMAKE_SOURCE_DIR}/tests/common ${TP_QT5_INCLUDE_DIRS} ) generate_telepathy_test(DaemonTest SOURCES DaemonTest.cpp handler.cpp approver.cpp TASKS --task ${CMAKE_BINARY_DIR}/daemon/history-daemon --ignore-return --task-name history-daemon WAIT_FOR com.lomiri.HistoryService) history-service-0.5/tests/daemon/DaemonTest.cpp000066400000000000000000000615471455450246700217220ustar00rootroot00000000000000/** * Copyright (C) 2013 Canonical, Ltd. * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License version 3, as published by * the Free Software Foundation. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . * * Authors: Tiago Salem Herrmann */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include "telepathyhelper_p.h" #include "telepathytest.h" #include "mockcontroller.h" #include "handler.h" #include "approver.h" #include "manager.h" #include "thread.h" #include "textevent.h" #include "texteventattachment.h" #include "voiceevent.h" #include "config.h" #include "dbustypes.h" Q_DECLARE_METATYPE(Tp::CallChannelPtr) Q_DECLARE_METATYPE(Tp::TextChannelPtr) Q_DECLARE_METATYPE(QList) Q_DECLARE_METATYPE(History::Threads) Q_DECLARE_METATYPE(History::Events) Q_DECLARE_METATYPE(History::TextEventAttachment) Q_DECLARE_METATYPE(History::TextEventAttachments) Q_DECLARE_METATYPE(History::MessageStatus) QDBusArgument &operator<<(QDBusArgument &argument, const AttachmentStruct &attachment) { argument.beginStructure(); argument << attachment.id << attachment.contentType << attachment.filePath << attachment.offset << attachment.length; argument.endStructure(); return argument; } const QDBusArgument &operator>>(const QDBusArgument &argument, AttachmentStruct &attachment) { argument.beginStructure(); argument >> attachment.id >> attachment.contentType >> attachment.filePath >> attachment.offset >> attachment.length; argument.endStructure(); return argument; } class DaemonTest : public TelepathyTest { Q_OBJECT Q_SIGNALS: void contactsReceived(QList contacts); private Q_SLOTS: void initTestCase(); void init(); void cleanup(); void testMessageReceived(); void testMMSReceived(); void testMessageSentNoEventId(); void testMessageSent(); void testMMSSent(); void testMissedCall(); void testOutgoingCall(); void testDeliveryReport_data(); void testDeliveryReport(); // helper slots void onPendingContactsFinished(Tp::PendingOperation*); private: Approver *mApprover; Handler *mHandler; MockController *mMockController; Tp::AccountPtr mAccount; }; void DaemonTest::initTestCase() { qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType >(); qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); qDBusRegisterMetaType(); qDBusRegisterMetaType(); initialize(); // register the handler mHandler = new Handler(this); History::TelepathyHelper::instance()->registerClient(mHandler, "HistoryTestHandler"); QTRY_VERIFY(mHandler->isRegistered()); // register the approver mApprover = new Approver(this); History::TelepathyHelper::instance()->registerClient(mApprover, "HistoryTestApprover"); // Tp-qt does not set registered status to approvers QTRY_VERIFY(QDBusConnection::sessionBus().interface()->isServiceRegistered(TELEPHONY_SERVICE_APPROVER)); // we need to wait in order to give telepathy time to notify about the approver and handler QTest::qWait(3000); } void DaemonTest::init() { mAccount = addAccount("mock", "mock", "the account"); QVERIFY(!mAccount.isNull()); QTRY_VERIFY(mAccount->isReady(Tp::Account::FeatureCore)); QTRY_VERIFY(!mAccount->connection().isNull()); QTRY_VERIFY(History::TelepathyHelper::instance()->connected()); mMockController = new MockController("mock", this); } void DaemonTest::cleanup() { doCleanup(); mMockController->deleteLater(); } void DaemonTest::testMessageReceived() { QSignalSpy threadsAddedSpy(History::Manager::instance(), SIGNAL(threadsAdded(History::Threads))); QSignalSpy threadsModifiedSpy(History::Manager::instance(), SIGNAL(threadsModified(History::Threads))); QSignalSpy eventsAddedSpy(History::Manager::instance(), SIGNAL(eventsAdded(History::Events))); QVariantMap properties; QString sender = "123456789"; QString message = "Test message"; QDateTime sentTime = QDateTime::currentDateTime(); properties["Sender"] = sender; properties["SentTime"] = sentTime.toString(Qt::ISODate); properties["Recipients"] = QStringList() << sender; QSignalSpy handlerSpy(mHandler, SIGNAL(textChannelAvailable(Tp::TextChannelPtr))); mMockController->placeIncomingMessage(message, properties); QTRY_COMPARE(threadsAddedSpy.count(), 1); History::Threads threads = threadsAddedSpy.first().first().value(); QCOMPARE(threads.count(), 1); History::Thread thread = threads.first(); QTRY_COMPARE(threadsModifiedSpy.count(), 1); threads = threadsModifiedSpy.first().first().value(); QCOMPARE(threads.count(), 1); History::Thread modifiedThread = threads.first(); QVERIFY(modifiedThread == thread); QTRY_COMPARE(eventsAddedSpy.count(), 1); History::Events events = eventsAddedSpy.first().first().value(); QCOMPARE(events.count(), 1); History::TextEvent event = events.first(); QCOMPARE(event.senderId(), sender); QCOMPARE(event.threadId(), modifiedThread.threadId()); QVERIFY(modifiedThread.lastEvent() == event); QCOMPARE(event.timestamp().toString(Qt::ISODate), sentTime.toString(Qt::ISODate)); QCOMPARE(event.message(), message); QTRY_COMPARE(handlerSpy.count(), 1); Tp::TextChannelPtr channel = handlerSpy.first().first().value(); QVERIFY(channel); channel->requestClose(); } void DaemonTest::testMMSReceived() { QSignalSpy threadsAddedSpy(History::Manager::instance(), SIGNAL(threadsAdded(History::Threads))); QSignalSpy threadsModifiedSpy(History::Manager::instance(), SIGNAL(threadsModified(History::Threads))); QSignalSpy eventsAddedSpy(History::Manager::instance(), SIGNAL(eventsAdded(History::Events))); QVariantMap properties; QString sender = "9876543210"; QString message = "Test message"; QDateTime sentTime = QDateTime::currentDateTime(); properties["Sender"] = sender; properties["SentTime"] = sentTime.toString(Qt::ISODate); properties["Recipients"] = QStringList() << sender; AttachmentList newAttachments; AttachmentStruct newAttachment; newAttachment.id = QString("dialer-app.png"); newAttachment.contentType = QString("image/png"); newAttachment.filePath = QString("%1/dialer-app.png").arg(TEST_DATA_DIR); newAttachment.offset = 0; newAttachment.length = 31654; newAttachments << newAttachment; AttachmentStruct newAttachment2; newAttachment2.id = QString(""); newAttachment2.contentType = QString("image/gif"); newAttachment2.filePath = QString("%1/AFELIX.GIF").arg(TEST_DATA_DIR); newAttachment2.offset = 0; newAttachment2.length = 28809; newAttachments << newAttachment2; properties["Attachments"] = QVariant::fromValue(newAttachments); QSignalSpy handlerSpy(mHandler, SIGNAL(textChannelAvailable(Tp::TextChannelPtr))); mMockController->placeIncomingMessage(message, properties); QTRY_COMPARE(threadsAddedSpy.count(), 1); History::Threads threads = threadsAddedSpy.first().first().value(); QCOMPARE(threads.count(), 1); History::Thread thread = threads.first(); QTRY_COMPARE(threadsModifiedSpy.count(), 1); threads = threadsModifiedSpy.first().first().value(); QCOMPARE(threads.count(), 1); History::Thread modifiedThread = threads.first(); QVERIFY(modifiedThread == thread); QTRY_COMPARE(eventsAddedSpy.count(), 1); History::Events events = eventsAddedSpy.first().first().value(); QCOMPARE(events.count(), 1); History::TextEvent event = events.first(); QCOMPARE(event.senderId(), sender); QCOMPARE(event.threadId(), modifiedThread.threadId()); QVERIFY(modifiedThread.lastEvent() == event); // attachments QCOMPARE(event.attachments().count(), 2); History::TextEventAttachment txtAtt = event.attachments().first(); QVERIFY(txtAtt.contentType() == QString("image/png")); QVERIFY(txtAtt.attachmentId() == QString("dialer-app.png")); QVERIFY(QFile(txtAtt.filePath()).exists()); txtAtt = event.attachments().at(1); QVERIFY(txtAtt.contentType() == QString("image/gif")); QVERIFY(txtAtt.attachmentId() == QString("")); QVERIFY(QFile(txtAtt.filePath()).exists()); QFileInfo fi(txtAtt.filePath()); // "<>" chars should have been stripped QVERIFY(fi.fileName() == "AFELIX.GIF"); QTRY_COMPARE(handlerSpy.count(), 1); Tp::TextChannelPtr channel = handlerSpy.first().first().value(); QVERIFY(channel); channel->requestClose(); } void DaemonTest::testMessageSentNoEventId() { // Request the contact to start chatting to QSignalSpy spy(this, SIGNAL(contactsReceived(QList))); QString recipient = "11111111"; connect(mAccount->connection()->contactManager()->contactsForIdentifiers(QStringList() << recipient), SIGNAL(finished(Tp::PendingOperation*)), SLOT(onPendingContactsFinished(Tp::PendingOperation*))); QTRY_COMPARE(spy.count(), 1); QList contacts = spy.first().first().value >(); QCOMPARE(contacts.count(), 1); QCOMPARE(contacts.first()->id(), recipient); QSignalSpy spyTextChannel(mHandler, SIGNAL(textChannelAvailable(Tp::TextChannelPtr))); Q_FOREACH(Tp::ContactPtr contact, contacts) { mAccount->ensureTextChat(contact, QDateTime::currentDateTime(), TP_QT_IFACE_CLIENT + ".HistoryTestHandler"); } QTRY_COMPARE(spyTextChannel.count(), 1); Tp::TextChannelPtr channel = spyTextChannel.first().first().value(); QVERIFY(channel); QSignalSpy eventsAddedSpy(History::Manager::instance(), SIGNAL(eventsAdded(History::Events))); QString messageText = "Hello, big world!"; Tp::Message m(Tp::ChannelTextMessageTypeNormal, messageText); Tp::MessagePartList parts = m.parts(); Tp::MessagePart header = parts[0]; Tp::MessagePart body = parts[1]; header["no-event-id"] = QDBusVariant(true); Tp::MessagePartList newPart; newPart << header << body; Tp::PendingSendMessage *message = channel->send(newPart); QTRY_COMPARE(eventsAddedSpy.count(), 1); History::Events events = eventsAddedSpy.first().first().value(); QCOMPARE(events.count(), 1); History::TextEvent event = events.first(); QVERIFY(!event.eventId().isEmpty()); channel->requestClose(); } void DaemonTest::testMessageSent() { // Request the contact to start chatting to QSignalSpy spy(this, SIGNAL(contactsReceived(QList))); QString recipient = "987654321"; connect(mAccount->connection()->contactManager()->contactsForIdentifiers(QStringList() << recipient), SIGNAL(finished(Tp::PendingOperation*)), SLOT(onPendingContactsFinished(Tp::PendingOperation*))); QTRY_COMPARE(spy.count(), 1); QList contacts = spy.first().first().value >(); QCOMPARE(contacts.count(), 1); QCOMPARE(contacts.first()->id(), recipient); QSignalSpy spyTextChannel(mHandler, SIGNAL(textChannelAvailable(Tp::TextChannelPtr))); Q_FOREACH(Tp::ContactPtr contact, contacts) { mAccount->ensureTextChat(contact, QDateTime::currentDateTime(), TP_QT_IFACE_CLIENT + ".HistoryTestHandler"); } QTRY_COMPARE(spyTextChannel.count(), 1); Tp::TextChannelPtr channel = spyTextChannel.first().first().value(); QVERIFY(channel); QSignalSpy threadsAddedSpy(History::Manager::instance(), SIGNAL(threadsAdded(History::Threads))); QSignalSpy threadsModifiedSpy(History::Manager::instance(), SIGNAL(threadsModified(History::Threads))); QSignalSpy eventsAddedSpy(History::Manager::instance(), SIGNAL(eventsAdded(History::Events))); QString messageText = "Hello, big world!"; Tp::PendingSendMessage *message = channel->send(messageText); QTRY_COMPARE(threadsAddedSpy.count(), 1); History::Threads threads = threadsAddedSpy.first().first().value(); QCOMPARE(threads.count(), 1); History::Thread thread = threads.first(); QTRY_COMPARE(threadsModifiedSpy.count(), 1); threads = threadsModifiedSpy.first().first().value(); QCOMPARE(threads.count(), 1); History::Thread modifiedThread = threads.first(); QVERIFY(modifiedThread == thread); QTRY_COMPARE(eventsAddedSpy.count(), 1); History::Events events = eventsAddedSpy.first().first().value(); QCOMPARE(events.count(), 1); History::TextEvent event = events.first(); QCOMPARE(event.senderId(), QString("self")); QCOMPARE(event.threadId(), modifiedThread.threadId()); QVERIFY(modifiedThread.lastEvent() == event); QCOMPARE(event.message(), messageText); channel->requestClose(); } void DaemonTest::testMMSSent() { // Request the contact to start chatting to QSignalSpy spy(this, SIGNAL(contactsReceived(QList))); QString recipient = "123412340"; connect(mAccount->connection()->contactManager()->contactsForIdentifiers(QStringList() << recipient), SIGNAL(finished(Tp::PendingOperation*)), SLOT(onPendingContactsFinished(Tp::PendingOperation*))); QTRY_COMPARE(spy.count(), 1); QList contacts = spy.first().first().value >(); QCOMPARE(contacts.count(), 1); QCOMPARE(contacts.first()->id(), recipient); QSignalSpy spyTextChannel(mHandler, SIGNAL(textChannelAvailable(Tp::TextChannelPtr))); Q_FOREACH(Tp::ContactPtr contact, contacts) { mAccount->ensureTextChat(contact, QDateTime::currentDateTime(), TP_QT_IFACE_CLIENT + ".HistoryTestHandler"); } QTRY_COMPARE(spyTextChannel.count(), 1); Tp::TextChannelPtr channel = spyTextChannel.first().first().value(); QVERIFY(channel); QSignalSpy threadsAddedSpy(History::Manager::instance(), SIGNAL(threadsAdded(History::Threads))); QSignalSpy threadsModifiedSpy(History::Manager::instance(), SIGNAL(threadsModified(History::Threads))); QSignalSpy eventsAddedSpy(History::Manager::instance(), SIGNAL(eventsAdded(History::Events))); QString messageText = "Hello, big world!"; Tp::Message m(Tp::ChannelTextMessageTypeNormal, messageText); Tp::MessagePartList parts = m.parts(); Tp::MessagePart header = parts[0]; header["message-sender"] = QDBusVariant(0); header["message-type"] = QDBusVariant(Tp::ChannelTextMessageTypeNormal); QByteArray fileData; QFile attachmentFile(QString("%1/dialer-app.png").arg(TEST_DATA_DIR)); attachmentFile.open(QIODevice::ReadOnly); fileData = attachmentFile.readAll(); Tp::MessagePart body = parts[1]; body["content-type"] = QDBusVariant("image/png"); body["identifier"] = QDBusVariant(".png"); body["content"] = QDBusVariant(fileData); body["size"] = QDBusVariant(fileData.size()); Tp::MessagePartList newPart; newPart << header << body; Tp::PendingSendMessage *message = channel->send(newPart); QTRY_COMPARE(threadsAddedSpy.count(), 1); History::Threads threads = threadsAddedSpy.first().first().value(); QCOMPARE(threads.count(), 1); History::Thread thread = threads.first(); QTRY_COMPARE(threadsModifiedSpy.count(), 1); threads = threadsModifiedSpy.first().first().value(); QCOMPARE(threads.count(), 1); History::Thread modifiedThread = threads.first(); QVERIFY(modifiedThread == thread); QTRY_COMPARE(eventsAddedSpy.count(), 1); History::Events events = eventsAddedSpy.first().first().value(); QCOMPARE(events.count(), 1); History::TextEvent event = events.first(); QCOMPARE(event.senderId(), QString("self")); QCOMPARE(event.threadId(), modifiedThread.threadId()); QVERIFY(modifiedThread.lastEvent() == event); //Attachments QCOMPARE(event.attachments().count(), 1); History::TextEventAttachment txtAtt = event.attachments().first(); QVERIFY(txtAtt.contentType() == QString("image/png")); QVERIFY(txtAtt.attachmentId() == QString(".png")); QVERIFY(QFile(txtAtt.filePath()).exists()); QFileInfo fi(txtAtt.filePath()); // special chars should have been stripped QVERIFY(fi.fileName() == "dialer-app.png"); channel->requestClose(); } void DaemonTest::testMissedCall() { QSignalSpy newCallSpy(mApprover, SIGNAL(newCall())); // create an incoming call QString callerId = "33333333"; QVariantMap properties; properties["Caller"] = callerId; properties["State"] = "incoming"; mMockController->placeCall(properties); QTRY_COMPARE(newCallSpy.count(), 1); QSignalSpy threadsAddedSpy(History::Manager::instance(), SIGNAL(threadsAdded(History::Threads))); QSignalSpy threadsModifiedSpy(History::Manager::instance(), SIGNAL(threadsModified(History::Threads))); QSignalSpy eventsAddedSpy(History::Manager::instance(), SIGNAL(eventsAdded(History::Events))); // now hangup the call and check that the event was added to the database mMockController->hangupCall(callerId); QTRY_COMPARE(threadsAddedSpy.count(), 1); History::Threads threads = threadsAddedSpy.first().first().value(); QCOMPARE(threads.count(), 1); History::Thread thread = threads.first(); QTRY_COMPARE(threadsModifiedSpy.count(), 1); threads = threadsModifiedSpy.first().first().value(); QCOMPARE(threads.count(), 1); History::Thread modifiedThread = threads.first(); QVERIFY(modifiedThread == thread); QTRY_COMPARE(eventsAddedSpy.count(), 1); History::Events events = eventsAddedSpy.first().first().value(); QCOMPARE(events.count(), 1); History::VoiceEvent event = events.first(); QCOMPARE(event.senderId(), callerId); QCOMPARE(event.threadId(), modifiedThread.threadId()); QVERIFY(modifiedThread.lastEvent() == event); QCOMPARE(event.missed(), true); } void DaemonTest::testOutgoingCall() { // Request the contact to start chatting to QString phoneNumber = "44444444"; QSignalSpy spy(this, SIGNAL(contactsReceived(QList))); connect(mAccount->connection()->contactManager()->contactsForIdentifiers(QStringList() << phoneNumber), SIGNAL(finished(Tp::PendingOperation*)), SLOT(onPendingContactsFinished(Tp::PendingOperation*))); QVERIFY(spy.wait()); QList contacts = spy.first().first().value >(); QCOMPARE(contacts.count(), 1); QCOMPARE(contacts.first()->id(), phoneNumber); QSignalSpy spyCallChannel(mHandler, SIGNAL(callChannelAvailable(Tp::CallChannelPtr))); Q_FOREACH(Tp::ContactPtr contact, contacts) { mAccount->ensureAudioCall(contact, "audio", QDateTime::currentDateTime(), TP_QT_IFACE_CLIENT + ".HistoryTestHandler"); } QVERIFY(spyCallChannel.wait()); Tp::CallChannelPtr channel = spyCallChannel.first().first().value(); QVERIFY(channel); mMockController->setCallState(phoneNumber, "alerting"); QTRY_COMPARE(channel->callState(), Tp::CallStateInitialised); mMockController->setCallState(phoneNumber, "active"); QTRY_COMPARE(channel->callState(), Tp::CallStateActive); QSignalSpy threadsAddedSpy(History::Manager::instance(), SIGNAL(threadsAdded(History::Threads))); QSignalSpy threadsModifiedSpy(History::Manager::instance(), SIGNAL(threadsModified(History::Threads))); QSignalSpy eventsAddedSpy(History::Manager::instance(), SIGNAL(eventsAdded(History::Events))); mMockController->setCallState(phoneNumber, "disconnected"); QTRY_COMPARE(channel->callState(), Tp::CallStateEnded); QTRY_COMPARE(threadsAddedSpy.count(), 1); History::Threads threads = threadsAddedSpy.first().first().value(); QCOMPARE(threads.count(), 1); History::Thread thread = threads.first(); QTRY_COMPARE(threadsModifiedSpy.count(), 1); threads = threadsModifiedSpy.first().first().value(); QCOMPARE(threads.count(), 1); History::Thread modifiedThread = threads.first(); QVERIFY(modifiedThread == thread); QTRY_COMPARE(eventsAddedSpy.count(), 1); History::Events events = eventsAddedSpy.first().first().value(); QCOMPARE(events.count(), 1); History::VoiceEvent event = events.first(); QCOMPARE(event.senderId(), QString("self")); QCOMPARE(event.threadId(), modifiedThread.threadId()); QVERIFY(modifiedThread.lastEvent() == event); QCOMPARE(event.missed(), false); QVERIFY(event.duration().isValid()); } void DaemonTest::testDeliveryReport_data() { QTest::addColumn("phoneNumber"); QTest::addColumn("deliveryStatus"); QTest::addColumn("messageStatus"); QTest::newRow("delivered status") << "11112222" << "delivered" << History::MessageStatusDelivered; QTest::newRow("temporarily failed") << "11113333" << "temporarily_failed" << History::MessageStatusTemporarilyFailed; QTest::newRow("permanently failed") << "11114444" << "permanently_failed" << History::MessageStatusPermanentlyFailed; QTest::newRow("accepted status") << "11115555" << "accepted" << History::MessageStatusAccepted; QTest::newRow("read status") << "11116666" << "read" << History::MessageStatusRead; QTest::newRow("deleted") << "11117777" << "deleted" << History::MessageStatusDeleted; QTest::newRow("unknown") << "11118888" << "unknown" << History::MessageStatusUnknown; } void DaemonTest::testDeliveryReport() { QFETCH(QString, phoneNumber); QFETCH(QString, deliveryStatus); QFETCH(History::MessageStatus, messageStatus); // Request the contact to start chatting to QSignalSpy spy(this, SIGNAL(contactsReceived(QList))); connect(mAccount->connection()->contactManager()->contactsForIdentifiers(QStringList() << phoneNumber), SIGNAL(finished(Tp::PendingOperation*)), SLOT(onPendingContactsFinished(Tp::PendingOperation*))); QTRY_COMPARE(spy.count(), 1); QList contacts = spy.first().first().value >(); QCOMPARE(contacts.count(), 1); QCOMPARE(contacts.first()->id(), phoneNumber); QSignalSpy spyTextChannel(mHandler, SIGNAL(textChannelAvailable(Tp::TextChannelPtr))); Q_FOREACH(Tp::ContactPtr contact, contacts) { mAccount->ensureTextChat(contact, QDateTime::currentDateTime(), TP_QT_IFACE_CLIENT + ".HistoryTestHandler"); } QTRY_COMPARE(spyTextChannel.count(), 1); Tp::TextChannelPtr channel = spyTextChannel.first().first().value(); QVERIFY(channel); QSignalSpy eventsAddedSpy(History::Manager::instance(), SIGNAL(eventsAdded(History::Events))); QString messageText = "Hello, big world!"; Tp::PendingSendMessage *message = channel->send(messageText); QTRY_COMPARE(eventsAddedSpy.count(), 1); History::Events events = eventsAddedSpy.first().first().value(); QCOMPARE(events.count(), 1); History::TextEvent event = events.first(); // now send a delivery report for this text and make sure the event gets updated QSignalSpy eventsModifiedSpy(History::Manager::instance(), SIGNAL(eventsModified(History::Events))); mMockController->placeDeliveryReport(QStringList() << phoneNumber, event.eventId(), deliveryStatus); QTRY_COMPARE(eventsModifiedSpy.count(), 1); events = eventsModifiedSpy.first().first().value(); QCOMPARE(events.count(), 1); event = events.first(); QCOMPARE(event.messageStatus(), messageStatus); channel->requestClose(); } void DaemonTest::onPendingContactsFinished(Tp::PendingOperation *op) { Tp::PendingContacts *pc = qobject_cast(op); if (!pc) { return; } Q_EMIT contactsReceived(pc->contacts()); } QTEST_MAIN(DaemonTest) #include "DaemonTest.moc" history-service-0.5/tests/daemon/approver.cpp000066400000000000000000000142621455450246700215050ustar00rootroot00000000000000/** * Copyright (C) 2013 Canonical, Ltd. * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License version 3, as published by * the Free Software Foundation. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . * * Authors: Tiago Salem Herrmann */ #include "approver.h" #include #include #include #include #include #include Approver::Approver(QObject* parent) : QObject(parent), Tp::AbstractClientApprover(channelFilters()) { } Approver::~Approver() { } Tp::ChannelClassSpecList Approver::channelFilters() const { Tp::ChannelClassSpecList specList; specList << Tp::ChannelClassSpec::textChat() << Tp::ChannelClassSpec::audioCall(); return specList; } void Approver::addDispatchOperation(const Tp::MethodInvocationContextPtr<> &context, const Tp::ChannelDispatchOperationPtr &dispatchOperation) { bool willHandle = false; QList channels = dispatchOperation->channels(); Q_FOREACH (Tp::ChannelPtr channel, channels) { // Text Channel Tp::TextChannelPtr textChannel = Tp::TextChannelPtr::dynamicCast(channel); if (!textChannel.isNull()) { // right now we are not using any of the text channel's features in the approver // so no need to call becomeReady() on it. willHandle = true; continue; } // Call Channel Tp::CallChannelPtr callChannel = Tp::CallChannelPtr::dynamicCast(channel); if (!callChannel.isNull()) { Tp::PendingReady *pr = callChannel->becomeReady(Tp::Features() << Tp::CallChannel::FeatureCore << Tp::CallChannel::FeatureCallState << Tp::CallChannel::FeatureLocalHoldState); mChannels[pr] = callChannel; connect(pr, SIGNAL(finished(Tp::PendingOperation*)), SLOT(onChannelReady(Tp::PendingOperation*))); callChannel->setProperty("accountId", QVariant(dispatchOperation->account()->uniqueIdentifier())); willHandle = true; continue; } } if (willHandle) { mDispatchOps.append(dispatchOperation); } context->setFinished(); // check if we need to approve channels already or if we should wait. processChannels(); } void Approver::processChannels() { Q_FOREACH (Tp::ChannelDispatchOperationPtr dispatchOperation, mDispatchOps) { QList channels = dispatchOperation->channels(); Q_FOREACH (Tp::ChannelPtr channel, channels) { // approve only text channels Tp::TextChannelPtr textChannel = Tp::TextChannelPtr::dynamicCast(channel); if (textChannel.isNull()) { continue; } if (dispatchOperation->possibleHandlers().contains(TELEPHONY_SERVICE_HANDLER)) { dispatchOperation->handleWith(TELEPHONY_SERVICE_HANDLER); mDispatchOps.removeAll(dispatchOperation); } // FIXME: this shouldn't happen, but in any case, we need to check what to do when // the phone app client is not available } } } void Approver::acceptCall() { Q_FOREACH (Tp::ChannelDispatchOperationPtr dispatchOperation, mDispatchOps) { QList channels = dispatchOperation->channels(); Q_FOREACH (Tp::ChannelPtr channel, channels) { if (dispatchOperation->possibleHandlers().contains(TELEPHONY_SERVICE_HANDLER)) { dispatchOperation->handleWith(TELEPHONY_SERVICE_HANDLER); mDispatchOps.removeAll(dispatchOperation); } } } } void Approver::rejectCall() { Q_FOREACH (Tp::ChannelDispatchOperationPtr dispatchOperation, mDispatchOps) { QList channels = dispatchOperation->channels(); Q_FOREACH (Tp::ChannelPtr channel, channels) { if (dispatchOperation->possibleHandlers().contains(TELEPHONY_SERVICE_HANDLER)) { Tp::PendingOperation *claimop = dispatchOperation->claim(); mChannels[claimop] = dispatchOperation->channels().first(); connect(claimop, SIGNAL(finished(Tp::PendingOperation*)), this, SLOT(onClaimFinished(Tp::PendingOperation*))); } } } } void Approver::onClaimFinished(Tp::PendingOperation* op) { if(!op || op->isError()) { return; } Tp::CallChannelPtr callChannel = Tp::CallChannelPtr::dynamicCast(mChannels[op]); if (callChannel) { Tp::PendingOperation *hangupop = callChannel->hangup(Tp::CallStateChangeReasonUserRequested, TP_QT_ERROR_REJECTED, QString()); mChannels[hangupop] = callChannel; connect(hangupop, SIGNAL(finished(Tp::PendingOperation*)), this, SLOT(onHangupFinished(Tp::PendingOperation*))); } } void Approver::onHangupFinished(Tp::PendingOperation* op) { if(!op || op->isError()) { return; } mDispatchOps.removeAll(dispatchOperation(op)); mChannels.remove(op); } Tp::ChannelDispatchOperationPtr Approver::dispatchOperation(Tp::PendingOperation *op) { Tp::ChannelPtr channel = mChannels[op]; QString accountId = channel->property("accountId").toString(); Q_FOREACH (Tp::ChannelDispatchOperationPtr dispatchOperation, mDispatchOps) { if (dispatchOperation->account()->uniqueIdentifier() == accountId) { return dispatchOperation; } } return Tp::ChannelDispatchOperationPtr(); } void Approver::onChannelReady(Tp::PendingOperation* /* op */) { Q_EMIT newCall(); } history-service-0.5/tests/daemon/approver.h000066400000000000000000000037121455450246700211500ustar00rootroot00000000000000/** * Copyright (C) 2013 Canonical, Ltd. * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License version 3, as published by * the Free Software Foundation. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . * * Authors: Tiago Salem Herrmann */ #ifndef APPROVER_H #define APPROVER_H #include #include #include #include #define TELEPHONY_SERVICE_HANDLER TP_QT_IFACE_CLIENT + ".HistoryTestHandler" #define TELEPHONY_SERVICE_APPROVER TP_QT_IFACE_CLIENT + ".HistoryTestApprover" class Approver : public QObject, public Tp::AbstractClientApprover { Q_OBJECT public: Approver(QObject *parent = 0); ~Approver(); Tp::ChannelClassSpecList channelFilters() const; void addDispatchOperation(const Tp::MethodInvocationContextPtr<> &context, const Tp::ChannelDispatchOperationPtr &dispatchOperation); Q_SIGNALS: void newCall(); public Q_SLOTS: void acceptCall(); void rejectCall(); private Q_SLOTS: void processChannels(); void onClaimFinished(Tp::PendingOperation* op); void onHangupFinished(Tp::PendingOperation* op); void onChannelReady(Tp::PendingOperation *op); protected: Tp::ChannelDispatchOperationPtr dispatchOperation(Tp::PendingOperation *op); private: QList mDispatchOps; QMap mChannels; }; #endif // APPROVER_H history-service-0.5/tests/daemon/dbus-test-wrapper.sh.in000077500000000000000000000006031455450246700234710ustar00rootroot00000000000000#!/bin/sh # export the home folder to somewhere in /tmp TMPDIR=/tmp/history_test_home rm -rf $TMPDIR export HOME=$TMPDIR # now run gnome-keyring gnome-keyring-daemon -r -d # we need to set this otherwise mission-control doesn't work properly dconf write /org/gnome/empathy/use-conn false mc-tool add mock/mock account0 mc-tool enable mock/mock/account0 $@ RESULT=$? return $RESULT history-service-0.5/tests/daemon/handler.cpp000066400000000000000000000116401455450246700212610ustar00rootroot00000000000000/** * Copyright (C) 2013 Canonical, Ltd. * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License version 3, as published by * the Free Software Foundation. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . * * Authors: * Tiago Salem Herrmann * Gustavo Pichorim Boiko */ #include "handler.h" #include "telepathyhelper_p.h" #include #include #include #include #include Handler::Handler(QObject *parent) : QObject(parent), Tp::AbstractClientHandler(channelFilters()), mBypassApproval(false) { } void Handler::setBypassApproval(bool bypass) { mBypassApproval = bypass; } bool Handler::bypassApproval() const { return mBypassApproval; } void Handler::handleChannels(const Tp::MethodInvocationContextPtr<> &context, const Tp::AccountPtr &account, const Tp::ConnectionPtr &connection, const QList &channels, const QList &requestsSatisfied, const QDateTime &userActionTime, const Tp::AbstractClientHandler::HandlerInfo &handlerInfo) { Q_UNUSED(account) Q_UNUSED(connection) Q_UNUSED(requestsSatisfied) Q_UNUSED(userActionTime) Q_UNUSED(handlerInfo) Q_FOREACH(const Tp::ChannelPtr channel, channels) { Tp::TextChannelPtr textChannel = Tp::TextChannelPtr::dynamicCast(channel); if (textChannel) { Tp::PendingReady *pr = textChannel->becomeReady(Tp::Features() << Tp::TextChannel::FeatureCore << Tp::TextChannel::FeatureChatState << Tp::TextChannel::FeatureMessageCapabilities << Tp::TextChannel::FeatureMessageQueue << Tp::TextChannel::FeatureMessageSentSignal); connect(pr, SIGNAL(finished(Tp::PendingOperation*)), SLOT(onTextChannelReady(Tp::PendingOperation*))); mReadyRequests[pr] = textChannel; continue; } Tp::CallChannelPtr callChannel = Tp::CallChannelPtr::dynamicCast(channel); if (callChannel) { Tp::PendingReady *pr = callChannel->becomeReady(Tp::Features() << Tp::CallChannel::FeatureCore << Tp::CallChannel::FeatureCallState << Tp::CallChannel::FeatureContents << Tp::CallChannel::FeatureLocalHoldState); connect(pr, SIGNAL(finished(Tp::PendingOperation*)), SLOT(onCallChannelReady(Tp::PendingOperation*))); mReadyRequests[pr] = callChannel; continue; } } context->setFinished(); } Tp::ChannelClassSpecList Handler::channelFilters() { Tp::ChannelClassSpecList specList; specList << Tp::ChannelClassSpec::audioCall(); specList << Tp::ChannelClassSpec::textChat(); return specList; } void Handler::onTextChannelReady(Tp::PendingOperation *op) { Tp::PendingReady *pr = qobject_cast(op); if (!pr) { qCritical() << "The pending object is not a Tp::PendingReady"; return; } Tp::ChannelPtr channel = mReadyRequests[pr]; Tp::TextChannelPtr textChannel = Tp::TextChannelPtr::dynamicCast(channel); if(!textChannel) { qCritical() << "The saved channel is not a Tp::TextChannel"; return; } mReadyRequests.remove(pr); Q_EMIT textChannelAvailable(textChannel); } void Handler::onCallChannelReady(Tp::PendingOperation *op) { Tp::PendingReady *pr = qobject_cast(op); if (!pr) { qCritical() << "The pending object is not a Tp::PendingReady"; return; } Tp::ChannelPtr channel = mReadyRequests[pr]; Tp::CallChannelPtr callChannel = Tp::CallChannelPtr::dynamicCast(channel); if(!callChannel) { qCritical() << "The saved channel is not a Tp::CallChannel"; return; } mReadyRequests.remove(pr); Q_EMIT callChannelAvailable(callChannel); } history-service-0.5/tests/daemon/handler.h000066400000000000000000000041531455450246700207270ustar00rootroot00000000000000/** * Copyright (C) 2013 Canonical, Ltd. * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License version 3, as published by * the Free Software Foundation. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . * * Authors: * Tiago Salem Herrmann * Gustavo Pichorim Boiko */ #ifndef HANDLER_H #define HANDLER_H #include #include #include #include #include class Handler : public QObject, public Tp::AbstractClientHandler { Q_OBJECT public: Handler(QObject *parent = 0); ~Handler() { } bool bypassApproval() const; void handleChannels(const Tp::MethodInvocationContextPtr<> &context, const Tp::AccountPtr &account, const Tp::ConnectionPtr &connection, const QList &channels, const QList &requestsSatisfied, const QDateTime &userActionTime, const Tp::AbstractClientHandler::HandlerInfo &handlerInfo); Tp::ChannelClassSpecList channelFilters(); void setBypassApproval(bool bypass); Q_SIGNALS: void textChannelAvailable(Tp::TextChannelPtr textChannel); void callChannelAvailable(Tp::CallChannelPtr callChannel); private Q_SLOTS: void onTextChannelReady(Tp::PendingOperation *op); void onCallChannelReady(Tp::PendingOperation *op); private: QMap mReadyRequests; bool mBypassApproval; }; #endif // HANDLER_H history-service-0.5/tests/daemon/telepathyhelper.cpp000066400000000000000000000203021455450246700230360ustar00rootroot00000000000000/** * Copyright (C) 2013 Canonical, Ltd. * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License version 3, as published by * the Free Software Foundation. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . * * Authors: * Tiago Salem Herrmann * Gustavo Pichorim Boiko */ #include "telepathyhelper.h" #include #include #include #include #include TelepathyHelper::TelepathyHelper(QObject *parent) : QObject(parent), //mChannelObserver(0), mFirstTime(true), mConnected(false), mHandlerInterface(0) { Tp::registerTypes(); mAccountFeatures << Tp::Account::FeatureCore; mContactFeatures << Tp::Contact::FeatureAlias << Tp::Contact::FeatureAvatarData << Tp::Contact::FeatureAvatarToken << Tp::Contact::FeatureCapabilities << Tp::Contact::FeatureSimplePresence; mConnectionFeatures << Tp::Connection::FeatureCore << Tp::Connection::FeatureSelfContact << Tp::Connection::FeatureSimplePresence; Tp::ChannelFactoryPtr channelFactory = Tp::ChannelFactory::create(QDBusConnection::sessionBus()); channelFactory->addCommonFeatures(Tp::Channel::FeatureCore); mAccountManager = Tp::AccountManager::create( Tp::AccountFactory::create(QDBusConnection::sessionBus(), mAccountFeatures), Tp::ConnectionFactory::create(QDBusConnection::sessionBus(), mConnectionFeatures), channelFactory, Tp::ContactFactory::create(mContactFeatures)); connect(mAccountManager->becomeReady(Tp::AccountManager::FeatureCore), SIGNAL(finished(Tp::PendingOperation*)), SLOT(onAccountManagerReady(Tp::PendingOperation*))); mClientRegistrar = Tp::ClientRegistrar::create(mAccountManager); } TelepathyHelper::~TelepathyHelper() { } TelepathyHelper *TelepathyHelper::instance() { static TelepathyHelper* helper = new TelepathyHelper(); return helper; } QString TelepathyHelper::accountId() const { if (mAccount) { return mAccount->uniqueIdentifier(); } return QString(); } Tp::AccountPtr TelepathyHelper::account() const { return mAccount; } /* ChannelObserver *TelepathyHelper::channelObserver() const { return mChannelObserver; } */ bool TelepathyHelper::connected() const { return mConnected; } /* void TelepathyHelper::registerChannelObserver(const QString &observerName) { QString name = observerName; if (name.isEmpty()) { name = "TelephonyPluginObserver"; } if (mChannelObserver) { mChannelObserver->deleteLater(); } mChannelObserver = new ChannelObserver(this); registerClient(mChannelObserver, name); // messages connect(mChannelObserver, SIGNAL(textChannelAvailable(Tp::TextChannelPtr)), ChatManager::instance(), SLOT(onTextChannelAvailable(Tp::TextChannelPtr))); // calls connect(mChannelObserver, SIGNAL(callChannelAvailable(Tp::CallChannelPtr)), CallManager::instance(), SLOT(onCallChannelAvailable(Tp::CallChannelPtr))); Q_EMIT channelObserverCreated(mChannelObserver); } void TelepathyHelper::unregisterChannelObserver() { Tp::AbstractClientPtr clientPtr(mChannelObserver); if (clientPtr) { mClientRegistrar->unregisterClient(clientPtr); } mChannelObserver->deleteLater(); mChannelObserver = NULL; Q_EMIT channelObserverUnregistered(); } */ QStringList TelepathyHelper::supportedProtocols() const { QStringList protocols; protocols << "mock"; return protocols; } void TelepathyHelper::initializeAccount() { // watch for account state and connection changes connect(mAccount.data(), SIGNAL(stateChanged(bool)), SLOT(onAccountStateChanged(bool))); connect(mAccount.data(), SIGNAL(connectionChanged(const Tp::ConnectionPtr&)), SLOT(onAccountConnectionChanged(const Tp::ConnectionPtr&))); // and make sure it is enabled and connected if (!mAccount->isEnabled()) { ensureAccountEnabled(); } else { ensureAccountConnected(); } } void TelepathyHelper::ensureAccountEnabled() { mAccount->setConnectsAutomatically(true); connect(mAccount->setEnabled(true), SIGNAL(finished(Tp::PendingOperation*)), SLOT(onAccountEnabled(Tp::PendingOperation*))); } void TelepathyHelper::ensureAccountConnected() { // if the account is not connected, request it to connect if (!mAccount->connection() || mAccount->connectionStatus() != Tp::ConnectionStatusConnected) { Tp::Presence presence(Tp::ConnectionPresenceTypeAvailable, "available", "online"); mAccount->setRequestedPresence(presence); } else { watchSelfContactPresence(); } if (mFirstTime) { Q_EMIT accountReady(); mFirstTime = false; } } void TelepathyHelper::watchSelfContactPresence() { if (mAccount.isNull() || mAccount->connection().isNull()) { return; } connect(mAccount->connection()->selfContact().data(), SIGNAL(presenceChanged(Tp::Presence)), SLOT(onPresenceChanged(Tp::Presence))); onPresenceChanged(mAccount->connection()->selfContact()->presence()); } void TelepathyHelper::registerClient(Tp::AbstractClient *client, QString name) { Tp::AbstractClientPtr clientPtr(client); bool succeeded = mClientRegistrar->registerClient(clientPtr, name); if (!succeeded) { name.append("%1"); int count = 0; // limit the number of registered clients to 20, that should be a safe margin while (!succeeded && count < 20) { succeeded = mClientRegistrar->registerClient(clientPtr, name.arg(++count)); if (succeeded) { name = name.arg(count); } } } if (succeeded) { QObject *object = dynamic_cast(client); if (object) { object->setProperty("clientName", TP_QT_IFACE_CLIENT + "." + name ); } } } void TelepathyHelper::onAccountManagerReady(Tp::PendingOperation *op) { Q_UNUSED(op) Tp::AccountSetPtr accountSet; // try to find an account of the one of supported protocols Q_FOREACH(const QString &protocol, supportedProtocols()) { accountSet = mAccountManager->accountsByProtocol(protocol); if (accountSet->accounts().count() > 0) { break; } } if (accountSet->accounts().count() == 0) { qCritical() << "No compatible telepathy account found!"; return; } mAccount = accountSet->accounts()[0]; // in case we have more than one account, the first one to show on the list is going to be used if (accountSet->accounts().count() > 1) { qWarning() << "There are more than just one account of type" << mAccount->protocolName(); } Q_EMIT accountIdChanged(); initializeAccount(); } void TelepathyHelper::onAccountEnabled(Tp::PendingOperation *op) { // we might need to do more stuff once the account is enabled, but making sure it is connected is a good start ensureAccountConnected(); } void TelepathyHelper::onAccountStateChanged(bool enabled) { if (!enabled) { ensureAccountEnabled(); } } void TelepathyHelper::onAccountConnectionChanged(const Tp::ConnectionPtr &connection) { if (connection.isNull()) { ensureAccountConnected(); } else { watchSelfContactPresence(); } Q_EMIT connectionChanged(); } void TelepathyHelper::onPresenceChanged(const Tp::Presence &presence) { mConnected = (presence.type() == Tp::ConnectionPresenceTypeAvailable); Q_EMIT connectedChanged(); } history-service-0.5/tests/daemon/telepathyhelper.h000066400000000000000000000060151455450246700225100ustar00rootroot00000000000000/** * Copyright (C) 2013 Canonical, Ltd. * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License version 3, as published by * the Free Software Foundation. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . * * Authors: * Tiago Salem Herrmann * Gustavo Pichorim Boiko */ #ifndef TELEPATHYHELPER_H #define TELEPATHYHELPER_H #include #include #include #include #include #include //#include "channelobserver.h" #define CANONICAL_TELEPHONY_VOICEMAIL_IFACE "com.lomiri.Telephony.Voicemail" #define CANONICAL_TELEPHONY_SPEAKER_IFACE "com.lomiri.Telephony.Speaker" class TelepathyHelper : public QObject { Q_OBJECT Q_PROPERTY(bool connected READ connected NOTIFY connectedChanged) Q_PROPERTY(QString accountId READ accountId NOTIFY accountIdChanged) public: ~TelepathyHelper(); static TelepathyHelper *instance(); Tp::AccountPtr account() const; //ChannelObserver *channelObserver() const; bool connected() const; QString accountId() const; void registerClient(Tp::AbstractClient *client, QString name); Q_SIGNALS: //void channelObserverCreated(ChannelObserver *observer); //void channelObserverUnregistered(); void accountReady(); void connectionChanged(); void connectedChanged(); void accountIdChanged(); public Q_SLOTS: //Q_INVOKABLE void registerChannelObserver(const QString &observerName = QString()); //Q_INVOKABLE void unregisterChannelObserver(); protected: QStringList supportedProtocols() const; void initializeAccount(); void ensureAccountEnabled(); void ensureAccountConnected(); void watchSelfContactPresence(); private Q_SLOTS: void onAccountManagerReady(Tp::PendingOperation *op); void onAccountEnabled(Tp::PendingOperation *op); void onAccountStateChanged(bool enabled); void onAccountConnectionChanged(const Tp::ConnectionPtr &connection); void onPresenceChanged(const Tp::Presence &presence); private: explicit TelepathyHelper(QObject *parent = 0); Tp::AccountManagerPtr mAccountManager; Tp::Features mAccountManagerFeatures; Tp::Features mAccountFeatures; Tp::Features mContactFeatures; Tp::Features mConnectionFeatures; Tp::ClientRegistrarPtr mClientRegistrar; Tp::AccountPtr mAccount; //ChannelObserver *mChannelObserver; bool mFirstTime; bool mConnected; QDBusInterface *mHandlerInterface; }; #endif // TELEPATHYHELPER_H history-service-0.5/tests/data/000077500000000000000000000000001455450246700166045ustar00rootroot00000000000000history-service-0.5/tests/data/AFELIX.GIF000077500000000000000000000702111455450246700201070ustar00rootroot00000000000000GIF89aæd÷ÿÿÿÿÿÌÿÿ™ÿÿfÿÿ3ÿÿÿÌÿÿÌÌÿÌ™ÿÌfÿÌ3ÿÌÿ™ÿÿ™Ìÿ™™ÿ™fÿ™3ÿ™ÿfÿÿfÌÿf™ÿffÿf3ÿfÿ3ÿÿ3Ìÿ3™ÿ3fÿ33ÿ3ÿÿÿÌÿ™ÿfÿ3ÿÌÿÿÌÿÌÌÿ™ÌÿfÌÿ3ÌÿÌÌÿÌÌÌÌÌ™ÌÌfÌÌ3ÌÌÌ™ÿÌ™ÌÌ™™Ì™fÌ™3Ì™ÌfÿÌfÌÌf™ÌffÌf3ÌfÌ3ÿÌ3ÌÌ3™Ì3fÌ33Ì3ÌÿÌÌÌ™ÌfÌ3Ì™ÿÿ™ÿÌ™ÿ™™ÿf™ÿ3™ÿ™Ìÿ™Ì̙̙™Ìf™Ì3™Ì™™ÿ™™Ì™™™™™f™™3™™™fÿ™fÌ™f™™ff™f3™f™3ÿ™3Ì™3™™3f™33™3™ÿ™Ì™™™f™3™fÿÿfÿÌfÿ™fÿffÿ3fÿfÌÿfÌÌfÌ™fÌffÌ3fÌf™ÿf™Ìf™™f™ff™3f™ffÿffÌff™fffff3fff3ÿf3Ìf3™f3ff33f3fÿfÌf™fff3f3ÿÿ3ÿÌ3ÿ™3ÿf3ÿ33ÿ3Ìÿ3ÌÌ3Ì™3Ìf3Ì33Ì3™ÿ3™Ì3™™3™f3™33™3fÿ3fÌ3f™3ff3f33f33ÿ33Ì33™33f333333ÿ3Ì3™3f333ÿÿÿÌÿ™ÿfÿ3ÿÌÿÌÌÌ™ÌfÌ3Ì™ÿ™Ì™™™f™3™fÿfÌf™fff3f3ÿ3Ì3™3f333ÿÌ™f3îÝ»ªˆwUD"îÝ»ªˆwUD"îÝ»ªˆwUD"îîîÝÝÝ»»»ªªªˆˆˆwwwUUUDDD"""!ÿ NETSCAPE2.0!þGifBuilder 0.2 by Yves Piguet!ù ,PdýH° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹3jÜȱ£Ç CŠI²¤É“(SªLÈŠÕÊ‘¬þý{2¦L—4/Î$hSæ?œ9'ÞØÓçÏ  ,jÔgË–;‘lÊ´©Õ¨R ^Ýz5+B®`ÿY±ÔëÀ°WYX  ¬Y´V­°]ÈìR¸MW̰¢®ÙªhõîµboÞ½kÝ5l4ÐÞ@ÓŒVíÜ¡Y7ÝkSòJÍçZÁ”2^ÁkIçMuî ²=_š6ì˜mߦ+gûD €l\²VVàN©û_mĽ#•ë[æç°–‘K§›³øôë}‘šŽ~±_©€ýwý=sSñãÛU`•{úòæ­ŽG ÿoQôäY‰¼þ`O¹ØyÖŸ6á·Ö~õ È“OúÕd *DY„ E!KÐ]hPxF 8`U¬4甆g7ZW$2¥W¾Õ؉[µèUS"¦E"a‘ÈÞr`É8cc\íç£vç]娉ªhbd'Y÷"“rGEXÕ~NÁc•ýmÙn å$J¦iùß”ªÅgdgRNÙŒëõc›2ˆp\‚݉[*פ‹Êµ9V–bá¨Ü˜0ÕùS›LÞ4Ú–ÿô%¢‰ •qÆíÇ'”1Q*R‹ºùSOtÞÓz6MÊh©¡ט_ˆ+Ätâ—²Š•]6iú¦U´*õj‡¬P ¥U¨^IYyjGžWAê\iº]Úž³£zj‘´Rb†$aË-‹c9c¥xáTÜOXÚX“a@['`Ö:änYŒmÛÞ§••å®OÔÆ+/‹Ší«è½0=epB 1;bašmÙÒ¯ììo·çN,›Äë¨ñÆwìñÇ ‡,òÈ$—\Q@!ù ,PdýH° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹3jÜȱ£Ç CŠI²¤É“(Sª\Éòa¿–_ü3ã?šûý“Y³âMž nîìIQçOFo(ôhS¥K!>Ý9hT…I§>õgõêL­`z5˜5ìTVc š]ûmÚ²lµvíW«?}ú ^…×_½¿õøyåwàz+G­;ÕïáÀQ ³å÷¸¾¥’Ùî« ¹ffÍœÃd –Õc}ùD¯ü\÷±Ø–¤ÃÞ¼âuKÖþöÚ§u3`Û·Íâ{ŒoêaC‰†µÇ¹ž•¦¾ÿæ›ËRëðæù 8¯Pº[ý±?$”Ÿ•ÚMƒO.þ0ózÞ¿Ã,˽=çúŒö£Ž2k}ûñW“P¦x˜jce `¾õ{€Ù³Â{ÝÓ`‚B]wØ ÷ä—] iåTÕ>VPøW þ€â@Ÿí×?0®Èb‹2ÄW>äÉW£ˆO±‚O|zí(ÐT Ö”Mé£åT†¨dXx!™•?QVe¡?f6…æTòÛp¢]SÓý£?.V>_òã]”5VåSFˆ“˜¥YÙT F×'Kû…u(t¼ýs(†B}ySžnwS^c=ÊÛ“yå7>O¢Xâê?þXA^?³æ%'‘ŽòùÖ>FYaå>QÒ:•¯?MºšP$ÞdEj 6uOSƪdÔ> èk?žÞ$hz!¢¹Û¨¼‚¥£Aî…l ì–‘:šû(+^‚…îSƒ%kXÍ–Ki¥öJÖÉ­ÆÚNö˜5îSÑv4ðQ™­@XÂ-¼_µkíƒZ¦C\T]<-œ»=Ž4pǤå0pµÔÂB¥ydH™uÅòMÊ%²\sͬg¶ÃìâÄX‘v2Ê=ñ»+. ô[FãüóÒ…>ýs“TWmõÕXg­õÖ\wíõ×`g!ù , PdýH° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹3jÜȱ£Ç CŠI²¤É“(Sª\ɲ¥Ë—(ÿÁôØï_¿™ÿýógP&N‰5uú3¢N7MZ´áQ¤J—6môi¿ªD§&Äú´«M­¹zµ öàØ³YËFE;–iY¶lÝ6 ·+Ï©tëv• ÓŸ^½?óþ Sð`ü…yí¾zõøuuiø/dÈ÷X¥e©¸ñQ}—/ó]YîŠÐ÷áôü5äѤãjnëºì•gA_Þçµj}ønŸ|õн¡ñýÛ§zæØ|µëÙÃZ³p•^£‹>jï²ßë)ý±Ò¾ýßxÈ’Á‡?Šüå@:Û×÷Oýú÷Ü_¾WsŸ•úØ—’NÐéçš=RáT?·Wa^×à=V%àG‚Yq>Èçš>Í]õK¥ý“OwõäSàe E×>’YÅÜiV¢ˆÍæ•ðM&P}ýä8×^÷äÓ–ZC=µg]øRW+tæÕ]H*€?V°E%’âõØd•ké¤Ïlgæ?VædK\­¥’;­ÞMR"Æägöð#çIKÅ>^êôæž% ÊÛŽÌu•¨OZÁµ?1>¥ÜˆSi©LurhYýäcÅ¡ŸñÓXFR:—?\ùsÏWbéÓã[bj¦ªr7‰Õ)T`É ¤Ÿ>*]¥JÒU¬ä³B‡W)PçgßáÅ–W †%®3•ÈÖÂjÔϲžYc¶‰Èš¢ošªR‰¢ÂåO”‹± W´¥²…O¤àVTYR¥íh½Ýûk]ü(@¦¹&ùû¯ºýÔh¿æ–µÿèS.R /ä°AßIÖ°h1+—öÆñ‘aýÕO  Šdëu%úòƒÃF›,UuéK2§h±²ªÈ`ŠåÊ6U\’ÌDGkæÑH'­ôÒL7íôÓPG-õÔTŸ!ù ,PdýH° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹3jÜȱ£Ç CŠI²¤É“(Sª\ɲ¥Ë—0cÊœI³¦Í›8sêÜɳ§ÏŸ@ƒ ì'”a¿Eý‡4iÁ¥L›:U5ª?§U£2%´Ÿ?­`·þÌV+לdˆ%ÚÏ뿳.Óª v¦\ºsá¾¼‹w-M¾}Íþ ¸&`ÂRe&¬¸íâÀz["F¬xráÊ–éÚ ëÕ×¾Ÿ£nŽÚï^½ÓõöÑ­w¯îʶaUÿó‡ºvë²+N»> [í>Vÿr×Fm%l>Ô´òîËU¿áëò®¼äbÏú ×ΧÕôvÑ$ýåöãxT+Úk‡Ö®)HÇÃòÓG_ß¾Ðÿ²§?Ýþ«ôüÌæTþøU?+Ü·Ú~õ¬ÀT µµ‡OdÍœ>ýp÷ lyå¶>éµjæåR?¬Ø§üàà?ùàç×sõè>³VÜ?P¨ÑøäãŒ2ºFã‡é©fOj<úø#Sù蓆PYÞ˜^ üس‚s$ý³ÏQ¬ì8Žy Dãq ÞfxL¥ØãVf–UИýŒ¹dmöì#›“•œVùph֛ᙧŸc’¶\TÙɘVx&QU(eà¤ÆáèAWfÊ•LùS§X¹§ÒQ…‚•ØaYŸÆ´”2²ò¢_9‘ÖŸY½¹ŠT¬LˆkZ±¦TU>øXyYÅšTU n¦9®q…沆R_X-õ:[ ø J—^°K´·ŽkP¹Î¦•¸xEFìPð}$y„±r±Y5 Ñ]åõÕBdù“®£ñpYóµ§ÐcoqÄ×oj±âÜÀûZæïB€-oÆ“müp¼«E!Ĩbt×À‹²ÄkÁu˜Ë„‰Ücõ43ȳ˜ÞÆ—´Jál´¬ÌN¥æÑJ7íôÓPG-õÔTWmõÕX!ù ,PdýH° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹3jÜȱ£Ç CŠI²¤É“(Sª\ɲ¥Ë—0cÊœI³¦Í›8sêÜɳ§ÏŸ@ƒ …èo¨Â~ÿþE˜4éÒ‚H›}ª jÓý–Z½š4+Ð~[¹6õº³Ÿ?±hÇâ›¶íU°Vg†uK«ÜºxÞÍKwªÌ¹|¹’ 8pÓš… +¥™Øðà–HÙ*NKx2Ý¿–ÛÎ<›YìÞ¦úòåÛÇ9-+|ž!Gõ·µžë×úÒúsÍŠëÊÆÿ^ë®·í ר¯¦,Ý6ÐnÝ÷¸îÓ]ú±H¾øŽëÞwuwðÅ%ß“®»)«ãÔýí’ëo_>+¢ù]ÎvR}Òk;ß7¬¾û¤ÿÿg¼}½|IY¡[ ÆY×GQ±HxüÅF—köpÜv®E¥Ï|YeÅ=V€ÖXùÔƒ|ÇÙ kêuR?ûà“Otø¬Õu”UåÚ>¿WÛrõ·bSü(à?VœE#Zõ\ˆÒ!µ‚ü 5ÒYþäßU¡]–d=÷(Ý=X±O#!…R÷¤øO~m±2XúWO‡&õ³ã þc…ƒ²dV?¾&£ãýs+ ^ÅZ ø°‚a’õee[¡|ŽÉ+ölh ŠYåIQíÓ!W¨)ØCj¡dç\’¡õº¨A`©TX’m½ZæL¹úf­·É—f%fëG™9'¬ªÅúÉk°õu„›[ FV«œfhÙ£ÇJKŸe %¶ß[Û*f볺Z©a¬5Dî©…`‘âÚ©c튟à^´nµÁ;ªXòN´ï°–Ï·ù œÁ§¥Omn1¬,]7,?P\чG;׿iuì²ôÅ‹pª —[&ÉíÆ‹¬`"ß)U4×lóÍ8ç¬óÎ<÷ìóÏ@/!ù ,(PdýH° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹3jÜȱ£Ç CŠI²¤É“(Sª\ɲ¥Ë—0cÊœI³¦Í›8sêÜɳ§ÏŸ@ƒì'”á¿Eö;J4iÁ¥Gÿ5uÚjT©T¯júÓªÖ«\uVýJ–)Ϊc˪•Šöè̵pɆu·nTšvëÎué5¯\š}ýj­Xð]À†ËÎ$Z8ñÞ•cYò[ÉŠefUù¨?~_G퇯žézùü©­·"4ä¾ýöI=M»该ë¹6™¶l¾µk³Òºï´°¼ëæ+ü´=­Â“ìì9P æ´õEeN[úȲüýòYÑ•vçQ­>¹ãRÆ}ùé³b%+ÕWÏŸŽº¢¹•ªá·~ýÜ“ÚZúèWy´µvÝcý³O ûhÇŠlÿàsÛZþ˜f…=ØÕ_=øµ„µÕÏr±-•OeþÔkù`Zn£…Ux+rÕ‹k55c=Ü÷O‡ºñƒ"FKZ ^å3œZC™–Ï=çé>PN·T}e€_q5#} ê³äFýð#[˜´VÖ\ý@Y ˆ´Ù£¡Y%õƒŸjüXñ›VþÜ·$ZˆZ`ŸWQèãUûàsâš!wR['j%eŽ)ªÒRúùdRú”?¦r´”?ƒFåÎÏ£–îU…[Á+LˆtÕeÎIÙVU¢Ï>ù7Xa©ŠÔª¬L¹•Œ½D]´ÈJ+ÙT¢òùio~™-JÎò“m\Þ–‰·ûŒäS¾–”­¸y-JPcÉVT?Îj…o¢EV¯DËn¨g U†(?–õÙº MËÂ̰B—±ZU©«m¿‚ýËq´a~ûq¼lKÆK ¬—ª¹²Û.ÅÝžûrB*cìqŠ^É»’þÞ1Zñœ¨ÏNmôÑH'­ôÒL7íôÓP'!ù ,2PdýH° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹3jÜȱ£Ç CŠI²¤É“(Sª\ɲ¥Ë—0cÊœI³¦Í›8sêÜɳ§ÏŸ@ƒ ì÷OèB¢EDúÏŸÒ‚Lÿýë÷T ?©X¥R5z5«×­>£zýгŸY³cÓ’ ¦Ú·cÑjٮݬm_н —æ^¾ióê|·æ_ÂY #îëwñ[Á+‰ö«ëx-ÌÕ§Î̬2KÌw)k–5_½Óõ¬ˆÎНžå”g±úã‡ôêÛŽê9$h+ÿt߯íÕj|xO‚–ˆßðáö¼šF]·7ÇåXõ±z>|_Öá+ý°Z׈ٟ\©Û¹£/ÕùpäR?"eåÕØ@Vòé äý»zÔXµö\>s]—] úˆ§>ú`¦Ï§%øÏ ·åcÛxýÊV¬ÀŠ„ü¬p_§ÝC!wRÙ¶›†V$W? 2Õ>ÂEU?§M÷œŒòƒ^‹]µÏ>(`ŸTDÚ8T„ÿ™wšTæ‰äÏ>¬ô³BtX姤ýØSO?*r×O öȤER©&¡TöÔØÙP‚ a=øTYRSn2ÇŒo”ÏYœ]>þ8e’yYÑøÏšZbgBCîsV?Cʆ’x5ò‰ÕvF”lÙÜ™Uj¨ÒÅ7QÊáµÒ—v£í´×yXñãcR9ýuc\|’ªQõýc«­“Žå¦~!…` ¬öÖš¾Ê+ý-¦«‘íeviáÓU´–'a¬Üë©iºØ>ZªÒoÜ¢w£M½k?Îþ³¹±¥…ng´fåOƒf ¥Ö¿q 0Ao¡XƒeWÁ#aÖÖr[qK1HCe—U„ëÐa‚-Çåb"74+BÇ+žH“n¼Ô›.c;ØZ5Ÿ{S‡|ÓYWÊ.ÅltUH'­ôÒL7íôÓPG-õÔR!ù ,<PdýH° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹3jÜȱ£Ç CŠI²¤É“(Sª\ɲ¥Ë—0cÊœI³¦Í›8sêÜɳ§ÏŸ4ûuØïßÐ…Eÿ=z0©Ò¥L:} •éTªU^ÅúO¨Ï­\Ÿzµ 6¬Ù~^‹Î,k¶íÓµnãr “­\®AïêÍ«7nM»}³Ö ìVfQ´„ÛÒué/±ÛÅ-†ÜÒ±b¸Xõåc%—VÊ)Ñ"~НžiÓúÚÞ«×XìÊÑXó)]qº¶½°úPS2î>~¬j ¿‡5øéÝ&%ÿÛ·¶ðÚû¨ —­”7GåËû=þÔ8t¥þý@ŠV¶Ÿæ÷¦nþ4÷sÏ]=în >ß=+üøõóGÿ_éõ¦%µÚi÷ÕcÏx×ýÃÊ>ù¤Ö>Vô7—iö¬ç sõPw xš)¨b¬ÀW?Þóßs³™F+ùXgQRE᳂Xr ¥]†ëyf…iOµØ+& H~&ÐúT¸]QîÃJ .^dž‚VØ3älœ]F^iöh+øÜ%IJEçÕísåSý%?š†O|$Ý?ð5ÆUVP'Ø‘üà£äiøÀ¥F°uŸŸJõù'Aù‰&rÉùcãSøaeéD”¦´ÛmxIäÔ¡ ²IÞBtØIhI(ÌÔ?ÄÍeX¢ ô>Yév®‘÷+èaª*ñª”IñWdu,ñÊÏo¹ÞõÚ]há­^á…6"Z¾&–mJËözdb¤vd×bØ™W®GáÒIdVÄËí´g5å–?ùðÓZdZdW¾o±.ováË,½aW¾’®›QY.¶«àf£¢1R Ëš\ Ù%¢¸uIj?kRå Æ%]5ò±X•¬kr’>äT~ÜoNƼ“r1ÕðÏG%ôÐDmôÑH'­ôÒL7í4D!ù ,FPdýH° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹3jÜȱ£Ç CŠI²¤É“(Sª\ɲ¥Ë—0cÊœI³¦Í›8sêÜɳ§ÏŸ@ƒ ü'ta¿D‹<Š4©ÒL›:UUêÔ U­^í™U+R®]½J%ØofX±b«šEËV¬¿µmã6-+“ŸÜ»5ïÆ}KSo[º3ý²Ó®`´2ýý;{x±Ìƃe2nLØåäÕ-Kõ—¾|ŠÑ2<÷e¿ÓýBÿ³R¯µk}bÕ[a•åi­£W»Þ]ŸÖ~®Uoyí¾¼y—ºÂµ½±&/ÿó§o_rÞRùñ¶Ršä]œý¯ïÔô^rßH3s,®•_¾|Võ‰w}¯©x}§A2íG>»¾|Õ ·Ø|®!åÏn÷°Òœz5åÏ>ÜMg>úh•yrÇŠk FýxO?ûì³a=q…ØÚ ¬]‡THYñaF¡ñÇÏQûô‡˜Àµ&ßu¾áÓZŒ3fôO ÷(PlG2¹c?Í-ixÜ%iO>EfÔ+úðC[ŒÏ¡Å­õ8Ÿg7–„|ýùcE Ë͵aö£Ý|ö¤iÒ‘Ruù>¹™B7ÚYèi¤e¹QTÔ!uO}¿I¤J¨5ˆqJ¥hAÐ¥Ä+ä%èÕ¦P}¥RT2.i¤¤Ñj™tÿÜŸ“ÊÚ¦×>YY+«¶ÞU©Tù舭§r ºÙ›hÕº^Š€i*j¹ÙþEº“™ â_„m¨X\R+/FòJpȺˆjƒ–oAU¹)ؾ1VYWVô™wì–Š›·á–$1aŒá³«»$…õ0[Ÿéۤšfv™>§H)j ]FÝ=þV0M[Åœ¾ž³Ža–?>?eôÑH'­ôÒL7íôÓPG-õ@!ù ,PPdýH° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹3jÜȱ£Ç CŠI²¤É“(Sª\ɲ¥Ë—0cÊœI³¦Í›8sêÜɳ§ÏŸ@ƒ }èïßЄýþýówÔ`R¥ÿú5øjÔ©U­^šUëÖŸ]½*•Ú3¬X¨dmž]Ëv¬Ì¶pµ¦…i6®XšuíZ “ŸÞ¶|_úý»¶æ`Âwñ">+³ŸÔ¼„™Æ\ܶ1åµY^>›yeW}+êÝ Äv_½¢{-­Çºõi±ýXë“ë¹ê¨üžºÞ-Ö^k­)mŸõwïŸéÝ­‹[=Î:ŸÕ“•æó—¹ëÃÿ|»ž­Ôdt¥øý²[o^©?ëå;sÌËêvQíãëAe¾Û±z‹O“†í§_>VZÁ7wÕ‘ÇO=÷]TÕ>üYˆ(}Ö)W`=ÜÝ“ EEñÃÆEÕŸ>·ÁÖÚÖ¨k+(Å Hý0؆~QWbb±Õ“>ãEÅi¬l¨ T¬ !w˜ ”ã="Ç öäFRRûìcˆJ­A±ñÃc|öŒ&dFI¢iKღV Åe?ü4¹[>³AçÏ>!çO„^±òâA¹Ù'hV'ù“žÿh(–dU5&™óI¨f’ªR~øƒ¤W*ßJDúÕÏšœ–•rÿ°‚m:-ˆ½¤}”Þ”Õth kMY±²)YyuêVøÜ6W]¾ŽÖ™©Qu+KÂyÅO‹µr¹¬wÍl²ÊrÝ_VHèVA½NY·ØJ˘±‘ás˜cë©}ö¤£ ÕÕf¹}·Ütý¨› f°m¤¯ˆÜíã-+ !Vl»pijÅø¤°FßÁ WfSlYvaLØÂé›ÙÀßn ØA$ƒ¬`^Œ:u±Êd o½f:ÕÍ8ç¬óÎ<÷ìóÏ@-´Ï!ù ,ZPdýH° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹3jÜȱ£Ç CŠI²¤É“(Sª\ɲ¥Ë—0cÊœI³¦Í›8sêÜɳ§ÏŸ@ƒ møo¨Â~ÿŠ=˜4éÒ‚H›*}UêÔ¡þ¬JZUkÓ ]½~د¬Í²bÓþC›´ßLµp½ÒŒK7©¿¹uãº6¯Ö½2ûúÝJSð`§…‹ÓðaÆ-‘²U¬UfVÊ‹ebÎÜó¾¿o›ö[Q¯ž½|jïÕãg²Ê²“Y•ž]µVÙ¥­®œ,Õ¶=Ú´mKý]Zᓎÿác 8«ÞÀY#6™üŸ•@ûšÓ¾7¼ùóé#ý«ÿÓç/ŸvÚMýê:#ì~þ÷Ë÷½)îóõµ¥½Â¾zíiT+ú×Û=ùr™Tø•¶BRüÌöà?؉$•>ø|¶–>.¨•µwÙlMé FÿðcÅ=,Zâ ŠŸzõìbsÜý£šm&†§¡=úÙXŸXd™·Úy9×ãHê³ÂwúÄH¤ýd—_ƒùì£+'rôÏ>øôs†øähU ÏÖ=öŒÖà„Ô}æÏ Oú—å_ù,)—ûôy#úµEuHù³kVHç•Yí[Ÿ°IÕåFHj]RQùפUÅ)¥•â“>þYÊC‚¦Tk£%uê§ZŸºv¢y¸hNUŽ£@z±ôÞ¯°‰:ªhÂ+GÕõ ¡pPÁª*\66…Ïww5›Vµ½çQrûÇÏ®%§fWÇ:d˜¡ÂýCŸ] 9¶—|ÈzÕ­©þÕnZ‘‚F©?þ°Òg>ù„iU>ÒMꮦÄ^ô^ ä©O üØš”£Œ.$ž¾ñVW>3tqk}lU|‰¼VÈŠa‘ÉåZë—(ÓÕ²ËuÑ:3Y2+œ?¯ÞLe¯il•¿Yzš±±È[V¦ ½˜Ï%7skP&iÕ…ÁöÔÖ\wíõ×`‡-öØd—möØ!ù ,dPdýH° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹3jÜȱ£Ç CŠI²¤É“(Sª\ɲ¥Ë—0cÊœI³¦Í›8sêÜɳ§ÏŸ@ƒ uøo¨BÿŠ5ˆ4©Ò¥ ú9 UêTªB­^½TëV®<û5ýJ¶ŸY§ýbz%ËökZ™cÛÊuJsíܶ5ïÞ};Sï\›~åÌ–¯ZÂxaJ=‹˜¬ÌÆr »„\X¦Ý¬ KžŒvE½Ïø2³½·ïêæ“fSkí÷¹õg+d­Ô³6åå¥ÿyvÝ:ßÖ}­s'EyÛ©¾¬y»í”7Ö’Åú®¼5m§üx_GNRìÞùýª»Öz¯zÓÓUó‹›´Ÿ>+Ì“ßúxRñè3jÍg»>}ø¬w•?ô}¶kIÙ“Ÿ~üü—OhIíƒO>ñ™†hâåÖšS÷,˜?ÿø£?üc>왦À|õd'^‚õøöO b””? –hbdÙSÏ=,*wr³‰X£ûP¨hùDÇ—>ŸYVðsdzH5Ȥp[Ƭå•%_nùpéÔ>þ´O ú’Oy¼­OS¨ýƒÏT+ìÛ€m.´Ï>ÿ²OTh¡Ær}úC¨[yuRRùTŠšj¶©¦ÐMå„Iás™+­Õ§§÷¤ÈN^I¨Ìf {Bj“Wýà£O¦ÞÉ:S\þX‘fŠiÝFêHkéS\°•Áô›Œe $ìK—Y¡j¢ˆ:ÆÒmo¶eX²Ä±õé´«Ä­IÑQè–YL±…Ztq:åO ëåwl§mñ+vûð£PqŠf+Œ[пȩÖQtßj‹Á*jä$cÄpÃéUÆðiO5lBÏ:;ƈm¼°®+lPÆÔê§1ÈW˜È§¶ï¨#¿Ü’j2sìº>A\ÌÐâl0TDmôÑH'­ôÒL7íôÒ!ù ,nPdýH° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹3jÜȱ£Ç CŠI²¤É“(Sª\ɲ¥Ë—0cÊœI³¦Í›8sêÜɳ§ÏŸ@ƒ*è'ta¿ÿŠ"<Š4©R‚L›:}Uª?¥ýüIÝJhÕ­R»îì÷,ׂba–5Ë–lÕ™lãÊý—öåÜ»[iâÝ[×îÞ»5ÿÖ+XnàÂqeþó·1R™#íËRr\Ê+#;Ƽ’«|øÝeµõjK·n›²ªÇºµ¾¸êí «2«Y}GýµÞ]ŸY~¬WÐ6©ù¿|ÿôñÞýzënÑ“MjHô½å»§#ÍÇ›4Ý’Åýco¼©½åL9[dʸl¿|V K¿{þò}L=æ>ZO~¿@ðí£T+ÐÇ?HÇš=üœ§ÞEþä#¡=+4—>ÞÅ¥\=×awRû°öarù<¸žj ˆß\DõÃڅ㵚=MÕc¢EZñ£>#â&WWýœW‚Ø!UpþØsãzüˆÆ tûÌvÙ@.ÊFù¤dI ®P+#²EÐ>×hà ¡-‰R¾5§ÏP‰IV”.·+¦•?÷ìcEiG-„_?üDÉYR¡ƒÿX!eSyJ4ÜHnݳÂ=Ò%\i½%ÒWQµØÜ¤5¥¦Dš‰Ï€H±rÏSddQÙϦgÍ„¨Y±ÊúOh¸B6¥@©MFk^jÅ5è­[9Ú«Kõóh\`.ËR³ÏÊ&©µáŠì‚ujGkµ¸×“Ò¦Ô,œxÙæmfm{—¸åžt.º[ ø@¬šš­´îEécMéqíºË¢l*¥¿ÒcmÅ nÁT²˜é_ßB4oÅrɇqHá™?óõi­EÖ$bG„šzÛªVrº4ÅÌJµï¾úrÌ—µ|ÏúÓι>eôÑH'­ôÒL7íôÓPGýP@!ù ,xPdýH° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹3jÜȱ£Ç CŠI²¤É“(Sª\ɲ¥Ë—0cÊœI³¦Í›8sêÜɳ§ÏŸ2ÿýÊP¨P¢ ýýç©Á¥Fû9ØêR§U­BšU«ÕœýÂ*õJ¶)ÕªRc’]»´ëÑ lã¶)·î¿´jíÆ¥©—-Þ¼}½þ…˜¬Ì°n _™Xñ\ÀŽ¡nÙ8²YÈBýíÛWyïJĈ¸µR¯t={…Öã·5eX­ýô dºv ¯÷êÝ{l2µÐªk×f •Ÿé±wOúº›•páVkÛ‹JòuÜ~Ó?7½o©>á²ýß~}Û*«}ˆócõÏùöÒø–Ú{Þ=¹Ç«å…Æ^a%ûèï•¶ÔvùPÇQUè£å±b…>û §Ui¤mWß>¦±fÜdõÓ?ùäVÕ=Ë……O=T(Ünÿ`XOW+p¨Tøä“ ~QU?ó=§T?§e…Œ3úƒ>V×bx† ä|'>§d= ¬7äHþäÓƒ¢µ—ŸW©[>ï­àO?+èF$GÌýÃO|F®…ˆõ ™[™ü„Õ[‹BuÇŠV!Whýìs§pøt·¦GnÝcE}B²¢Ï¢œõÃÏ¥ÖݵhGYúŽK¢+›ÄJÿ¥G£xuuã™rFZœl¥”Õx‰™©PöŒ Õ=?¦>±h”ƒ¬R6ì@õXe{µ†´\[T*°B ûüwl|C)+'b÷$ª-+_ö)›·‚ýÕ˜ªPù\KrjU?ÜÎÛ­JJf«³êËY&æa“Ÿ|Pbø7/¼‚!ä? j…®¿-eE¾ ›Bc!i0K•-Z•»3•Ÿž(…ÌЙÚ^-FÕZrŽ%ÅÜд`î™±B8ç\óÎ<;–2Ê7+ö²J=S –^Dï„Ø·GÏDhhSUmõÕXg­õÖ\wíõ×`‡-R@!ù ,‚PdýH° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹3jÜȱ£Ç CŠI²¤É“(Sª\ɲ¥Ë—0cÊœI³¦Í›8sêÜɳ§ÏŸ@ƒ*è÷OèÂH"ô‡ôŸ?¥›6íUS©M•ÅŠ5èV®\{öû ,A¢Ta’-ËéX©3ÛÊ›6æÜ»Sãâ½Ksïܺvý¶­)xpÌ´…ÙÊ\›+`—Ù>nÉ8ò¿É--›],µß½zõì哈UW–c¯^ÞÚ´ëЕÿå«gEêÓ”±ÿáCúº7Ø}®;£œÈðÞ®wc}}/oÉÜRk[AþZõ¿é¯÷¹}.y­=ê®ýõIýÞÛtÑcÓ¯ågå>+£ÇƒŽÔøíU³ $)¿÷û˜ÇÕlóÕ³Õq Ùófýà ?º±ÒŸ?öÈåOkõˆœrú¸¦} bÔqìéã>Dˆ\µÖÞ}ÿtX„1†(bSûÜ“y¥µ•†€·[kþÃ7ž¨Eþt?‚¦yÈI7ã“$É”>V!¥e ´Ï ~6>úŒÅ“ý¢Ý?öôGAûŒµÏ>T¾¶B|'¦“ø¬È+6ÖÙÏêÁÕgS¬äãO|MµgâDÂõ9–xý@Š”æ!yÐWžz´Õ£Xåóæee…ÛTš)×'ª;½Å*Vì9–“¬­ÕOm§Ñ”ÞœkùÃk¥jA9WÂnFtÛÅÖ*¬+áêã±e «j´w©™›>¯B»ª…?J¦±(1«f‹má#àµåÊtû<jGæžÕ–°Ö9¨R½1‹Ï©8žk’±ý²õ>öX‘&]ó^D0ºÝ¹õß³ø‰”ÛcÌr¥ðœ#Å6YÆXù³BY STf c¥ÀÎYl+B)7å&Ç«·PÌNæ[ñLÒúÈXɸñ«À‘éñÔ³[@ߤ^ÒU5íôÓPG-õÔTWmõÕU!ù ,ŒPdýH° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹3jÜȱ£Ç CŠI²¤É“(Sª\ɲ¥Ë—0cÊœI³¦Í›8sêÜɳ§ÏŸ@ƒ d%”!«Eý‡4iÁ¥L›:U5ªT¡U­þ#4«V¦\{zý5,αdµeµÔlK´iãZ WnZ·.ëÚUKSïÞ²}ÿþ­éW0aÁ{e²e‹Ø.^– Ç,ܘnd»–¿Rþšì  ’ `…V–ŒµŽfºµi²ŸœF™šl £±[ƒ¶òÕJèÕLOú]¬[wUVºç–,|4PñÖ¼£æÞuù]V·­ú~ÚêóÕ ý Ô¨|yÕ܃å¾ÔcV¨¬VO›ôkçê­<ÖößíèþÍGV|¥mW\s¡-eEHl™§Ÿ~LxU áW\tÔù·_FðYa„˜QX w£}6Úm")ˆ‡Ú…¨@lwÛ +°E’†[I(¡f!@ŒÏÕ¸!Gm•×ÖŽ³ tvºvÞV&5§•‚ecBKbg|ÂYå\o%QU´Åâ”|A¤\Jl!‰ä§ÒQTª…$”;)¨Zp¾TV•‹ xVYZÅÕX}²©ÝxRÅ£b^ÖWX^%:RVVihAd½•ÕŠš¦9 ´yˆ¤{áEêrcÕù}^ˆ%ÞG|†úèS¾ç^\Ù9¶¢«r(W¯ä¦Ðf–:T±j-ökdÉ.¤³“´XéU›\]ÛèEpÙÈœµEkÔž G®`æ6äì•馺®¼+i‹Ð¶è¾Uí±ï:;Y¥SõønÀlðÁ'¬ð 7ìðÃ!ù ,–PdýH° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹3jÜȱ£Ç CŠI²¤É“(Sª\ɲ¥Ë—(ÿÁôØï_¿™ÿýógP&N‰5uú3¢N7MZ´áQ¤J—6môi¿ªD§&Äú´«M­¹zµ öàØ³YËFE;–iY¶lÝ6 ·+Ï©tëv• ÓŸ^½?óþ Sð`ü…yí¾zõøuuiø/dÈ÷X¥e©¸ñQ}—/ó]YîŠÐ÷áôü5äѤãjnëºì•gA_Þçµj}ønŸ|õн¡ñýÛ§zæØ|µëÙÃZ³p•^£‹>jï²ßë)ý±Ò¾ýßxÈ’Á‡?Šüå@:Û×÷Oýú÷Ü_¾WsŸ•úØ—’NÐéçš=RáT?·Wa^×à=V%àG‚Yq>Èçš>Í]õK¥ý“OwõäSàe E×>’YÅÜiV¢ˆÍæ•ðM&P}ýä8×^÷äÓ–ZC=µg]øRW+tæÕ]H*€?V°E%’âõØd•ké¤Ïlgæ?VædK\­¥’;­ÞMR"Æägöð#çIKÅ>^êôæž% ÊÛŽÌu•¨OZÁµ?1>¥ÜˆSi©LurhYýäcÅ¡ŸñÓXFR:—?\ùsÏWbéÓã[bj¦ªr7‰Õ)T`É ¤Ÿ>*]¥JÒU¬ä³B‡W)PçgßáÅ–W †%®3•ÈÖÂjÔϲžYc¶‰Èš¢ošªR‰¢ÂåO”‹± W´¥²…O¤àVTYR¥íh½Ýûk]ü(@¦¹&ùû¯ºýÔh¿æ–µÿèS.R /ä°AßIÖ°h1+—öÆñ‘aýÕO  Šdëu%úòƒÃF›,UuéK2§h±²ªÈ`ŠåÊ6U\’ÌDGkæÑH'­ôÒL7íôÓPG-õÔTŸ!ù ,–PdýH° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹3jÜȱ£Ç CŠI²¤É“(Sª\Éòa¿–_ü3ã?šûý“Y³âMž nîìIQçOFo(ôhS¥K!>Ý9hT…I§>õgõêL­`z5˜5ìTVc š]ûmÚ²lµvíW«?}ú ^…×_½¿õøyåwàz+G­;ÕïáÀQ ³å÷¸¾¥’Ùî« ¹ffÍœÃd –Õc}ùD¯ü\÷±Ø–¤ÃÞ¼âuKÖþöÚ§u3`Û·Íâ{ŒoêaC‰†µÇ¹ž•¦¾ÿæ›ËRëðæù 8¯Pº[ý±?$”Ÿ•ÚMƒO.þ0ózÞ¿Ã,˽=çúŒö£Ž2k}ûñW“P¦x˜jce `¾õ{€Ù³Â{ÝÓ`‚B]wØ ÷ä—] iåTÕ>VPøW þ€â@Ÿí×?0®Èb‹2ÄW>äÉW£ˆO±‚O|zí(ÐT Ö”Mé£åT†¨dXx!™•?QVe¡?f6…æTòÛp¢]SÓý£?.V>_òã]”5VåSFˆ“˜¥YÙT F×'Kû…u(t¼ýs(†B}ySžnwS^c=ÊÛ“yå7>O¢Xâê?þXA^?³æ%'‘ŽòùÖ>FYaå>QÒ:•¯?MºšP$ÞdEj 6uOSƪdÔ> èk?žÞ$hz!¢¹Û¨¼‚¥£Aî…l ì–‘:šû(+^‚…îSƒ%kXÍ–Ki¥öJÖÉ­ÆÚNö˜5îSÑv4ðQ™­@XÂ-¼_µkíƒZ¦C\T]<-œ»=Ž4pǤå0pµÔÂB¥ydH™uÅòMÊ%²\sͬg¶ÃìâÄX‘v2Ê=ñ»+. ô[FãüóÒ…>ýs“TWmõÕXg­õÖ\wíõ×`g!ù ,–PdýH° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹3jÜȱ£Ç CŠI²¤É“(SªLدßÊ‘ýþý{2¦L—4/Î$hSæ?œ9'ÞØÓ§? AúÜYt©Ï–-ý%MèoiS§X¥N=ˆµ«×[ ~ë_>¤aŽõÚ¯žÛzhîíjå­Û}i^kÏn={òî]»Âo½|y«ÎuzÏ0Ü´‹±êó«O_\šƒ×¶µ;t«âÈKý²êœ´d»VHç̼¸ïÛG§~6ýÔî=|¬z¾dítöÒ}¶÷9]Évã·VF;µ’ß½á)‹w\_ že«ëV9—õïwýWÏÿýÞ ™7“7|~êÞ@ë ãk¿ëôøoùåUà[=~úõuµžkõ˜VK¿}gE úä³Ïeûõ”y÷@¸Ÿ^2MæØ|^XPOàÃ\Kûtè!O^ØoK©¨ož¸—>ŸÅxáUü$ç”yaHjY¹ˆâRäóúTäXHNånL ©ÖRV°Ø¢” <%–NéóÕ|M¢×eoþ‡lRå—DZÇ ab¶œr2íHü¨vcoù°ùÿùÔ‰^"œLÏe˜ç zå ÿ蛑kêéäW–ÉÄÏŠöS¥LûÅÏ™°q*Ó™Øc©x])6ýjU€þHžŸÆIRU×ýæ¤þóf¤Ÿ=*ëh¶Šd“‘¬p+‚öcd>³Ý£Ï ?(²‹zYdˆ°>™[µaÙtg²ÆÄÊ¢•ZÕVÖþó\?‡Ú¹£Ÿ2±¡+Œí?ò.EîRö &“ûXÑO޾Š•‡tú ï^’áUù,Š>Mkì^þPÛj¯W®ã`¿bUtÅ 5XްY‚‘áô³ùäƒÅ;ÂdN)÷ƒ½V P)_Û0£ ”i¬€ŒôGŵ4$hø°¨1DéÅU´OŸæ T`'D›»&‡J¤?]­óÐ`«ýrÏX~ØöÜq×m÷Ýx ç­÷Þ|÷í÷߀W!ù ,–PdýH° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹3jÜȱ£Ç CŠI²¤É“(Sª¼Ø¯ßJ‘ÿþ¹|é±_L™49Ú¼‰“࿜ûùãs¦‚@úkÙ’¨Ó™<“&ê´jQžþ¤&´Ê•¨Öƒýòá£Úµêׂýê©­·³¬×³÷­UkÅ­S¸ Ù›[Ïž]¢Fµæã[oÅ_ž“¦%|ïðͬ_ûéÓÇWŸã›gwòmë81ͬø^~¬u§•¹–GÇ,=t®_ÕEiÞìÇ ß½¹œGCVItßíµû¬’uë¹$Ñ{øXY©KTùÜ{í7^_½±³Y'¬6xÙé'ýår¿–•[ ¾É«'Ž^=ù܈¥îÛîžo ªàWò«OØ{T¼âñ§Vn»Áà^îY…—@ýì“>œ6ž2å'U?¿‘—Ïl "´Ï|ã¥fa‡¶´>÷ÅÖáCÒ­èWºÖw2"Ô–?©V#ZÍYÁÏS;]LV¤øßŽðÝ”‘*Ö˜¤’Lþ„dWµ½µcWþ0‡YOÆ„•26ø¥U9®¶ãPÖíãOteöä¢i¬h¥>ÃÔNüx…ÊéØ!gÙ–>ùDgçJœñ“cc1íå–píDç?…ÆÔgL•â‡WLYö£èMü(S›`^“=÷S Tá#ýª‚™ý“å?ü0Y—?®rÚbdÿÄI)Y³Zaž—ÌYÖÏ}1*öÏ ú0Êé†ù씯ÿb^ ‡šdmª)+ŠKþ“?×6ùÕR¹MJ«y’­j^ŠÙ¦Äfn{6j£ñ¢µDÕëφ¬¬`E¾(9å(QYòÓû0GðIÑ] Ø>´ñÄÊŸ€uË?"öðHUaWÕ CVxaSN +]—E}ÌŒÿøØ"Ëkk?øä£ó°fuXK#=©>4«Xt|!%ÉJÇì1ø³HI&çAG»ù|¸^Ê]¹ÜÖZþå×@픳‡%ÄÔÚ^CD6Ó­ÈvÕæ†iÐA287Ûy÷í÷߀.øà„nøáˆ7!ù ,–PdýH° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹3jÜȱ£Ç CŠI²¤É“(Sª\É’`¿–"ÿ| ób¿4þ“Y³âÍ.ö¤¸s'ÍŸ8‡J,ZTR¡J>5ÊjT…ýüUÝZ4çUƒS¹nýz•سFÉ4‹¶­Ú°m¹zºSŸ>­q¹ªåW¯o=¼y™ÎíÉo…ßz+o “ïῊ«^Å÷¸¿È‚•:~¼sf•ëuöÜ•p>}Ù’¶ÊòçãÕUŸüi¸ï=ØLkºö;ºê¾@ûŸ•ò¦¿Ã[)Ƈ–xñ|‡{ÿ³ºž½³5™¢î¨èæêÌýǬºÂÊåªWÎZeÕíöÔW—^ºõÏ~újËÝýsÔ~ûU×_Zd}`_ª©5¨ @üÜs˜=+ÄWYgÎ]ˆcøÝ£Ÿ_á%õ`Aø“Þ„VìV†Wñãb?0¶Å¢‚p 6#YL’\#"ô>ªÕ×ã@pY1`SCê$–ޱ%)Zùé“  ÅÊ‘<%‰–?;ææäy\ù£dNÚuV—H&éO„på#Ü=ý³?úÜV•›þØ8¡¹?XŠ8â?]êæ?\ŠÕã?.îdEUWЧJ7…×–¬I¦ZÿD*'}žÊ5éIÿŒ¹>ìõãçNo’eì"«!v5¦©ZýæåU{îjQs–zS ¿ZY1vúhUÀÚ+Çö£â[LÙ)X>üäslŠB~¥Ý¡EùÓ°I6*©E±bêbû˜ú››Vu,Wt\¶W!(«úì*h½ÓžÈ»Mâú^8ŒCMe.Zöl#¥’&6œSŠKQ±¾FßbN9œ'HšeŪ ä1½Á¥ÉÍ™—ÅÕÈrËDz ³TrÝKsP%ƒ—˜˜1†ñÇ)‹uîË Á¨ôÍ ½S”/3=ÒÒK7‡0•5S­4Ö\wíõ×`‡-öØd—möÙh!ù ,–PdýH° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹3jÜȱ£Ç CŠI²¤É“(Sª\ɲ¥Ë—ÿÁôØï_¿™eô÷O'N‰= ö´ù3bÍ "-êP©N§>™.Ú³T¢R^¥JõfV„[¹õúU¨Ø³CË ‹viY¶mÝJå·.Ù¢p뎕ª·ï?Eý ¾Û’ª¿@‚í¦Ê¯^½}‰Û¾ìÉêžcÇ‘Ï^Ùïòe}™ ÏÜçÙñŠÐ{av.]µÜ–«K‡ew3Ë~øôÉKú3ÚŸû ãó uëÓhm«¬ÉÊsW{¬ç;û³¦¿ËöÆFwÎï¿ÆŽýÛÜîY|U¼û†×Ã×3yÏìÏãÕ·Â ä~–ß_¾'§RèúE7]Tªqe\€¥ñ“ZUõ£O~Öƒ\W3Õd•@ûè>¨Wm)÷R†¥å“cö ¨˜TÁé£O VHTü@&–ˆ£YµÏ@bÁh#…j­uV>÷@…#Saá÷ãkeÑu–?2ù•“gY¡à‚AˆyX)P\¬€F•—±Õˆc’™OKþcE”]zÙ?öˆYÕ=\öØ…oõ„æPès%WJ•ÕSp\׿¡h~uT|Œù³hwYš'`ÿèC¥XY¥â^þðc'£ù¹˜¨7*PÓ=›öãÒ©œõTjª5áÃê?a¦Õ©¬š $é>VúÃÊ>°Þöר¢YÅá ùÐVl¬nz£UVúìIGáçZöÄÏŸ®½Ú­Ÿ“fƧKCA¹©X¡öuíGJñi[¥Zµ¢±€– d^ïrä+ H›\R÷b댮êE¿se²qÝU›IWië×f´•>¬ v™$mH^X Ù«ÆgiêîBöòYè[rBVÕÜ/E[ÝÓñÀr^Ⱥ7¿dóÐ5«iôÑH'­ôÒL7íôÓPG-õÔ'!ù ,ŒPdýH° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹3jÜȱ£Ç CŠI²¤É“(Sª\ɲ¥Ë—0cÊœI³¦Í›8sêÜɳ§ÏŸ@² êðßP¢ ÿ)=Šô Ò§L›|J5jSVT³ZŠ5«×­<»z VfWVCÅŽ]kÔ&Û·o˪T ·®Ü•të²½;W¯^¾*ýÚ­).à”…ãÒÌ›*ÚÃ$ו)YqÌÊk!—¬ÌXsdª+¼  4€j=ÌZzÅÚЦM? ôUõFª¤K[ñj%vl׬hg¶}‘ªïÔ¾£lòiïßTŸ'/M›ñ׿ÿrû6>Ýôn¨ý–“ý̪{WíÝ…ÏÄ>¼WE²Ú^8ìî¢G£†ªUä÷òÔÕ¡í7šzVg”Rò ÷œRÒù–[Zpý— í¡Õ`v§q8¥U!TV$ÕHh­€z¾=·ÂTo}!†ÿ‘”"~º…8uF‡•‚%È}­‡V Åx{gÙ^†G*ôØXȨTu›‘Q%Z©êµ…ƒÑåå?%6‰™åuf‚Äá5–‰¸í'JÖ‰ÝR;u¦çlbåÔ|_Ñ)fMy1e]‰U!z¡™­æUÙ1Ú¨KŒ]ZW•aE©Ezj—¤µ¡Ÿƒšú}院‚DןZº —{ãuÔY®ÑJV¨›n„i e‡ëWÀ‹Ñ°öúXžÉÖºlb`1kT´Ò’JíAÖúJlFÖf ©a¬ò8ª­äöÊ|‰znµëîøì¹yëX½é¶4ï¼ ™+«©þ[î³lðÁ'¬ð 7ìðÃH!ù ,‚PdýH° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹3jÜȱ£Ç CŠI²¤É“(Sª\ɲ¥Ë—0cÊœI³¦Í›8sêÜɳ§ÏŸ@þë´á¿£Eú;úÏ_R„ý˜2}jPªT§TXÝJ4éÖ¯CuFí*¬Ù~diJEk¶íZ™QÝÊ•:3îܹjïâ­«Wn;nÿ6+xðWi[Ú5ü51ËÅŒ­ÂÜX&åÖ¥² ä6Ÿ½zõî-vÜr)S+õòí÷´ë¸lúC«Xªë}_]ë}ßY•vïéÞŠo·î}þ8>wßn+Wë†].éGY·'UŸt×Üý×B~;2îwÓÿ¾ƒ·šÏ ¾{VøYEKÿºÆ~õìµ{¨zЪ}ÅÊ>îÉw”>°ÂVá·”s éÓÛwù€Öz`Ùcš>¬øÆ‚"õ#á?üDxÔwðÕÓ•[pÖ>þèröeÔŸEUœtXˆX[›e—Ï=ý…b %R÷=Þ©øãYÊýcúÀhdˆ"îˆ"~+ì3P[üŒè”„þÈgÒQù¬ =û샖—Ç”>áíH?ÊÕ¸‘xoºÙœP)¸U øDIâ•!Ò%QŒð±çO>‚"ZSz+Õ1•OTúЗÒTÙµO€ššV)_s¨©]§ÂÓY|ìÖjK[YYlâá4ž†%§§yYÅ+W ÈI鬓²·Z±Ö! R­¿;Ÿ³A¦Ïˆ•áÚµ‡´]]h­Vu¸mœÒÅ­[åSdeqºµ.F‘Ê6ìoñž‹Ò›[í㛾OâK’¶”Êh>ø¨Jl¾Ë V> ÖnZ;¼•Œ€%n²[­Ðë¸;¯Cr^¤ýʱTvfœÐ­[•\_zÖ72Dã|ÖÍ!ÕÇëñ¼’ÎC ý’ÍY%­ôÒL7íôÓPG-õÔT!ù ,xPdýH° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹3jÜȱ£Ç CŠI²¤É“(Sª\ɲ¥Ë—0cÊœI³¦Í›8sêÜɳ§Ïÿþý|4èP†þŠþów4a?¥E›„ª´ŸÔT©ZmšµëÖ›Oû}MÚµ¬?±_aB}Z¶mT™lÝÊ}3î\¹4ïÞ¥iWoW¾~ÝÖ Üv0a¯bën›¶eßÅJe’…¼Væá~ûöM¦«V)¿zŒíÕ]ÏJUhѲ´{¯Þ½®HËTßã‰SÆõGšÕϲe]á¶ñȸ¢IS œÕ¿×ÅOÕ\ŸÒ}Ìeû¶w[«ñŽlý±·§_vÒÎYùþÇ/Ð>çT¥þ7_v¥ç•SeÈÊ ÛJ¢@d!õSoÀÕ³OPâ1gÅhmù³>VÀ' >úÐöFý” Õšƒ±á#Vq¯xO>íY±!‡¦%ÚS¼1gp[uW”æƒTÇUÈ ý³Bvø˜WS¹¥Ï‚FòˆOR$õÓÚ ýøsdvùŒø•[8gU…ýäÃäqýð³ev÷ôó ‹éøÏ”súIOí£dp÷ì“ZcEmf”sP.ø"G'~˜&?üˆµÏBýèÃJŒEíctžd×Dý°¢mJ™æg¦'MvhBAý訞®DèjDOé³^Q}ujM4b”§äU%– ¹YöO…PÝ#ªwÝZÒS“.7²s);RªðÅÊ>¬ P=rVæÒ?OÆWíµø´‰Vt-)({ ž§œÒ~„[W&jUœ.ug©½÷šõmW.zÛï_ŽuÅ´¸¤c¼ò|Væî]º¯^ ·úê©^Í™iÛ´.Ý®PÍ/,¾Õ¸ëUí§/lW•fáÞš:÷êÞùjÃÅü,«Üù¸Ç}ï_ Ótý•Ûϸ=¨ùý¦çË|#ÓÒÆ¡Š5•|Ój.±»=~öV+ç·^õ ßû¼Hpõµ[HýäÓÛ?÷¨¶Q Ná‚o±‚Otÿ賂=ùt¨‚•gÝMw¡jC=f•{Ö)ÐZ\"õcOU³µõ؃”ŠQ-x>úÔöaŒö]~â¡×Fíó uœÂ}õãÙlëå#Ü=û×=­å³Â“'ùà –Æq¹OZ]zyÖP ö†Q&±Õ?NNÙO› h•ûÜ£ÙQTV UEƒ6iÅ?ø]U§£%jÔ `ñsÏ ÷´µ^=¦OŠQ]¦Ó‹KùƒÏŽËݤ"¥K2YÄ(J*zF(Ai½ùjIgQlY½ukŒaˆV[O¢“Š`Öµæ›/!›×²Æ¶¤bhxµ-K±’šªŠ¿v¤bWyzmJ<Š›ªå®ÄãZùSldè2k’¦Lfå»ÐÂï¸ Íe¥aLBš.°b½)éµ®"(Ø”t•%os…açkAC,X CJ+¿|–”ä/Á¢­k ½ÖíDoµ¶¼ÒTkµ*ǬjÆ®­,WÍ:cåóÏ@-ôÐDmôÑH !ù ,dPdýH° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹3jÜȱ£Ç CŠI²¤É“(Sª\ɲ¥Ë—0cÊœI³¦Í›8sêÜɳ§ÏŸ@ƒvü'´á¿þŠ* Sòd¦2-ße6³Õ}÷Ú²ÂW¯ôŠÂ4­Ú«gå«•Ò°Kï-Krɨû`ïÓš/vìGwwVÕ·UV¾}ÕçÖvǽ«có‹=yéÝùä:×ÈÖŸõÐGýûY]³Ö´&Å[gªo<ì@Ç­è³ì_í‘ýìÍ´úxVÕ‡>úL•>Ù…'R?àý[pã‘V>æýÃJ>ø7Z>Ú‡|ÿô¦ŸƒãñSZ Tˆ>­Â>zˆŒ«uÏxÜhÏ@*þÓÚ? Q$õÃ|½¹HiúðØV? ²¢Ï…»¥·¤{¥!g?U¸O”"µFMå³Br÷äd ûäpŸ%¸+?¦G–û©Ï>m.äÏQY±pLáÕIRI¤Ùžožö‚cr§`Dš}>ùdzOšõcèQ£zhNšùs¨ƒ^…“yø€ÈÑÔ>&j“yiiµ>ʺÖ[)ž÷¥¢ÖU™b½¾Åœ®/™‡•ŠùÀ‰LÄû•??ªÚR…]¶Å&¯+a›í´øˆj-JhDÛV>Y´õS_ €ùd¤‰‹¬Aüìc`¡ûrËQm=ŽÚîY+p¦îF[Šþ„{pF +ü­bf±{QÄ÷ ”ðTÒ»Æwš«“Ãaò¤ 7—²V+t²À—Üea-,ØbÚvß¿ë pÍÏõX–R?ïLÕÑH'­ôÒL7íôÓPG-õC!ù ,PPdýH° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹3jÜȱ£Ç CŠI²¤É“(Sª\ɲ¥Ë—0cÊœI³¦Í›8sêÜɳ§ÏŸ@ƒzü'´á¿ýŠ*ø\ØYHgÚQÛAgÅ>«ùwYûÌg…÷äh…$Yè[‚ÿ´Ýrû +/þHvûPhQ?¬è³påÙWO“F¾µÏ…¬¬ Dñöåµf=W¡ÅJ ]ÝCÝ?ûÜÓ> •„W dÆöâ>M²B…û}–O|]®0–#!¦Ÿ‹DYü€ÇÏ>sõž“áà£d±²åQ*Y™'.´TÄ÷&>l¶eòÓ©LM9 Zü¸ú*\þÅÊæ¨gé¥@¬o"Z¬Q „×›ÿàóß²º­D¬YLå&ŸáyôŸA3–…íQ¬ ›du)š&¸Å æ?ù€ÆV¸Ú*ëbç±ò º”NÆ®bœ*¶Sñ¡šn»ÊÊ»¦ÅϼHá“Oºû°â÷†Ü¢]ùÃO æH ×^D¬½……p]¶Nô±Wõç¦üEÙòÅòŸyÍ,”‰Ùl,ɹ«ÌlóˆÒtµ)[5eÃ…élP¾(÷ •§"2[rÀüyõhŠNÕr³^‡-öØd—möÙh§­öÚl·=P@!ù ,PPdýH° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹3jÜȱ£Ç CŠI²¤É“(Sª\ɲ¥Ë—0cÊœI³¦Í›8sêÜɳ§ÏŸ@ƒnüçO(Cÿþõ3š°_Ò¤K™túªÔT«Z•šUëV¡]½>ú3¬Øªdkš=ËÖë̵mãÒŒK-M¸uµ¦…‰7ïS~5ýƽµê«WoígÖ°[M\¶=±±sCå—ÕŸd–UóÅ]•tnØ¡ï9öê;åÓÕ±oÿ=;Ÿ¿|t'¤ŠúòîÔýY߯WW{GÉýÂÿÞ6ѧþXe6™žxÖØÿAo/ý)«|øèc–ST™—Q?÷$µ_~Ƶ·žV¬bE U ¸žõCÜ ¬ÆJxüÀÖ—?úèThVðÃQ#õ#_ ¬9>µ&P\×!ÅÛ=’íƒáüØS>†—qQmvÚ?¬¬@áW"õ3šíábZmõ³ÂSVX±Oh?j¤_ÔåÃÛ^J=øT ø8ˆ>†™ZèÕÙbSí#„øC\hû4GRVr´Z þó`?E•d×Cx±™+žT DxéãO —ö©Sü<ù§‡íùO¢Jí„zÏUš½^õ–”wÍו>¦¦j\iõFÞ£oÍkUozUè ÂûÔ +2w,H«NU•?m¶åÛ³E+­V†­EÉbU+VZ´è)À+B’ýU­·ç饿lME¤a­«BwâÃh>óf™­b ™J©‚æõíA›‰Å.[þX¡ivãkPüQ| Á祬^ ?ì×d ëjéÈ kœR»q™g./1 †vÖyÕÍ8ç¬óÎ<÷ìóÏ@-ôÎ!ù ,FPdýH° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹3jÜȱ£Ç CŠI²¤É“(Sª\ɲ¥Ë—0cÊœI³¦Í›8sêÜɳ§ÏŸ@þ êðß¿~D5Š4éÁ¥K›:ªT§U«^ šµëVœW»ŠeúU¦Õ±h­Îô—¶­Z™ýÜÊ-ÛR®[~4ÙÚE[3îÞ±}ÿ¦)x,^¸…ÅÆõÓo⮈g¥«Ò±ä¨‘—ò „Ö_>|øòéeJêŠzœ»ê«Çºµ•¥þúɦ|ª¿Ö–âkÍ»ÞëÍ“i‹„j¯õŠªûz÷þ·­l”Ž­ôæUyï}úF®ÑñîÞ÷ý–²Î[´v¾$e¯¶¾ôùÖúªæËGÝ+÷Œýê­`å¾µÞ÷¸UåvùèSŸx~ÝgQ?©IÇ+ÿ8ø^xcù£>VèeÅ>£‰ÔÏkFµ¢uVœVO>¹uõà>ûôs>(ŽôáˆøüórëÕÓTZ4×Ïf~1&c>ö(ž„½Ù³Þ ;¢¥Oj«ÉYI?‚ Ž¬Iu^Uö€øÏ üèÊ‚ýXÜ{üÈA~µˆ\ ÅY>FQ©Ùl?¦ùcB²Å [aB5eIoA”â?÷P˜f&A%Ñ¡ûX‘ÚQÏ¥Dš¡]ñ÷O õÓ¡ˆÆ5ã¥;zO§ÿäƒ*™,ºÒQÅ«)º¡|F5M ò« iU ¨úÈU®ÛÑú¨ùxÉTss¹ꦿ’5­VÒŽ§vÉæ–²]ËŸeylK׺Hn¹m[f?Ø¡* >Ü–{­§'™û?li”¯¶t­vîå'©Ý:W°]þ8‹°ÓºѽY2i¡£'ã^ûÔ¨›bì¡]Ÿ) òd&Qܱ± l™ÄÍ6Y /ú‹±ËÂÖŠç> ²O“ET?þæ<ÕÑH'­ôÒL7íôÓPG-õÔ!ù ,<PdýH° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹3jÜȱ£Ç CŠI²¤É“(Sª\ɲ¥Ë—0cÊœI³¦Í›8sêÜɳ§Oý~BüThÃH‹Mˆ´©Ò¥›J} UÔ«Tö»Ê5kÏ­\ÃÕ9VA?°bÓ:©¶mX¯,Ѻu —åÜ»uWÞ›Wï^µ}Uþ\spÚÀ(垺1IÅ‹¥ú“ 9rRÊ–ÃÎTÌÏ-«|úºn–\ï^Z}õR§ÆÇø,L¹¨ë…æjOµíHó½uÜQ®mVWïÙΊ߾¶)Ñæ.ußðá+ö§kÒRçÄ› n»ßtê#ýÁòã>ûwæNï­]9èYÞß×¶²=µi¢çU³nêÏ_?~üЗuüÉR?öä¦nÿÄö\mõTÆŸø×”>ùì|ñåcárõ0x>ÂÙ#áUülØ>À±w nH¥fÅ?ãq¢{t5µk`qQ?°â\‚ýœgOlEø?0þˆ@$^ ÷ThE~÷,‡O’j±Â “öXœ>>J´>ù¥ÆÏ{ÉE`Sì–ÖMW’xhÚf>k"ÔT>36%Ý’HuöÏqeþØÚ{.ô WÁ¸&cwJ5Q?oèX˜&zéZ…e¢¥+5öéUr*©§*UfÍšYWùÇj¥\±š@J¶“›Vè³ÂŒ\þ5ëEöGàY 6lE·¡|±”é]¬l™ëe+M‹—Ygµµì©Â¤äd-­x-¶mW?V´ ž¸i‘k×’ü ¨nA'~;QSõÞ‹ïa忦íT Ûê{ÿ5)V]ù&æÔgŠ%/BêËd ¿µPÅ'A­Æƒü˜XåIˆcùg҉ɚŒîÆXu¬V½jC[²²_½­V9­sUDmôÑH'­ôÒL7íôÓO!ù ,2PdýH° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹3jÜȱ£Ç CŠI²¤É“(Sª\ɲ¥Ë—0cÊœI³¦Í›8sêÜɳ§ÏŸ@þë´á¿¡Eú;Š4éÁ~LuJjÔ£þ¨ ´z•iÖ¤\»^š³Ù°bÅš];“«Ù´p»¶K·ëW™uóJ¥‰V¯Úš~é’Åîà˜… óMœöð˾Œÿùƒêx%äÈMa^Ž<7êÒÈ•Y¢­‡®?+õR×Ë5´åÖ©©UMûžT~Ÿ‡šŸêÜRi —m%®k\ýÑfÕžpáüd >Éu…ð¨ûž g¥OïñLý}Cgj]»jV­û;Òj>í¥š§}u_ }ù¬HÊÊúÌõSÏ=ï©¶ÂQúÌ—Zwéƒ>\ésSßYÔz¦fÛ?æ­pOjyݳ?²²‚V Ò…Gñ“Zi˜W Q›•Ï>Pò U»­øYjêÍ—`=SÕÈ>L±¢À‘HÞ…Ü?ØH†ÚÝ# =EÆ•_s+\¸“ȱ"Þ|VÖáVZÚ•>§e’?þäãÜs+LiV>UÁHtWå³TI¹íƒãZ‚î£Ð…S^ÕÝ­ù$S]XÏLI—WÔyé?V¬Ç €™þC‘]} êNPq×Õ[þÔÍ"‰cõU¡fG%Êd³¶Tézº™Åϯ$ö§›MúåO ÷I×cLVµ™Ø>”ûVñÖOq±ºÄ?ÑúÅܪ-åf"’Å©ØJžÞÓ­qØ6¶Rc¬þùvÁz.Jî*°›ðÖxêHpUµY–÷ŠoZ .šèï^úN—pÃ=¬©Oœo€ÖblœA—å*‘Å­E¼qA²®Øqȇ®µìG$·jrÉå5˜["SçïË[ñ\“Ë@û¬ÕÐDmôÑH'­ôÒL7íôÓ!ù ,(PdýH° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹3jÜȱ£Ç CŠI²¤É“(Sª\ɲ¥Ë—0cÊœI³¦Í›8sêÜɳ§ÏŸ@ö êðß?¢ ûŠ¡R£ÿ˜6%øjT©M«Z]:UëV¨X}zýjµ_XšKÍŽ%ûUíÙ–kÙʵŠv®Ý¯4ãÞ%›wïÞ·.ýÞÜR°]Â, ÏE¬R¯â¡ŒO:VU&åÖ/Ëù•Ÿ¿Á¬ÚrÞº¢ž\ùê©®‡¯jä“_Uã#ËoµízJ÷y5ËR«•Õû¶²º}û_¾Å¯9Bn{«=â¶÷ Û(ñàFõA·(Ðgê$ýŸ>¿mêøíõøYÕg%Ÿz¾áûý&¾*úÕ[ý± dÅŠ¾÷`AVHýÔÈ?¥á÷v÷é3j÷<åvPùR?Øá£Z}ÿlgÏ|ßÍÅÏlÿìš>ûœHUÉöOmÐåSS!’ÕÏqb˜Rj±¨žjŸ¡·jL9v£U¬àW,‚O BwOj¸ 4+Ó-u ?&gQ?Qn×_•éå=V¥Xo#=5âyªÙa?œ¥× Ü>üxY‘YºÅfBýèW£QùX¡Þwþøù']™µ>¥bžL†V¤Ëe¹Tp=¢T¨£ håOyVùã ©p}åÓÝVù|ÆjcdչЙâ×XòA*[2­5Yúä³>uZLÂþJ+¬ÊÖ¬5[êeÔj´–…Îbë’µÝ>6hJà^«˜?²Bî´MöÕ„eeÑ®)»ºï–¤`‚² pÿnêQŸj-ÖP?¡Ñv¨Àõ)XdŽyÆ· žBFœ‘»[1æX£ûjû˜PsínÇ"ÆÉSµÛF3L¨\v[mÌ+³,Zcûö“sRZ˳ >OdóTH'­ôÒL7íôÓPG-õÔT!ù ,PdýH° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹3jÜȱ£Ç CŠI²¤É“(Sª\ɲ¥Ë—0cÊœI³¦Í›8sêÜɳ§ÏŸ@ƒnô'´á¿ýŠ*}V5ofʪ^=¾\õ¹ž]O,ß±‡£âs½"ì Ú´«ëö>«÷€ÓT×óêÝÁ™îSN_ß“[§}”:í{ýMVm­õ?òÞëYgÊÏ´•|û`3%9¶Ÿ•zѶr4_z×ÌEiú˜Z}#õc^?³÷tÊÙãZ]úøÏ>p§ÚE`ñãÿHXÞnù´•˜yÿXqˆz…•kÇ¡GÛ ÚaE×zý¬°>ùà³Ï†!Ž1H]®Ùø–uþXáQü86?ûXHrÞ铜Wtö¢>ù XRŠÿánqwiì×u@ÙÕoßõ“ÖAòÅb>4žgEViUšõà >gæyÔ>j®Wlneߊö¨y•L™WX1v©…LYq\‹›ÆééUŒúçp‰ ”¯YN§¶xjBƒ®«Z‰¥¨](ÝjP¬uy4¨Z¾þZÙª ÍE詊»³ZµZ(dÈÒêÖ®¼&Dí³i¬´}Ö†v6,‡æF4l}ã‚JªD⊅‘§FVQ¼ÙNÄnXVxZ­@øþíµù¼zo]Ë Š©qlÂt¥½á‚ /],²Å´›Öº ­+0À e¢¼Y Š`Ê,·ìòË0Ç,óÌ4×lóÍ,!ù ,PdýH° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹3jÜȱ£Ç CŠI²¤É“(Sª\ɲ¥Ë—0cÊœI³¦Í›8sêÜɳ§ÏŸ@ö êðßP¢ ÿ)=Šô Ò§L›ô÷ªÔýªj4«V­þ¸öôúµ¬Ø™YÃ%[¶­Q›nãÆ=»’­Ü»tYÚ½ë6o]¾|ý®Œ·&a¹‚UžKsïb¨ý"Ç||W&eÆ“/› –¯?VT·r®z¯^Ü}õR×»Ç6ñI­©W´-­Zuè}_%~Hu¾¯Vj×–Í w[ݯŸ çWµŸpáýX1Gœü_>á÷ª^®ZßçÀ&ý•êãþÔ÷ÚVª² ÄÏñÛ‘Tùg%þ|÷ªþöéÛ¯ozU‘¹VQ?øü3žj(µ‚}©Å•ß dõãOhïôÛ?¬¨†›sÜá³ „rí“ Rùô£†T‰ÔψÁÕC>縖ƒòƒ?Vè£+R´â?¨Ùäy¦vJ…•>ŽÝ öðÓ‘©Ý3’Oå“>ö8 +´Ù·Sî)……VÐ×n%yµÏ>ögÏš‘íSP\Vxµ¢>ôUR„o¾  EçØ>òægHÍIô™V$šd='ZÞS±å¤JE*á¢$)•)p’ØžOŽ•¨¢ ÚäÖ?}–—^«™ÙWú•“cGíE`¬^žä+Veùƒ¤Á’4,€n9âR£iUP™@KaX/96í]àH©È¥¤í¶wñsϳ9 ³¹DmUpºT²Ùuk»Å*{Çyä?¥B{нq­×WGï6šýµeðÁ%,ÚÀ‘±¢¯zÆõñbgIü0ÄI¬0¹rµGÝÆ„u Áø¢ ž»…aµ,G"«|2A®û˹Ñõ.½4ç,ôBe-“¯F£5ôUL7íôÓPG-õÔTWmõÔ!ù , PdýH° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹3jÜȱ£Ç CŠI²¤É“(Sª\ɲ¥Ë—ÿÁôØï_¿™eô÷O'N‰= ö´ù3bÍ "-êP©N§>™.Ú³T¢R^¥JõfV„[¹õúU¨Ø³CË ‹viY¶mÝJå·.Ù¢p뎕ª·ï?Eý ¾Û’ª¿@‚í¦Ê¯^½}‰Û¾ìÉêžcÇ‘Ï^Ùïòe}™ ÏÜçÙñŠÐ{av.]µÜ–«K‡ew3Ë~øôÉKú3ÚŸû ãó uëÓhm«¬ÉÊsW{¬ç;û³¦¿ËöÆFwÎï¿ÆŽýÛÜîY|U¼û†×Ã×3yÏìÏãÕ·Â ä~–ß_¾'§RèúE7]Tªqe\€¥ñ“ZUõ£O~Öƒ\W3Õd•@ûè>¨Wm)÷R†¥å“cö ¨˜TÁé£O VHTü@&–ˆ£YµÏ@bÁh#…j­uV>÷@…#Saá÷ãkeÑu–?2ù•“gY¡à‚AˆyX)P\¬€F•—±Õˆc’™OKþcE”]zÙ?öˆYÕ=\öØ…oõ„æPès%WJ•ÕSp\׿¡h~uT|Œù³hwYš'`ÿèC¥XY¥â^þðc'£ù¹˜¨7*PÓ=›öãÒ©œõTjª5áÃê?a¦Õ©¬š $é>VúÃÊ>°Þöר¢YÅá ùÐVl¬nz£UVúìIGáçZöÄÏŸ®½Ú­Ÿ“fƧKCA¹©X¡öuíGJñi[¥Zµ¢±€– d^ïrä+ H›\R÷b댮êE¿se²qÝU›IWië×f´•>¬ v™$mH^X Ù«ÆgiêîBöòYè[rBVÕÜ/E[ÝÓñÀr^Ⱥ7¿dóÐ5«iôÑH'­ôÒL7íôÓPG-õÔ'!ù ,PdýH° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹3jÜȱ£Ç CŠI²¤É“(Sª\É’`¿–"ÿ| ób¿4þ“Y³âÍ.ö¤¸s'ÍŸ8‡J,ZTR¡J>5ÊjT…ýüUÝZ4çUƒS¹nýz•سFÉ4‹¶­Ú°m¹zºSŸ>­q¹ªåW¯o=¼y™ÎíÉo…ßz+o “ïῊ«^Å÷¸¿È‚•:~¼sf•ëuöÜ•p>}Ù’¶ÊòçãÕUŸüi¸ï=ØLkºö;ºê¾@ûŸ•ò¦¿Ã[)Ƈ–xñ|‡{ÿ³ºž½³5™¢î¨èæêÌýǬºÂÊåªWÎZeÕíöÔW—^ºõÏ~újËÝýsÔ~ûU×_Zd}`_ª©5¨ @üÜs˜=+ÄWYgÎ]ˆcøÝ£Ÿ_á%õ`Aø“Þ„VìV†Wñãb?0¶Å¢‚p 6#YL’\#"ô>ªÕ×ã@pY1`SCê$–ޱ%)Zùé“  ÅÊ‘<%‰–?;ææäy\ù£dNÚuV—H&éO„på#Ü=ý³?úÜV•›þØ8¡¹?XŠ8â?]êæ?\ŠÕã?.îdEUWЧJ7…×–¬I¦ZÿD*'}žÊ5éIÿŒ¹>ìõãçNo’eì"«!v5¦©ZýæåU{îjQs–zS ¿ZY1vúhUÀÚ+Çö£â[LÙ)X>üäslŠB~¥Ý¡EùÓ°I6*©E±bêbû˜ú››Vu,Wt\¶W!(«úì*h½ÓžÈ»Mâú^8ŒCMe.Zöl#¥’&6œSŠKQ±¾FßbN9œ'HšeŪ ä1½Á¥ÉÍ™—ÅÕÈrËDz ³TrÝKsP%ƒ—˜˜1†ñÇ)‹uîË Á¨ôÍ ½S”/3=ÒÒK7‡0•5S­4Ö\wíõ×`‡-öØd—möÙh!ù ,PdýH° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹3jÜȱ£Ç CŠI²¤É“(Sª¼ÈŠÕJ‘ÿþ¹|é‘UL™49Ú¼‰“࿜wòì©`'P…2[ zs&Ï£ ™Jå¹ô'T„S³½z•+Zµr-È €YUÃÆ;0ÐY³`ÕnehÅ[+äÉÕÊ]¼z©Ž-û7oà›ƒ¹}è0b®;秊ËèNÂg?…ºÓïÙÆš×rŽùÖph¢*›zµ{vòá—Cë2>-85ÏV¼Æ¥êÙ¬i­•Q6õ»[f ÞAºø¯ó·®7Ó”ý¼:µÁU6·þ7zö•Çý¹;W^û*fñŸ™²¸}Úõìϲ¶>~Ñ㊑ß%ßÒþ@Vó=·Ûwöå÷húÇ^ -}e…r *XÐTJ(X"]LÚ·yMeHPU^¥ÕáXS=¨žˆÑ©Xž„ºø˜…z5W†agU†1ÞãqY(bL~ˆ'ZF$+øÅ¶Œ‚¦oOzÈÓn¹Ýd•ë-ÅäM¦M)|;6 –Y%)Ü’_y™‘Ò™Ó §\–Y±Õ™qW.–š%Ùô¥Ž`áùÏWGò¹£œtþv衵Éࣨ%¨}Ú$‚6ŨII9É¡ RÚ„ }LMÊ“˜“ö›éI©V%&Xææ_ªFææÕ–¯Âº—ŽQ¶ôâ`¿RllI,˜öZ“°Lº×†I­D­´‚Q«šI®éöUti»­H®'îLâ†Òd˜Rö_`Î:4¶a¨W¼ ÍëXeÐ.·®²ûÔ/SøÊË”£r%¤ÔÂ?4ð²õzÈða ŸtnÅ(1¬1Æ"vìñÇ ‡,òÈ$—lòÉ(;history-service-0.5/tests/data/dialer-app.png000077500000000000000000000756461455450246700213550ustar00rootroot00000000000000‰PNG  IHDR[2T4s²zTXtRaw profile type exifxÚ­•k®$7Fÿ×*²ó2°¿²ƒ,?ŸëÞ¹£ÉDE™r·]íÆs?ç¯?ëù5nšGÏÞMMx‰öñ>Œ^>Þ¿Í½Ãøü-ï—ž„ú—07ý>¯ú9Ï?ÎO~W¿Ïs»ªUç•С–õ÷ÇÛlšóS£ÊksGs| ïÞüA—hÑF[¤m·Õ&Ú¢$&iEJ›w\w_V>ì™Ë;8'/iB¢·Q±KÊ––ÅG³üe ½jóU·( x$™ìFXò¿Ûó+ªuÝE—$ÃÑ—óu=]Êí«ž…?©>YÙ7¿íŸÏe) f¯›mBv˜öÐÖ+òÂÆääûs¸º Æ€@ë$Fš3;‘âh@¦³(O 3Þ0’U¤NðÕ5N¯(L#x¤‹ ¨¥ ÀR5Äk ††‰©™us K]ºvë½{¿Y0\\ݼ»{xú }¢‡GDÆHNA–XöôŒÌJvX= 0Æä)S§Í>}ÆÌ9¢gé³lõå+V®±yËÖm»oß±sC¡tôØéÇOœ<£j%¥eÕË+*k|§F©÷SûÔè“¿¤® Qìû·- ‰M.3c%±Kñ|2À‹Tù’»ÌZ2²ÂFÚ…³éA=ÄVôÅî;¹—ÛƒœúÜ`ÐýrÏE÷ r?sû7jû–ÌõûHÃëÔ&ȾӜԤÈ\2ëvm¶|«¤lŠM³ñäY§Ûqű4Æœð6iç9WŒÓa×rœ–6,GFµ³f?sFÚ¹hbyyJÇ:ÐÚbñ„?’c:¯IÔ­2MP£t%rö6¦!2)ë4­V½Ö?(Ç áËÒŽ'¢}®r‡ûwjúZ›áˆE~Ö2°‹rH¹Û{iÆ ƒ4Î1{q+”h¿Ö€“Ýòœ”}ßj0~ÜÇ&<¯8D;ˆV!¦žyp¨3”t¸Fìçøh1k³œ…°Á Ý¸á ¡:ÅߊP¶ñgCó½|Ž#k¶×pø2îábn‚ܧτkîV¬å¸”L¥'e"PÎztºêè°7 •mT¢(;"Ø4mAJ-èÅ6ݹÓ`"'?K²æBŠô~¤ÂeA<› “.ì räLñÎÇ ¡Tq¼YO ³}´Á1"­½/ßG)Übùü )÷ÄK‹oÓ pHYsE5D!Síê› IDATxœì½ypÙ}çùy/³.Ü÷Eo6o²›}‰}I­ÓR·uŒlÙòÈ£±½ölÌxfbf76b¼³»³;1³±c{½»¶$Û²e[–Ú’ZGw«[}²/²Ù¼A‚N î«îªÌ·d(²ª²€€ùí`£2ó¿w}ó—¿÷Þï‰cOYqOÂA±ÅrÒ6¿r‹±v `åTP±–Uë …Vð\™×ËȰiƒÕÍI£kTŽU+t ÛUyYOtG5¶’Bf¦¿^:0‚E¬¸çˆesó’xjÑŸÜa Ílý4ÀÚa¹•foµª¯@×ê¸Úù¤§Ÿ·jW­û®ôíj f+ž”õD/X–•Ö~¶øw…Rˆ•e^ô)ýYG˜ëN…H¶œº]K–+«1à×_Cß8¢¼»­/äÉ?;áâlÓ#­r¥mW¸¢ÊRxbw|¬ÖÛrµò+45‰·ñ7ò~ ¤/'ç<‰feT+®}*¢@²½[ÈѹÝ~ïb1Ö›ÁÕÅJ‘³EW¥¹¨Ï… /—¬æqj’uIÑÅÚb%£piÑ.æ‘S¡-:é.×^±Xô«ek¶Ùº£Kª.ÖìÔl#sµmÑ.Š…¬ÜZTÃr ´Ù±1Ì.\ "Ë.ùÞ8Òv³pšÃrw©†+—“½ .\¬7ˆŒY¬FƺdëÂEQàÐÖò‘®ÈÉÌ™VÄzèš\¸(2×!ÛÙ‡WmÝR‘`'߯^á‘‹·ÆÊW(¸dëÂEQ‘Í&l7ä׋Ý×étw> oc’1°°;s¥Ûçs¼—\3‚ ëkmŠ(¶s˜RD®Z) |K‚n”M .\Ü3X-3ÃÝð¦°A5Ý”9`ÅÆ“ ³‚«Ùºp±n‘i†6÷íâØý»ÈÌ?Ûßu±ø§°{P`:®fëÂźF6»é:&*[8!Üõ­ /(ªîfHw5[.\¬#¬£—‰ÍGÁ²5\\²uáÂźÁÆ03,—p]²uáÂÅ:ÆÝ´9§‰`s«P©\²uáÂ…‹|Èʬ)W¸dëÂ…‹ u`RȪá:“Í%[.\l¬O“ÂÜí|’¹K¿\¸("r ¸õ½°i#á.ûj°]ùeÝ̵(Ì%[ްVúÄF$$§u³Ò:܈u³ºÈV£kt]µ“Â%[y±–nNòZí¡dç³k=`ã,ÿ¿Û°£»µÒ†³o}pÉÖ…-ÖÉdb5>ÕøäZO(D¶{—˜³µj‘j$kR‹ Vx™‹ÍƒuG*+p›©•Ú_wå]e8uäXDZçX{-\²½'±îÈÆ©;U»{YÆËúÞƒtw‘ÏM:67ùt³&‘Þ-;®¾äÙ6w-ßSX7„³‚Ü;êXñá =²Ñ‚H¿ 8Mà´{¿¹‹Dºy¢ëivp;ú]AÞæY ±»MW›ííÒ¿—ûå*Ö·Èr1ÿÓ¦ÞsN4æ“Õ&½Õ3¬ðÍdž»r3B¡ {/‡XéÎ@lî/µ ÖVÔŸs¬îZ—kšsðõÚÛlïâÒ¸õ1ÿ¿»Œõ C±±.GfØí²V2æ™4]†Âœs…"Këg‚lw¤Õ\ã¸Î‹¾ù±œXN'p2ŽÝÎP8–Qg™6g›Ë•aI[ ôå¶íj*sr®õ—`1wÙ-§v±‰°ÜFu;ÃúA†e¿:v…é§%±lͶØ[3Ó˵êg-g>×kú.\¸("RÄXôÉ·4½kf„•Qæ'}¶Éh—ð\¸páiÄXÔ…©Ä6¼‹Å•®\sáÂ… ;8õSë0±O¶.\¸pQ4–¬.åºdëÂ… ™‹ƒp]²uáÂ… ;ØîJH×%[.\¸Èv].éºdëÂ… ¹ ±”b %]—l]¸páÂ1–Ò«ÀñºdëÂ… N0ϨÙi5áºdëÂ… … ›MËuÉÖ… . …Èo­Í$ÝõãõË… .6:R˜ æj¶.\¸p±,¶ÌÕl]lJØù^O'¹ØDp¨áºdëbÝ¡ØN„Š‘Þzôûìb=À¹«0—l]¬ lV/m›ù(46XWg¹pÁæ%×|XÍã•\Ü-dØÖýd.6%îURuWëÝݱ㺫6Šé€s…"¬Š"ãï&‚`•ëî^A¾V­rmZ.í2·fk;Ë–í8F'ñ×Á»[ˆÅÓÓŽE*²ì+9ìÈIgɺè:½íT–¶eq˜E>ëÕBÚ*[E:Øb³Hž‘9hVë¤Û\ñÖ€ íN€-ë`´-Å8“Æ á®U¥òÔeœ÷Œ™9¢ÊpežomÍ¢øw³ ˆE–üÎ)Z‘Ž‚Ë¬#§ùmpg~É·¬È™YgémoS‘\¾$ËÕ@¶:tܯ³ÄÏv-ÕÎåØRåi¿sF-R×.+m'¼ãD†Ìt‹{&×çÌ¢×èb­ÆVÀ|÷²Ê‘­³Õ^æèÍö;›P¨!Ž‚eK×IËÛÅIKo~t䊷ºj•ƒ]ˆyÈò;o„"õø\/³åÄ_op*ŸM;äê0í¢„¸’ðÅj«U!Ý¥‰ê‹9xµJ“¹Œ)ÙF¯ÓQ½ZeXN]åŠã´ «H´«–²‹Íǃ «bZXHTŸ»LǦª@Y±¾ÈÕí…›®ÕV‘pm'È 1e¹ØXX_ëb³ÁiÿZ×|²J“gŽ×Ùnº7Ø=—d]¬'¬{…®èv\±üM Ùﺪ°"bµË»ÜÎç’¨‹Í‚»Ù—s®Ô(Ò /ú²bé1šBjÖZb¡™H©@ P ¡)k}oê¯ )èÊ~Ñ›P©?"K'‹ »'i÷DjsjCÇ¢¯3˜¥ʘ»–`‚2…5þL+®iXaMS  ×HÆ$FLbÄ%fbយЬð.Ö%lIw…v\× ç ù’èeI´²žÒ$žÒZÀ@ú 4Ÿ‰ÔM„Tó¶de± ÛÙ`î0vû?2©ÒÑæ #2~äÝÓcN)@ ”)0’d\bD5!XÐC,¨ŸÕ‰ÏzHD5Wy_G°]°L-×%[Ù!@ $ñVÅñTÅñVÅðVÅÑüR3—„Í»Íc•ˆVdûå„Dóå“ùˆ,¡@*4‰·diFR’ŒhD&½„'½D&–³ÎIç¦&;Êu-gœ„q*‹¦›hå&¾òÕ[C˜¦ ÖˆLø˜ê+afÐoi½.Ö2_æN\¾¸dë°´Io}”’– ¾ú(žŠ¸Õ™Š¤±:1äÓhçï-ÑXmˆÖÉg&\N:9ëÉ!ÏÕ” _YY’ª¶‘)/Á;~&JÞñ;ôç”­€N|¸("£êìjÐ%[xªâ”mŸ¦¤-„ô¤}³:$Z§RH˜|¶Ó…ŸÙ‰ÖÙÓɦDëÈl‘¥„€’ê8%Õqêv™è.eäZáq¯MªNOš•’î=LÚsýÁ=©ÁE:´’$¥;f(m¢’y ¢D[ íÂenwNØÙìX[uB´m B€æ1©ß3Ke[„ñ›¥ŒvU ®Ö0ÎÔ|ó…ÍôHWHüM›÷K¶÷(|õQ*öMâ«ù bÕÌÂä#Ú»¥­: “5ŸÔÅr´^_I’–ƒÓ”×ǹu®ŠÙ;~ûÌŠ†92Íö,ýo¡ñ71ÒH×=çDI[ªûÇðÕG¬o¢µ“A´…XOD›·.sÉ2w£žÊ›"t<:Ní¶ MJņÈòÏE^W³½çPºc–ÊýH¯Q³€Ó0Kâ“hsÉ’ñc9DkxY¤™y]¤—VIeœŽ‡'Ð|Š‘«å6!Ö îQí6W³½‡h Q¹Ï"Ú%p@´yL¤ÎÃ8€2[«8Ë_¤ºtôÒJýÕýmG'©Ýr$âÝý« »d{ÀWµ4ZŸE´…š²Ùm‚å½^Þ¦…ŃÔ>ŒÍuZ4'kÎÛž*ŠQ—ËÖzÓnê~ƒÖ£“”7Æ–Fvq×á’í=½$AÅþIô²Ÿì`Kš™÷ $Q'a2MÙÃØ ç38©§»J´6a• ZNâ+MØH»ž üÛ\pÉö@é®Ç«–„Á™Ýp=­c­7íb½írÍ é·*›#4˜Y¸¹ay+tíH8ßwF¶WìÝ!t—l79¼51JÚ­™êeÈ]"Z;•hÓÆÚj­]„µÔzkw„(«ÛsÕ†Ss~—°´PùHÚ.­Õ¯—l71ŠÒm³h>Ã9¬©Q8ÑfÕElÓYì˜hó…I]äæk©õæ•%íÚë7hØ=ƒÈ5û¿¡wãÃ%ÛM o]ŒÀ–ઃp¨õª­.\:'Zá0Lú\DåXÑ)f]æ “qÓÉ˦vkˆòƸMÈŒHÙ”ÂB‘³Rmò²‹W±º¸d»‘êÐþ-!4o~ÿ|N4Øe‰± 2ÎG´Žò-,xÖ8Ë©—bÔe±^ZŸIMG›²£Óvq±yž-ý\÷³Å+:Vp“í]³¸(9:½æ3ð×G—„w¢eÙe³Ò0ÎÉLd}ž=άZoŽ0v æ%ÍUªK[ù—ýÒ‚ŠÆ(ÿwˆ›‹\óÿ²3,>dÎ7‹]¾.!ß]8ìP¾úžÊø¢8N$ß­»é*1ÑæD¢uBØ‹.–A´¶ò¯€D¦SR“ ²9l—òæÆªj¿ËCn3B..äm’Sݸ‡ëÓ(óù ÞØE 5lM“'Ëôk¦©ev¼•që„[›°‹®×ˆhínÚmÆ×Úz!Z;¬{¢M«K)¥5y–€ÝdöÛBÂÙõû¢ ´ p],n2Þêxþþ² ¢]¾L+c§õf“ƒŒ—#TÞ ™ ŽÝ…ƒü–óqä4Limbé+xý`¥’9‰ïXK,g„ÂtlW³Ýd>saÂÜ=òkbËÑzíÂåÓÄd±Ñjs¥‘çÞrâä—¥ž„Âc*KyžgÍ7G¬4LIu=°Á—€vqÞ—rÎHÎ_®f»É —&,{m KºB–OÞÌ0ùneê€G#àõ¢I™Ú]&æÃ(¥@©ÑD’p,±$¯&)ñyÑ59W¤˜\¡PJ¡Ä“I"±fÚñ²Ð¥¤ÄïÅ£I¤„nÅ7$ ƒp<Ži.ÕHr­Z¬ì|îÐo³µf7G/Ñ=v‰àãá¡ìi¦e·äåTë]ûJ­“z÷8t;8Rd³EšÓp³Zœ‚K¶› z™Ô, f9¡“0™ƒÚïѨ.+A“2k˜9ò x=hBŒ.¬–Ð¥ ¦¼][Lv²<:Mc*´àÅL¨.àÑ´y‚ÎžŽŽß£3>Z²€$ý"ÑÔ”4p¤íþ*:j÷ò”ú"³‘IFƒ·¸zç ×GÏ2$nD—Œ?‘Jo>Ÿe¼Ø–£õjº‰¿"Éì¨Ï&¶‹9,‹tçbÚ©›Ö.Ùn2èeqûÑHþO~'a25@‰×ãˆh­ô%>áX3Õ¥ýÏ¢µ•?% ß££kð^*Þç%ZG×ðz4b ŽÔÕÒx¸í J|å–Æµê£¼¤‚ò’ýìj8LÜL00q‹·Oráö›c“éÉ-&Þ\ò²´=tSC†4Q,¬£Îk†¨Xï>n×–OºÙá’íf‚½Ü™ƒðb­€EDéD Ó4‰@Ê„¨ëK§rÉ+…@#õ@—öS¹ê@“ßZæ “Bòè¶Ï  »< 4 u»Ø^·íù2ïõ¼È×¾KˆÍW²HOXd/³0~åeë-É–‹3ÔÄÑd22ÈD³—‹[§‰êÉùt²Ö›ebþ‹×…3d±ä=Ôª­³uÚž¹ÌU™Ÿ`Ù®‹-ÓF… =àœ@ÇÍ&‰ a&[jé-•‰µT¢¢m±4G§šðzz#.W ] H‰ÐRoÔe­б;Ì ŠA¢vò­™e‘ùÀ"ZMè<´íkµ"ɉiŒ Èký|æ> ÑÝGÉtÙØÃA„~FQ‡Q€éÔJ ¿ñÅÏÁ·þUSÉ`« v`m öN ƒwSA‚ªžiö]oã­ã‰¬&MSx|ŠHÁ%ÈŽõ:.׿²WU³-fCê3ænÞÝ6W=E¶Ë$Úei½@zMg%Ú4òSyZ&‰.oí  ›h•Ũ9ˆv^ÖÔæ„ÚÒîk9žSîùÔM“S˜cS˜7á/@ÏmèîE!ðÃø…¥PcÐÔWn |ÓÍu¨I¨®"±»Ó‡B”üÇÏpÙ×ÍP“ILLoPÿÏ›ù5óeö@<†Hìy}”w—cûuÖBShº±DÞ• Ÿ‚ä0ÌÕĪ­±¼Š/F#m¢-vg”Óž@2¯‹@´éŸ©sû œ-°x?,$™ŸÄVü´8s»µÒl¹ê`.z~óÂ\ÁÇ·=ß°¡D☳AÂüîáÔy¸9€Ú¿Î^BÄ ¢±fÖMÁH"BQ߯º]Çqþ÷¦»9†±k+WÃ硎%ÚêX|ˆ¯â‰©VGŒLœ¤<\ÉT©}y¤°LMë ÙÆ@¡cc-¹eykc³MŘÌ*ÔNZl»j±Ply¤n"3V¯–6ý¦©T^¢M‡iš¨4²3ÔÒÉ„%ÑÓ*Ôü2‘Êß)Ñ",íÕP6“I‹¦ÏÕ¼V[]RÏÁÖG?NL’쀮^øÞÏ0Î^„½Û‘¯¿‡0S5ò[q…ÀŒÇ‘š†ùÔGàÚ5å%ô¬åcLÓ¸šAˆ+£ðÔ‚l6fyâaÄû 8~ êG)¹ÑCswœ©ƒÙÚU¡yÖÛ(X½/ÂléfòÐZñÃ"²Í.ßÒB*©ÂMO;sU‚ÈSÈŠ‡»Mº…¾U ~©ørÏF¯Ö¦†X/šH!&¢Dj‚‰I& ‚‘Øü=°4ã©P”2¿å[A¦ÞÊTs]ÊT$ “P46¯MÛ"¥Õj¦¤m¶ŒNÎøÆÿ‚yêÂëþ!äž”H%H~ïy¤a‚”h“ÓÐX w@Ó0[[O<ˆxà0òéÇ ¾áñp{â*o¾ƒÔj9[ý7ø·ÐÚ«•õ²ÁEaF>¾‡ŽçßB•” t\‰ÃçuÛtàɳ4ЮV–£ ù”½Â5‹Z%9JÅ&Ü|ÏVK¶•”c¥¯àIÁ¹•™÷|ÝÛi¬¶ql4_¥`&#/9â<=øœ3;±8‘x™.o†Y@)‘lúÃX"A<‘°´[aS”TC*¥²ö É–éÊzÇy`d ¯^$ßì%ðzõ•à¯A„ˆ³WÀ´^nrlÒÎ4Q»·!ÿõïA,ŒöØýPUލ­mñ޳á©~»bàS^¶ú‘ƒÃû󈺨}æ<Œ-1Øp´/å!AÅt’½Éml9‡Æ!Eàô0$Yd”H&QÕ5ˆcQ¡˜ !*Ê ƒíMˆA{ì~ÌíhM àÉî½T)“þñ«óBVëµTN4 ¼êE»p†qé÷!†ÎBSÌŽÐ2TK›Ö΀Ñ?/ÙÈÑ"?‰âïëEèžEsWáãbI} @jÙ=ÚÚ}¡fC¡ZåÜx[Í1²–XÔoÆ)è ²\o®bÜ_k,— óuœB:íròŸËÃV{Ë“q1´^a*;g\; c÷0K˜¼dœF (êø’ûz<ì|kšq0Aà‰}˜Óåˆ+—¡£s|11%GÀ0 „žÛˆ¯¶oCîn‡X ¹s+ÂïÀÉvH$H¼·­³¥Š|áMʇ¢È‰I`T–`J¬®CMÌÀäòÙÏÀ˯ano‡ñ1äC ü#È'…-ué|8+CÁì,LÌüñ«x.^åËïw¡¢Ä–×áÒˆå”gÎiCU9*ŸÏÒ’»n ³§Ÿ}]Ü8²¸ ñGᵨ±1„&éxk¾TG&ÑBn²Ý(È7†VC™+T“·ƒK¶w+í Y5´TÔ³¬=_J´]R[^jyóÊ#o…RL…"„ã îþ4!¨+/ÅãɮΥQ†"1‰- £¬åèÒØ1ZÊ®ºÃh=”÷e̺*ô­ÌûáÚ«©Q«÷í…ó—0• Õå˜Ñ02Cþ»‰~âÚ¾]à-ìPe˜È™(ÉWßA½ŒºÞ‹zé5ø –Dhµ­ñáEøÈƒ¨ÎNÄä,è:|þ³¨7Þ@>ù¼ñ.ü‰!4Iýõ]\'oWwò¥ÃÇ0/^F„ãx'BÔ&w0î™^"—Ü$¾Ã UZÖ\²ÝD©´–D‹ÀZEà€h-Ù¥~/‘Db~­­ß«;"Ú¹ø%>/‘X‚¤¹à$ýÈX=õï÷³3VOˇCh3È/<ƒ ΢fª²YU‰yêêÖ0bK#¢wа/lßAgÍ£Gvrm«Æž-ÇùØþ/¶=7f’è¿ ]݈©‰ ]ð7ü'_Åüà,¢,_xñò(a"wî„¶Ôé‹hŸ|Šä^BiùÍï ˜×¿ƒ1·gÎë'ÐÙKƒÑÁˆ6>Ÿ­Ü¾ õ×Á¨e~0áàÛÞxr©ˆRÛhU8е ¨Ø(Œl3_'+Ñ­—cav±;[«4WàË ã:O/MœÍñw6³Åœ©aneGwF´s><ì ÕSyéG®éÔjµ^;Wàì,GĘÏýÂ<¿óÄ}^è¹…*ñ“'7{y÷“µhßOïþ®Î^"!L óúŒ¯=ñï­ ňÿé÷Ѷl!ñï¢ MõˆñIŒ¦zø‹ï@"ص ~ü܇8sóJ*Fj:æwŸG ¬MÇ©|¥<T, mm„·ÑµÃÀã+A$Èözì2ƒÛKi¹ _ûÕã"M™þµCŠl×­óÅÈ2’Ú“²8¬S¬'éËš1[& IDAT%rroN¦ ížgÆK¿^ïäì¤NNÍ0…›ù‰hÛTË&c±ø:-Ìß°6é¦_K’ô`_À¹»š’ÔLj4Þ‘´w%ˆÖøi~û65#Èϱsæd NƒúzÄ};Q¯žD™ į>K¢§áõÒó`ávÐùh)S“¾ à:„J0¿cìXÇS”úÊle²•3àÁ»{+ÆÍ<ƒC¨¡Ôþû`àÚSO 4¬¬„­[à›zÿ Äãàm@«ª„†ZÌ‹W‘Ói>i+ÊÞ^ÂÔîJn}¤.o/FE„ ADrbQþ&&³Ý‡z¡±³5¢ãbßgD|‹íÛYü¬old+z§äã´e`õÍË-q®ÑšœïŠU¾b¤/k1K; Ú|®mµÞg.ÄÍÌSC§fJÇâ쫦äêýaür¨­c¢³ÉíõÔtM Î^D¯®Ä@Yî^5ÅÔýíTõµ2¾¥„™Oíåµ[ß#þl5c• âÜ‚è-ðcÓgºôòðÎ}ÖJI²·õg‹2šßƒ*õAC=¨拯#~õò/;¶!>ù”U z{PÍh^jhQ^FxK5?ÿ¤ÁDyœÙF?!n¦*1˜U×9ç¿Êî†ZÔÞpêÞþš'ÓÝ´8Îf˜ »ë(Pé›ÿ¢[ Yî 6ɧъkYO±ˆ63ÎJ®Sš’”•SŠöIóÙê¯ñß™AĈ_{ãÔK¨½õø~ëkÔ}ç»Ô<÷:¨Ñq’×»=·ÝZ‚zb/néBýËf&jãñïA;@(kñ •Q%“1žŠ£ñÿú Éý;Pç®@<ØÒ‚xä0r[+¢©Î6 ý£O`ľŒ™öd¾„àÉ|ä§“ˆþ£„5:DÀ­uÐw #"«t[¡º.]Å|þUŒ¼€÷»„(+YHW“ˆÚ ÔñÏ!^{USƒ¬ÙŽ "ÿÍïòú{ß ÿØ>?óeÿþÿA Z; Î\‚Ãû1žû bW=NUÛ!¿k_›ö˜ûÙs¢Ží?éFkj†Ùn𻣂¥Ä«ÂhºK¶ËSE"ÝçG¦ÉÍ%ÛM‘¡µ, Xv¢µCwL-·$-—¦Ùúá4u½aˆFQG¼õ>þƒuøêIMÃõnÌ®›(Ÿ†ª(C~â ÊÇùW÷3Ú®q#|Å43aË[˜]™Ò.2Å~àªgþvÑ\ ·‡@€ü•/ þþGí!ðz‘ÑÔÖÁØ8bd ê*1wuÀØ0JÓI¾tý'åá ]‡ö›ðwÏÃ# 9ŠçÄ#¨ú2.õ¿Ç+Ãè¡!^ê¼À—Æ'àôYÄïÀ§>]=¨€fg)‰Æ8ú³1Î>™§Úm´Þ–ò™O!wïÀø/‚gdšÖÈ^¦«Gçøf„ÕG¶6sÉv“b9&ìL¢µKÌvnRYš¡’-£~ê?¸Å®ÛåÔ¼Õ‰VQgxÒš}Ô ‹h‰FÙr3ˆh;Œ)Î#R;¨’o¾ÃÕã’ÿô“\Ùo0¡_ÁÐ&ƒžËÏÆ!Mž2WQÁo¾Bí÷þúQÃcHe-µR/½~?D"`(L ÆÇSKÔ’¨;£Ðц<~IJ»þÿF0‚þµgçóÐ<쳈7åx&žâççÿM·œ:ØJä3» D<ötÝDõvÃÔ ¢£3š qßcì+)§3üaAeŒy’LMwSù¿†øü§ ½¶S¿ óÙ´ˆîÜÆª"Wõºd»™åó%«}5ÏÀË—ŽWé´DJ)U%<õãI*§LJwnÃ÷­·-µZ)˜%B¢Pˆî>ˆÇ0wu BAÔµ o&ú‰Üí¡[ï'¨Ád4k¾¦ib î$üÍLÄF0TÚ6à4­¶Ê[ǯÿ,@íé?ƒî[Ö³H…´´êp”‰eïPW‹Bi¥LxòQä®°cúîÄ~ðûÖËÇ4 D[+ò×><車ùʈûû{lÚ#íGf½ q{[€ª7㈗^‡¦z*ëÃxÌjšU7ì2vQdè¯cp '­º>i6V9–C¢ —"kèJ£*$h4¨ò×qÿ?\§nü¿ü8õ{ñ £fÅSU•ˆÉiK­«áüï#ðÞU¼'çmß%¦Ë`¢Ô‡ôvðz,_¶)ÃלdN‹UXD[I¥cCR{h>ÙMy4ÊŸü#I$E(™æƒû/@Óß¿ {®Z˨Ì]ˆ±)ÄÔ ÄÖò­Ž¶´]Û1nJÚÏ#ÊÒdujtã¡5·ÀŽ­¨öfŒÙ´ÙfâöÄ Þ¿þS„μ¿)à´çÏøÂ/^A{ì#¨pþ¿oÃÔ"aB0ƒ;¥“+íCV¢»7s|;â[-“HÏ-Z&˨V ·¸JíÝÆ*i¶N›U¨V£cN‰+/ÇZ"å3{ñ½Ì07m¬³óaté¡rƤ, öÕPÕ5H§šWÏ >±Ñ9‰0¡ùç瑱8¢wµw¢«P‡ÉŽJfŽï浃³Œnf¬]o-ä˜ ˧­&vPÍ=/“å”D­3eì~}˜Žáa¼å ¨·_D†¢ˆgž-$–m©VÆÃÏ ¡"QDIU@„¢ÈîÌÝÛP‰à4_€óû nz†Aõc>*9qd+ÿVÚïÀÄ|ò1hmD>´í«Ÿƒúʬªb8ä•óK01‰¦[BÍ•kvªó‚†1Dûæ_¡ã“()¡± Röv<”Ñ2yˆàl “G·4ÃQ„蓳TÝ©d¤Åc+§‹µƒ>GÖìÚ°þf_$ŸÖ®ùEF¸ÌßN®‹Ì¼V/õÕÉ¡P8#Ú…R |I¨MØy9Âöþ;® N#â7 àG5˜ 4äÔì¼;@Ñ7„ò§ŽÅ) `üÊ/¿ÞÅÕæ7·é\¯˜`:1–Ê%¶T¶A¦‰aB‰(AF¢—‡©}ÿm½a¶¿ú>$­I1ùoþ{’§Î „àZsÔ²“KË|ÜÜð'˜îƒW^Gh–2^׌züQ®6MðÃW¬£uÒ|ñ¾µ/Äñ ZEíÏü*¢¥ìÀRÊäôõW¸>ü!R)­VÌ;V7ËKQ3“x¾~ñ&<üê¹#‚@þkýìH>J·Þ¿¨1s-( `>´ùü+(!Qö\ñpíXf`k –ÚÎþfo§öv‘å·“¸«‰Õ§Æ\¥[}"vL´ †Î¡X+;.N±õZºÓ×±‚mLêÈ>Ä–:øÁ¼wT*AŸŽâìAÉíöÛ;éi»¶@ ¾–Ê&@(k ïÓŸEþôUnÄsîjüeðúåÔÉçÉä¼-ØüÆ_#ËË¡¡žÚ ÅoWþ&ßœø&!"‹ò‰p"¤.ak ÔÕ£Þ9Rb\¿Ž”I“óÇ3×OþÉ–w©ñoá÷* Úa‡½=ÞÍ›ßͰHV¦¶ÉZÊ€w˜Þ™0Û¿õê×¾€(-CC²¦ïÓŸàLü<Óädí³ñQü)%"e/s¯këë °xÒ˜?÷²ã”ž<ϱpõ?;‹¼õßPR ÄeKn¥0å,b4•Ž.¡ª1> f#B¤v`êwã?" ù× ¦W”Ãè(&&ª¶ ­m ƹóx>ú<ýGߤæ‰2¦ªìډ꼊zàœ¹ˆ S姦ëy’> µ¥ ÿÒSÖydÇ‚|ÿÝ?&hL¡iÂòÚ•f™û§ÿƯbv}íÒMÌ×奨–:„ *ž@mßB{âÌç >WæÈ—žÂüö_"îÇÜÞNŧ©Ijs–‹Uú"Ûõ„õgp;¢ (/[ïHZf|xc˜Ê’2*oŒ£ÝCjÏèíGÅc°­5:Œ΂(eX§ÅîÝË•ú)‚ÿáY.é½ô¨+ĤÄTГ’úŠR<š¶è“gîËH¥Ž:oQMT]çÄðNÔë§ð;6pŒkV-åž&å,ŸŽ(+… ëhoa˜0= (T,KiºÑ(êå_к­‘ñ†Ð¢òËÖfðÀ;gÀë¡¡  ªÊ;:HöÞBniæø•$\½;uTÔÅ~d×D#ÀqõG¨m­¨ÇB–Uüð2êf?Ú?ÿ:Úñ [/¥ŸùK†fn é,:mÑšg¯Ïü”¯£¨«W‘e%èŸù‰Ó§Á¶z:ÄŽw’ðùÒ¬íš gCoóx} ¢¯áóB][MÚ"9b¹Xm¬²]O´–ßl’Ž•IZür/šJê¸ÜôQ÷þuÊB½´‚¤ i ÚZ ï6¢oPˆ© ªÌŒ%1[k¹¼W"9Î+½Ä*4¦|AêµM9%r‰Ï3‚]EÍܰë‡Ýþ‡!¨†‘ ëÈ™¤HwEå`–5UÈÏ}þðOÁïƒhÔʯ¤bQ‹8gÃ(ŸÎľ&îÔÆ©ªjF‰ 9.D"ˆŠÅÐöïE]¹Ž95‹60„jªÇìïG´6#J0A(…˜˜B M(MÂðT,†üÅÛ˜ÑrÆÚá–<{ þÕï"û œ¹ñgz^Fê ¤@Ê[-©:˜›Ä4›jà`9êÌ9h¨±®›áýsÖ¤[0ŒðHê-Kœ€Û™ÒïÊàÔ?jäá åÐ3€ª*gk—Æ{inEyØLXd›‰µ$ß•.Šeö°›dO Aۘγ?³ëÝ!ˆÏí²R 4K{Öà `VUZöÙ­í ´@ò—?Í™àÜ®JÐïS®§JÌÛ53áÕ-¢­J–S2¤}¢Mðþƒq •\ˆ³¥º1LŽ*ó#FJ™‘"ÜH‘ˆ[ËÅ~ñÄLð€@q̆*nÿÒA¢Û𘒗gÎÌÖ¦™Î.!¢ó±ó<¾½߇]˜—¯"h·­s½Ô•h÷…WO‚Ï ‡÷bh^´^Á¬­FJ“`€ …QM €Ó!0’09CòϾM¬ÂäÇêP2i­¨HÙhç´Û9óÁƵiz*"t(‰¸oÉ¿ü;ÒZ«[[ #ãȤɞwC¼ó¸]¿À–hÁÊ/æ3Q×»Qm­ÈëÝT†=T&[ µ³¸„]Ö!ÙfbµÞÃëgZ6·$…•¿aZã+ߺCSY;*GJ BX[j“Iؽ®÷b<Œ6ùоøKî9Ae¸„ú)ÿÙ{pýCÄÇNpé~³raKíÕ#’ÃR BAK{”:TUCeªïö‚m3iX">¼ÙJI´£•©Gvs¦n[®Ó;­Dkt`ÆV>Ì&&9óL;vöB,†ØÚ×çëUVW£Î_¶L±žƒÁ#P/¾‚öÀaÔkïX6ßújd}êð¸p…Äœ˜@F¢ £=ÿ&òYsþ(ö¹° Ù§3nRÅ™úô1Ä+ðÊ[ÈPåó „¿ßÒ€˜i唸MR%lWpdbî^üØÔŸ½ŒðxP5”´·Ð p‡;YzNáp5ä°ÈÒ¦;Š˜ÖF‚³n-|á!¶Ü ƒçzj±µÌ‰²RºÁÄ¡:Þÿ¬‡Ùڪǀ·R9 ,ÊÂŽh}øÐ"q•¤õ•«<4SI2æAïü|õ˘z•uò€”˜g/£Íî‚Ê…t&ôiÄ}{àâüT°Ü*•Zi”z 5Vý¥Çx];Ãà¾J†½³@·•TFÚ’NÚÍ3ûâì}r5/ŸGÔTa¦•@ €báó{á%ë,3LÌë7ÛZ1§CÈc~p íà>Ìï=^?2nX$PxÆfxèN='[‹—zÉ¥D;—÷ûþ‹Ñu‹°…ŸO¢î Z„‰RõfþOUÒÓ–tä!Z€ ž‹<Ö\ÿS¨ú:d(AÛ…çÚD\Šo^Û¼Ø d;‡H”ÅBþ²·‡*Ùu¾!$ Ë×®­\}¢›5ò§‡éø`*¹PÖT3‰öã'ð¼uŠcCÕTŽÆç^¶Ž÷V×>/Êï…¿þ.b6Œ†-³@E Îäg•ΧcjÐÀC{§¦ ¡0"‡™™”öm‚2éùÚ þê‘!bÆY¬3fdËø¾êc´ŸŸà¶g‚ËÍ3¶aÆ£wøë/øš÷Aªžû©e²©5µ—¬É9J¨GD\îLùi02ª®D9ˆ:wY_:{|kw¯ß†‰ˆÅ±».‡y·MC s‘ùÀŽhŒŠâÑûá­SVàÙåºQy³³€@õõóDé¿åg±Î'hçÂhO>×¾㓨éY¶U†ƒ«?†r°È¸—/l:q;Q9Ö³¶½ÁÈÖE.ì¼DǬ Cn,ãoþ‡ÜöÏ}$Uû#®çàÓüì÷ßGIÏ»VùâËÐý –'UYšò% ¬ÍÞµ•ˆDuà>x÷C‹D‘¨-ÍðúIÊ~«eQ^cWÒþ}kª2AíÝŽ¼rÓz¨i(¯ø•NüO´˜³g¤@½¯…Ž>8Üç£úæ æ ?B56pao’[¿µ5«Ökàû_¯àà㟦ãÅ4¼ßš 2?ägCpé jzŒ¢PȧGݸ‰l¨Á¸…k½ÐyUZB"‡G˜gÒ‰i¶NÖQ퓚ZX‰v,»™ãBÓÍ|©À ø ©q>µüMzP¿8IùÓÙÛ-›yáíº.žÜ·Ñyqô÷ÎPþ•mZ}õaG‚Ùh_äãäUá$ÌâMYk—l7¼“!0LÌ-MÈ[wˆž8–"Ú4ØôĆx)Í“:{‡|lyî þðedûVÌî7a|¡y­µ¶ Ty9bzóð1˜CEh¡ £ª¬Eùe›¿…‹FSÚÔ|^Sûê1üí«¿†ˆÆ01äÕ(Æ Ÿ¹ÀÎ>†£1t¥±w0ݽÁÉ6xïëåh¿º“6ÑʶHôöð”²g÷ äá=I"k‘U•¥†¦Q^^=em©­®BK$axqt?ªï6foZS#Ù¨"ÈÇOÀßÜ@èËQÏØ$bë6æ©@Á¾>ç5?³ætn"É|ÑTV`&’I¤eÚÇ]¬§ïR‘ñ7«AÂ.Ùn&x<–&5>ŒÉ ¤}Îí4âï½Ã¾¡[Î ã»ÖIÙO¢Nž²|x=pé =— LM f‚H"‡F@/Aþò§PömÄLÂQŽª(‡‰‹Ö.2Ó²C Ÿ”‰|û=ZCÚœ›Áù¼æ’š³][ónq‘„€ÆUsÎ]&þ½|n÷WQûIÌOè¥eyâ?ü34YBòw‡÷"f£W¯Á®íV½ÿʳˆh !»)3Âüj›¦š»w¶i€ÕÕ––íñ€€do‘'÷1*ÆÑ?þ4ï7\'dÄ :q£R«åþ"02…Ò4DÂÀ¨,Cå82~³a%ÛÿóÅ[»d»‰ÙZ‹òö#¢q@PÝ?Å}ž\MÜœï=õþF¾øíIÊß¼S3˜DNÅQ½wwÆ0+Ê ¼ 1ÌŠR¤W³&m”J¹PMµè¨áq„iu»äÛïÁ‡0n1]bßSÁù/nc¦qa—Õ\ž>±“º¿x51‰œ ‚2 ¹îŒ ‚!ðz8ú.‹u‰©{‘c5h·† œL tá󃮳¿¿džl³mÊ.”ãïãIÿGð½x’ÆÐŸ’üÏ¿º¯=wa¨*âÎ(êÕ7‘=ˆmÛ1nAy\¸Œ¼3 £Cÿà_€Ö“z±‰%ä˜Yˆ¨(<|?ôô1R'|b?ç·Íj 0£•€èZ'3´5ªš@ÿ—~ýï_¤ìÕKPVŠ©{"ÆíOm[äïÄÅòá”ÄÓkÛ%ÛM„¾Ý¥Äê«ðŽ‚èCãø¯öÁÎ…0cÑaÆEœò‡Á ¯!G&P~?âËQµœ™…ûö¢ÊK/½‘rŠ í06Ÿûœ9…ú¿‰0M¨ªÄ(÷0»£†±Ç:è.­a¢I1‚ÌiÕ™¸¾ªÛ›Ñy~ðSèé‡èª¥ &§P^ i`*Ty„"K ¥Ö Ó´:q,µ6UCí݆~îúµÂXÜËx„‡Ò©$ûfZ¨}õ #“Tt"¢Ìù[òw¨ÿý÷¡ÔÒÄEÌDML ÷M!¾ý#Ä>+½wO#ý‡ÉIÌÈA.Ž¿Ya,š¡ËJ’©J*^ø’†¨>Ê-ï0Is2 ˜×F[)ª!äèÄNÁ–Ï|œï,CñÛ,d­P ¶”nç—ÿ°yò‡,x†Y,ŸüÞ+Èá)ŒC;!ž@ïA¿Š9úêgËMfàô4¼x)Kø8’Á‡•=\…´å¨éŸÌÝ3W>ÚBóÛ‚¨öV¨©‚ËMZ/Ó„×O¡¿~Šy B ZêA×PUpëâKÏÀ÷žO™±xíc4š²;$ڬȳ=¹ú—.pßLÞÎD³¯\¶êÓ ]jÖ2¿ò Äà0jlUoµy¬¹–3ÿt;WŽçí>.V sÚ­K¶› õ 呈¤¥&i]=´;¹­Ï‡‘--ˆm¢çÖ·µËLœï¥0îÜBûØÇÀ€ÉiÄñÃðÆ;ÈñK#T ¥{1ƒU’ò ˆ%M‹g»³-K¬‹Rî+E›˜ñ$rlvnGL€4Ó!‹@ ®\ƒ€Ï¡ýœ­ÄøÜÓ\,`Îõ£:QïÂqA…Zß&‡Õ†À%ÛM‡áý „jnS6bmZð&íçG¹}láóqиõvØõî0TV t ±µ•®í?²‡ »LFü!yÇÇCßF¼ü–µË+u*RX‹ïß|ŠÒY¿YCÄ;^ÿ p£|‚>@²·íÓ_Áxñh݈ŠTĚࣼ„ÛŸ>ÀÔ±­¼QsQž$*«Pâ4ÄYª%¦M†µù·šgLNÏ“íx­‡;_9A–Ÿ× IDATÓyññ'QWºÀïEîÙ…éѳ!ÄØ¤µ¹b÷v8{Ѳ ¿y ÊJ`gâÄ£ýýhr†ÐCûxá+%Œ–Ÿž³jäÕh3ëE ¸ÏsˆÄ;ïðàÄ.ô·Îàë=‡ll†›½ ëÖÚe!R((obqë¥×Ö„‘Œ3¶»Œá>îmâN]Ó'05b%D›Y—pW}Q ¸u¹¡!€ñ@Œéíu”†Lë³8£n GËæ ÏT&cO¢¢wŒ™£;xmç8fySå“óé~üæi-‚ñiÔØÄ¼g/1e­ßm-§Ê,%:wômš,é™=Rí%v²ï£ƒ®#+*Q³1Äô´µuº—óWÑY× h¢–y"}[qŠ*´ jzƒTŒ%9r:JéÍ7P¿þþxï)”©ðë¥<}ðרýÈ6L|îhkF‹ÆÑvíÀ¼rsf@ 54 …Sg-­?FœëDUW2óÙ‡¸Þ2KçAAЕ,ˆh›âuxnÜb·ïUç©û%¸} »@j–Ý\÷X> †F ¾–ѽ³m•t?ÝÈxI˜h­Ÿ„–¢Ö¤a6Y\Ü5,ÖlÝ—×Æ†PÜúôA¶Ü>#–éàð»“üä™RÒUÁ7ʺ8ý¯w2kÌ>;Sa.įðéÖf8y³Ä¬,GÍÌ.̶+…ºp‘²ñ¡&CŒ´‹%]Á@¸‡‰)¦ê]¨ÏCyâX#¼wb Ôш/RÑ?ªMÏÒú̯•µøÇfÙf¶Óz#Jà秨òÖ"nöA<†ˆÆàBj¢ÄSÁ§ücÚj;0H`þ¯¿…§® ùÜK(ÃÀøþO ¹ÑZ”`˜]G ݉feÒP•Œìª¡ÿñ-\ÝeÆ3·îUd]K …F½¿ÕÓϣѣðÂ/¨ïBEy{QÛÛQ·SfŸ¹É5‘ÒЭ²&·5ÔÃLo¯fð@c&ë!¦'8ë 7ûúv‰v}`©![˸$¼aИäÁÙYDÌ:.[ܺÃÑòg8<;ß¾†J¦ˆÖiŸçѶJ*Ë‘†Y](µf¹UU% ¦¦9>ÞÄÏ[Y¾šæ&ÍâìÅüƒ¿@TT ¾ð9Œ H¯O"†FÀ08Ö]»G¦ñ‹þ`’GËO ýâ]Ú.ôQz{–d¢ 9Ó@èÓ¨úZÄQðú0^z™ÿ¿½óŽÏã8ïüwvß÷Eï@Ø{¯%Q]4U­j)²ãرs‰ã‹Ó|NÎÑ/ÎÅ9ljsšÇ>ÇIlÇŠEIV±z¥$R…bï H½¼}çþxßxñÖ}Þ‚ù~>”ðîÎÎÌÎîüöÙgfžÝþ[¿ÇÂe[©¯jÀ+ý” ®G>޶k+ÖŽQxùuß ‡ð̫Ǿm-òñ§‘+êùõ4Žb,jåœÞ…ÐúƒN-²ÐVPmÈÁ÷RÊ/Ú™?w+üÏ—‘ï@E•ïË·Ò€Ú*´å‹‘ÇO#½N(*BØ]EìsJ˜¨/áìÍô4xž?‰¿!ÝL ANoï¸B›’ò*ÿm*˜÷ÙªÇcîãïý50¼ºê=þ0„ê_; [¿bâ}«¥—›=^äeß1·YYè¹<õºzôÚR <8ÈgZ¿œ´x«è5ÍÃ(µ!­Cˆý‡}¯Ì/¦¡¿ð6·T.£©g”†3£0ðCp8Ãc¾ý†M"þø÷#cˆ¿û>ï†5è6+;:kpl©Ã®ÉŠf:[–áÜÜ¿¸Ü^;üG.ìE_?]÷"´£!ÐDB›²°‹4ViAsz¹Âµ^}‹eãuè{¡@ûÏ"äÀ èVÄ5[¡¦ùáaÄÕÛo¼†t{ð”[¼q ª^PE÷º*Æu¿kFèˆioæ„6Òµ¼fÞb#j«I¡•!iIÏK:Î;™ç\:Û[=gý¾N8Pâf´¾Œê ]W^jd>È„w<Úѧ%éK– ì… ‡oúÓð(Ò¢¡M–Iædþ+éл§ç©“Ê©ë5:¯ŠÑæʇ ä±ãècV ¸=~¿­ hÐÁ†ï!Mˆò2 ‹æ‹.fø¸,lE>ú8,^å%H« 몕/¼sç"½þÏîjz«ˆïo§ÛÁ+ŸàøÅ÷°Ú4ÿTÜ /ã„Ö¯¶%–rÖL, òí¬:]Œñž·}mô››×Â;àyâ—ˆ{ïĸåZ´ö³°l1îÑ<Ë[ÐwlæbñEÎýJ#] -Œ•y‘bŽ¿F¡5)¢‘/eX#8¾¤ÙNöº’€à:¡ç£iÒ2!Wo&Ú_¹š}þg‰Áá%^Z¥þ/ xßÝ77@qìÏ~¶¶ÖS9<Ž,+ETUú¬Z¬…ösÐ^ K‹’3p%OZ´Åíw\æRƒÅg{ÑþóiäŠE`·Ck ttú¦™iàûD¸½È•mÈö߇@hÈ3çÖž·|¯çãc¸žyËÐ0òç/ ׊­CL ®œtH.;/~¸›3½bñ ­Ðü¢øÌs4P³ç×[oÀûãG¡ÿ4|ßB tÝ÷øÎÑÊË‘EôÃ'‘ï ×”3¶i)ûtbóN6Ù±—ü;òú@þžðöØYb}ó::c¼ž˜É©ãâ¤mêW¶b3U~.‹ºáj…¾Z YZ‚°û?_}ò4×ÝÅ3¼ùà hýÔÇà'ÏA_ÒãAØí¾… à‹hå*m#›ÌJ[²“úbÈúÂHtÍBÑÇ>Šö‹¯ù,ƳˆE  »ŠKÁåð/È’"„׉xåMt²¹)$zUò\Æà ÔU#&ìˆïCØqJ'¶Oßç ¬¨Ì¤àú˜pŽóòÝ´÷F·êè“B+XîhƪÙXýòe*Þ_Í¥>Vê7ò†85ŸH¯²î1ìÂyÎ7 aç Ðy::1>r¢ý,bÙ"vÔ/g¿| )Á",´Õ-cÓ‚ëi«YˆÛÃ-G}~å B$à¾yÖÿó}DY¢lÂ'ä ÛÒØu#ö-h›7àj*©¥Ã34mÛ”5+}¯þv'ÅgÇ»ŸA?zッˆáQæhB‚·¼¬:ÚèÑ0ÈIÓð”Ù˜h­æÜŽf.6{é_^ŰuAàLÁÿ:³BØdä´!f¹/ÊJl ˆà28+‹è_PIÓ!Çäö¹Ï†Jö¾Ñÿ";N'Þã§°y´f·ã­«…¾(/FŽŒc_3Ÿ3«t./­äܦj&´ÀRdã[ܨ1…ÖŒ6JA‰Šq~¸’ašÏ)kµˆ…ÛBú;Rà¶³[ ºÖÖÒt¨wrT¿î¢JË\F=ÃÓŽg?Œ,šÃHkUg.cB>ý¼/öBçEdë\䟤zûF¾ôðŸ0ÑlÁáaÌe‡01ôÙK1©º²ªãéï0Z:ݧ¿ú-è8_.bj›¿Ou ã}ç]Šß<ŽñÁ„¦ûâÍööû?¼(X­“må ‚£Ä çÖ…toª§c Wx"FÌ“˜9&nšbɈ±áMïxBnù ³#ÁS¥*±- #|ÄúÜš6K/>« êÚ‡©º8ÆhC”L¢XbÖ1çWPyºÏwûôûS À°Y7_‹vâ4ã*ó.ðT×pëÓ7 Ì'¸RÀHµ_°™ð­ÐºXº†ï£ø‚·}ë÷G·jÈÁQ„ô"nºãt;Ú¤Ðù¸›ê¸¸¼”áUßVÎH©w‰¯æõ l$+ÚŒ5~@ܬˆiâÏ<ˆï‚ˆœÆ0´Ð­³†D2ég%¶…D„WÄÎF/®ÖfŠßîºÔËUî;ù‡sø=¸mm{º‘¹m÷s“smµ =.¢w€Ò½8îÚê;FJÓ‚îsõ§5–žQdÇ9Šß9 û õ !Nv˦µ0<Œhn€"+ÂîEîÛÖ4×—ƒôâ˜[Ã@-ŒoYƱ•0\-­Õð/ÿ¡aø…6vï0ã>ˆ¹y†…Àðææ+v.’qžÊE‰miðÃi“ô,*£­Ó/|šFé¾ãpObB ðaÓë«Ë¯íîJu¹Ñ:»1Ö­Ä[껥&å3訂;é&hRCu¢O¿vïù Xžz Î÷€Ã\¶1049ãAò-¤``árˆ ã(‘tÞ²€‹W4ÑUïb°Fà±y}e ÂëO:uâéÚx–qZ„6^9!ûõé%^Ë*±- d„ŽäÑ$]W/ õÌ8¢o\.jŽ^¤åõ\ô„Ä296¬ã7ÎA^Q‹q¦Kÿ ”X‘µ•ŒÖQòð½Ø·´â^ÝB˜5ð™žH‚+ñ­Æ¡ìÈ<ñ<Ö®¨®D¾ðÖÕË‘g:}É5 z.Ãxuõr_íòb<ëWà¾í †ëm¼i;KÇèq4‹ÃHF ­ÿ|‚G:ç(‘~¡à¶0!´ff&„n,ܲÜG‰máq…t5ÿÏÓ«ÊØ¾vƾýhý.Jœ”w A# ‰Ã€åL™ƒÖyMè×^É eGpn_ËiÇYnYÓJSMKH.2\p¥Cbuhõyàù×±=·ùá_èAðÇ-ðYâòˆïÓ²²1j÷}ê\Ó‘e6¼[Vá¹bòOÏêVd±…Þ‘n^=ôÃc½èVÝ?@†?  S"á¸ë#xÀÛVƒç¿Ü†s}+F™ü³|‡ø*{уo Ìîçhç‡ëz—1fÑЂ\ì¦É™0'¢¡ib’F×ÁL -€×%p;”e›-”Ø^—†Û!(*÷ý‹«ªX÷¼ܾo£X^Úâ]WÓ^oGhº!¨l¼Ô@íë‡XØ_AÚÛû}ÂZÙK!¤¬¨BÔ·!».",¤Ûƒ´hxt‰Üº÷®Mx®ZgÁÃ`ÕCj$®&V™^/g{NñAûŒ¸Ð-Ý¢û¬WiÂ&BTÎŒˆ¦Ã¢/šé •·ðIœvË5{—ëf%¶„×î ðFÎŒÕX)4°¸¼Üÿ—§Ø·£”ò9­¬l×GŽBçË`³!]N¨ŸãûÚ-ÀÈ(bÿ!„Í=}ÐR6:Á…ëÒWéâÒ¶6Ú›=l]u ‹–NƈÌô…¹áÛ$†!éêïàƒ³{èëÂbѰXµI,“.ƒ€]~ÎÓÿÈŒ6ì˜0×@úC%FßýǸ†×3©"ƒä¤ØfúE§`W‡KpŒhQÅ ¿ÄÉÉ›²éÑÈÒr„c.\âŠÝEP6„\±jê£(¶!K‹ÑNu ë#`·#jq¶ÔÓ1ßËùÍ tÍ«gÈjÇ)&@Jöž~™ºò9T—Õš©²ïKº“Ê"q{ËáÎ÷¸<Ö…¦ƒÕæ³È¦ù^C]Á'áw:y˜ÚD\17% ´cC"è£›Š™&+b›m¯Q¶Ë&Ý÷¾c$¶÷æ® æô̧íåv&MÄEmÈSíˆêJ´!Œáa4)À±tKGè[^DZµ:Î{‘ƒ‰i' „ï§Ã˜àµ£Ï²cåG¨)¯‹$7~‚܆dÄ1DGÏiN_:Ęk)¼èV1ÝBj‹àMaçº-qÿjøq]„ÖL>ñ-ãÔ…`|X¹²IÆÅ6—„-1Ó>‰²cTÇð‚f‰œ¿Ýæå—od{,µëÀ(!Èsçq à¬-fp^=›šh_¤3<·˜‘Òb¼š‚"ÞL •ÿU^ 8.ñÊ‘§Ø0;msbµØÂêjHƒqû}#—8×w’ÞÑ.ìî1„„E ¡…Å-õU;ëBk.ÍL ­ôÂØÛl"¶ßöPÊÆ•ÔìzñŠ* Vß>AQ™³Óê4wyY}Ä öxŒÛé½b>gLTè ÕÛpëÞøã[ÓÒH)1 MZ¨-›Ë¼ÚÔ–Ï¡ØZŒÓídh|€¾‘nÆ{w #4‰Ð‚=åƒ ‹cäFˆ*n øh£SèÎx‚žGfC%Æ"RǘƫÿY¢¬Û,bQB™ß„^?ׄ†cH£¸ÌˆžN€¡Ã…6 m:ÜÚ!MBØ)ƒ´Pt]GJƒ~{}ç/ø>ŒàOë‹öèsèV߆hb&ˉ#/´&ˆÖ.£û˜Úl’“dŠðÂø€FuPL˜t¼îF: ¢ Bˆ×'¨ÚÔºƒðrE„m¡å¤KD3*´ÂDší#Mô±w¶ öé*¼b–±¡ƒ(ò›‰þ©y­éÚ„$ø‡Œ’&ð;Xü¢‰ŽYŒ‘&!«1I¡MÆZ͔Іþî3gÕæê›n!ÈÓÔyMSä/ƒ:nGø°H$“&¡ÛÄ_Mûæb´©Ô-FâÐ4) ­IÒÖRÌÈe×î×ã5wNwÿ\®›YÂwñ®ˆ"çqŒh؇BÌÁ4ù M[bqö›¶XSt¤]h#¶ef>;-Ÿ¸ù†üŒG˜‰o]=_êøïé¾f,œ\¸ ²]~ º0ÝŸoB/M¶„6ü–˜ùP‰Á»ã–twXðzbç•O¤Ô5³Ø¯žLF(Ó!œ3Ù8fîà<`ðœçXàkÓI‡8${I21« æ%KAh#g8}c|«7ɶ4!´f¶MŒitŸ)ÜqpÓ÷a´›dûv~ÍQþNwÞfö'cÒ¥’.Aìã—uS?+Ì fD&ôw2BµÜ…6v>‘§w%#´Ñ ŽŒ¡ Ôeà’Æè`uóîµi÷²É·‚àã2ý/ÿy³EpS~•†:-ÌYàŽþ‰îåš±°’9m3Bº1[Bf­†¥Én¨Ä¸åøÿ#%\:kÉÝxÑÎ5•~€àF-3Cí•G¼YF¬G¤I:,Œ\²Lf–œMf¬^ÙfDhc–.¡¸-=¼’Ú¸–sÐÆË]ºrÉ…ä}<㤡ïEB‰m>bòfð¸='¬H#üø¸÷M­™lÌ䑌՘ÂÚ)}³’¬N¼¢'‘œ=lÁO—g@¨r†ÎM‰m¡rôŸ³0|Ñ2m\ñJ¢”&ÔbZ®ÙÌcX£‰Zµ‘úHì6Hþ³ãq7™è°¦@þ=ç³`Õ’¨&Š H‰mcx]‡m8'üñ`CöÇ¿#aÆ5ñ˜1NT°C2n±I=lÌOïJø¡e¢qZû¸Æ©ýV¼ž §ŠÉ·!S•)dË5øÛGS-Uø uY¸pÀ†ô†JH8¦|‹¡ÇDú'Ÿd¬Þ¨¢Ãê = «ÞL>fÅ8ö†8u‹rHp;Á‰wmôœOÀªU%¨i#Ȳ÷ÈŠô[‘/ôœ°qñ˜mr µ „6F>ñvÆÚÜ •h¦Î´Ò~Ø?óH)k5£˜t#D»ìfÞQÔÌ /\8`£ï´µà„6æ]Ãê͚КÀtû%ìfÄŒ'SNÚ 8&^Éf«-]NÁÉ÷mœÚo+¨ø…ˆÛYˆáñ¹£­ëœTÔO½wÆÓ‚d\ ‰fŒ DrÄKœøkú>;÷ Ib í`ÎÉ÷m\8e)ˆx¯…ŽÛYŠDÐ×ne¸G§q‰›¹Ë]”UŶrS²4St¤]h#¦IŸÐFõx˜Ú±!³Glœ;®ãPŸºÉ”ØÎr\ãŠè?gaî27u Ü”VJ„˜n+%ë_M‡Ÿ6BžGöbÒšyˆ!£Ã‚‹g,t³1: |³ù†[C:íût.±QÝèeÎB5-n¬EQ^Pã¸&·¥(¢éÚÈNßßê/´™ s9}:]g¬ô]Ô”%›Ç(±ULÃ5®ÑÛ®Ñ×a¥¬ÖFå/õ^Êj½”VX‹¥ymm®ƒˆVïÌ ­Û!ò}Æf°Gg°Wcø²îo‘¬g6Q+Xy€3[ED¤c—uÆ.ëp,6Iq…¤¼ÎKYµ—’*ƒârƒâRKhºœŠ– Ï:¡•àõ¹OL7‚ÕÂ!›×2?»†"wÉOWFÞølùKªBŸ]I‘=òcö„[EÎc¶+)_·"1‡èÌ¢ÄV‘W¤Ë’)a6“ontýÙDn°[…"éöwç§Çqú<å|=‡)2=ŠÞ"JlŠ,“ËcìñâCÄÛ¹y^™'Üù¥ÄV¡È²=̰\gÂ’Ofáq¾1}¢Tb«Pä"©Z»±.–˜ÎÄAòÿBÝhm¨ÄV¡Èaµã½æçâÜùB°rã·«Pb«Pä:Ñ£rQ8“%Û.”dIä(±U(òŒBÙHäƒð&s ”Ø*Šœ%æ-§ëá¦ÄV¡Pä5ùbé«) Å  ÄV¡P(f%¶ …B1(±U(Š@‰­B¡PÌ–täå꼸™¹Ì•óŽw~©|ê(WÎQ¡ÈGbOýJT™dšÄ,Áømé ( 鈘9“ëÒÓy¬aEZ(ÔOQû™.¶©.R6{|hDŒD¢c„çд\÷8çš/óÿReæ#ƒfˆDïËPB+Z¨‘VR%Ùö—WèöD‹E»öé¸vþ|,“?‚w¤šq¢é)3Þq™¼±C£yäFø‚gFš9Ò5M´/Ä‹3›î 3m—Χt¤¼‚¯©™×ßH×>•k’Ÿ¥à̯lŸOÁ˜y9†ˆùȃ¦É‡Eÿ©’íþ™2"c –ëæ…ì8Mñ†Î+-Ë«ÊÆ!W6ÇPb[(äëG¡2ÔQóê™”¯]•È&„šg[¨r»3„ÔO×õ-6Ò?…ITƒ%…²l ïŸzè®Ý¾W÷ìåG?{<«uÉêËAº Ïñë=›Q–­"*‹NqqQFËX»r9‹´f´Œdɪ«pežç%ʲÍs4!¨©©¢¾®–Ò’bJKJetlœîž^\.wÂù¶45òÏßús,?þ³¿æý‡Ó]õ¼Fsêg||br_¾»_cMÍÇsË6¶Tb›Ç|ã}‰µ+—SÃúôz½tv]bÏ»ð󧞣`ÈTÞ×^¹•Ò’bv^µÛÊËJù×øKlV+?û"ßúÎ'÷%»d:S¤kº°™¼”Go#‹ZŸ¿”–”PR\ÄÐð]—ÇéraU•TWVÐÖÒÌ‚¶´µðð½wðÒëoó×ÿø&쎘y¿þλ+–.âÛñ?&ŠB™°;ø‡ü;O=ÿJZê}Í›ùüg>ASc}Ø>ÇËOšïýëÏ¢¿dá|~ýáûؼ~ ÅE¶ˆi:/^âŸûbÔ<¼û6žüåË“ 'ž}‘O~ìnšç6°fÅR?÷\¾òßþ+WnÞ09ã#^¯—WÞÜË÷þíg\ê½5]YY)_ýÒX·zyÔkàt¹xü™ù—Ÿ=núÁ¤HYžúeVò²!Ê…-Ç>ñ,Þ}‹Î¦u«ysïûaiJŠ‹c.£íêî¡»§3ç¹ÔÓGO_?v;ëW¯`óú545ÖóÅÏ«ÕÊî§ŸO©¾Þ}ŸûôÃôõðáácÔ<Ûˆä—ÐÆ ‹ÆåAzúúi¬¯ãÆk·GÛxŒŽókŸÿRØö^݃¦i<òŸãÆÛùܧ~…_¾ôzB¯ÄÁ\±yý¤Ð>óâk|ó€Çã–æ®]7ñŸû4×\±™>z+>ñlÔü:»ºùÜûʤûÅì£üþ;?‚‚=ûÞgpxdÚ¾?ö v\¹™«¶n¤ºª’¡ý¡hþkößþ.{ß?1ÍüÖ¾ü»¿ÉŠ¥‹øÚŸü!ÿåáTû¹˜ù~áËÿ›K½}÷mZ·ŠGþà·i˜SÇÿýóGøÄo‘1eáÎy²\·Ç&Ó‡O3?Ï#þ×ݶ–æ(i$Þ^†aðõo—ÛÍfåÊ-ë“:7M¾øùÏðêž½üåß~/LhžüåK<ö”Ïzþø}wNŠY4‚ýÜfÎÈj±pÇÎøÙ“áB~ôÄiº{úBpïí·˜95€˜>ís]|þÿ”c§ÚÑ4ÁWÿèwã>Xc=Ð>8x”Ïþþ# ŒRS]É—ï·L×S‘:y"¶ŠL08ä³¾ª*+RÌ)²L9].Þ?p€ùóZ’Êùš+·P_W‹ÇãåÿþÓb¦ýáOÃkTWU²rÙâ¤Ê *¾×]µ•²Ò††9xäDÄc~ñüËÜõ‘›’zÛˆ„Çãå«õw4ÏmàÚí[SÊo`pˆ¯ûŸ¸zÛ&æùA™G‰í,&àŸŒµÜ7q¦‹LwO/móš’Êí¦k¯à½‡Ž™vdtŒÿ@ÒÚU+’*/÷ßé[Äð싯ƒ”­ág_|)%5Õ•lÛ´.me_¼ÔË[ïî`שO/{ëÝýôôùf\ÜpÍ•)秈MàþPb;‹ Œ„;“T MÓ(++¥¶¦š’âb„ŒŽP^^–TžkW-àÐÑÈÖd(]Ý´¶ÌMª¼H45ÖOZÊOù­×H  OZò÷ݾ3­N©}`ÍŠ¥iÉ/О[7®MK~Š)¢¹¤ÔÙ,¦¶¦€ÑÑè#ðñX³r÷Þq Ö¬¤¢¼ ‹®#„À0dX4¬DÑ„ ¼´€Þz»nÜõ#ª¿ëjkßüßtq×®›8qú,Ý=‘Ÿ<öÔslÙ°†­×QUYÁðÈh˜à&3·¦ý|'e¥%ØlÖ„#¹…Öá¬?¿†9uÓÚP›TžJlg1Ë—, · ©ãï7»o»yòwïåFFÇ*Ê˨(/§jnCÒõ³Ù°Z}·h@DÍ`ÃH|hB°ëÆkxôÉ_RVZq€.À©³ç£²¢œ]7]Ëì~:,M¤¡ÇxŒùßt]§´¸wØ&“_À__^V–G4f“ gb˜\‰í,¥¬¬”Öfß«ö«oîMøø_ýØG'…öÑ'~ÉO:,|£Å¢ó…ßø$wíº‘ø2¾ßãñâõX,:û½õùKMbÈHQYͬQ›ÎÖMë©©®àO~ßÜè}`pì®ÜÈì~&^ê ºD—³êJ_¤”IÅ'eŽÿá•H^ÑZ¬D8“s‘”ØÎRî¾ujÄümÿà‹Yt]çWüñ~ø“ÝQ£]y<Þ¸ƒZ±ðz œ.K •å8]®¤óJ–{üÓ¸¾g³T”—ÑÒÔÈŠ¥‹8~ªÝäQÑE·Ùÿ†àñzq¹SÛ&~‰žW$òU„gz’§ÛYHYi Ýs;¯¼¹7l‚~<_…xí­}i¯_)%í¬]µŒ¥‹æg¬œhT”—±m£oVÁÿüú·Ùð¨©ãŠl6~ðí¯ÑÒÔȽ·ïäkóKÝÅ Ú8{î^ot7†Y´ÍàÐ1sÉK̲)ÄÙšI¯f#Ì2„üé}Šò2ÜÿýO8ÒRŸÐJ)c®ÙOïøWW]±yCZ½ÌpóuW¡i‚¡a>'¾x ‰Ò×?Èѧ¸÷öèu¾ý–ëcZìWnÙÀ|îÓüÛ£OÆõßwçN¬–`ïàô«ùàÝ·ù,áo¾ûCÿ@¢b&P>Û`é¢üæ¯=ÄØøv‡¯×KMUuµÕ¬[µœÖ–&4Í×é:ÎwñÕoügÏ_ˆ;7 øïдÿû›ÿÀw¿ùg,_²Ÿ|÷›ì~æNžéàòÀ ]§¸¨ˆ¦Æz–Äñµî}ÿ ŸüØÝlX³’/ýÎgùÅs¯pìä™iiþå§»iijdçõWó…ßøUî½c'/¾úGOžfdtŒ¢"õµµlZ¿Šu«Wðû|¾ËÉMg °jÏž¿1¼¤v?ó"«–/aÍʥ̩­‰ø±óú«¹ñš+ysïû¼öÖ»t÷ô¢kÍMìºq›×¯`Ͼø?þyÜ2?ù±»¹÷ö¼ºg{ß?@wO/¥%%,ZÐÊm7_DzŠøéî§3êoW„c™æ‹OÅÖVÈ%X×®Z6¹Ò*.—›ƒGŽó?Ë{2]†/^kdKñÌÙó|õÇïüƯ2§¶†Ï~â˜yÙí‘?†xäø)~þ‹ç¹ïÎÜvóuÜ|íUÜúÐo„ }í[ßáÄé³|æã÷3¯©‘OýÊ=‘Ëq8Y³b¯ÄøHe¼9¸šìØîó‘&cÕxãíw±îÓ”qÛÍ×ñ£Ÿ=–æÙ—Þàêm¹þêm\õ¶°ýRJ~ò˜/N¯WÆ ¯½ÅuÛ·rÇÎë¹cçõaûí'ßý—Ÿðø3fÜ3D:Þñú•Ã:$vÜùpúª!§tùJbU2¸Œn넉×v‹N]M5 ç·Ò<·²ÒRÊËK1¼ƒÃ# ŒròôY:/vÇœˆë®òuþw÷Œú5^M®¾r3+–.f^ó\êjªp8]ŒŒŽÑÕÝCgW7'OŸ¥ëR/îS––,šÏ¶M먭®ŽúñJ«Õʆ5+¸jëFêjk¨®ªÀétÑÓ×Ï¡£'سa[š'g5¼º'ºUW?§–ÕË— „à÷$à†k®˜üû• ùÌOþÛ?RYQÎïþÉŸsôĮ۾•+·¬§¡¾‹®sy`ˆýòêžø³EÊÊJyúÇßࣿúÛx½^n¾î*6®]ÉŸ¾Ëý¼õî~öìý h ]½%œŸ3E2b£}Ò*¶ê:$GBí–¯O“4ÜY?õ4V Xl>žR^¡b;<2š`QNl6uè`Òg›ˆ®®AÛ  íg‚ 4PÖß`ò¥í&d^¯êàÁ’øFENé¢Í”dñ†É Ý‹T‰÷N#ìúÅ_6¬H5¡™…OÍœX2*BþŸ Ò2©4Ÿžùƒ[EA“«²‘è"‰ï˜gÝiSp(±U<¹(¯¿ý5ÕU¾éu±|"&…õ¹WÞ¤´¤85J¼JäJ«æ6âÚtNýR(òuãgÕªÑP–­bÖb&*UN Äå31³>Û˜?Çà”JlŠDëNÙ~¡ž=‹5Ùb™è9ÜÉÓ†ˆòw¢Ç&zL´­Û¼¥@îÀ„Iþ!“¯ßÏR×Â@‰­"ÏH¯dæ’Å{|æê·¾æPb«(âÉP|ÙÉäw±);ù*Ñ9b]K%¶ŠY@br–XêLÎLLª߉‘©‡Ÿ[…"%ÌØÄéš_—ñM–|ílr(±U(ÒF¦»s®ÌQó‘ës¶Å5%¶ Ŭ"s+S÷œg®ì\@‰­B¡`&&Æ¥ËÎa„[…B…ÌÎÑÈWÑLÏV¡P˜`¶IcúQ–­B¡0ÉÌO[ËÌEôQb«P(2@6—‰Ì$æ@Jl E†Éר±H<[…B‘Òñõ“DO…Ô}ÖJl E–IUÈòcð.)±UŸ1Q(ŠÄ°¤û™ Æ+ …"œ¬Î³Íã_¡P(R'ë>Ûlº¼ …b¦ÈºØF#×# ) E"X2þ.ŸŠJ†~õ;ƒEe63s¤z)²ý@R§ EtrÖ²M†´>7òдÎe¸8UÌv2/¶fœ²¹¬òP|ó•lÆEU(ÒIð½œ=Ë66J|³†úʬ"W‰uoþÀ³dÞ¸2IEND®B`‚history-service-0.5/tests/libhistoryservice/000077500000000000000000000000001455450246700214445ustar00rootroot00000000000000history-service-0.5/tests/libhistoryservice/CMakeLists.txt000066400000000000000000000041521455450246700242060ustar00rootroot00000000000000include_directories( ${CMAKE_SOURCE_DIR}/src ${CMAKE_SOURCE_DIR}/tests/common ${CMAKE_CURRENT_BINARY_DIR} ${TP_QT5_INCLUDE_DIRS} ) generate_test(FilterTest SOURCES FilterTest.cpp LIBRARIES historyservice) generate_test(IntersectionFilterTest SOURCES IntersectionFilterTest.cpp LIBRARIES historyservice) generate_test(MmsStorageManagerTest SOURCES MmsStorageManagerTest.cpp LIBRARIES historyservice) generate_test(ParticipantTest SOURCES ParticipantTest.cpp LIBRARIES historyservice) generate_test(PhoneUtilsTest SOURCES PhoneUtilsTest.cpp LIBRARIES historyservice) generate_test(SortTest SOURCES SortTest.cpp LIBRARIES historyservice) generate_test(ThreadTest SOURCES ThreadTest.cpp LIBRARIES historyservice) generate_test(TextEventTest SOURCES TextEventTest.cpp LIBRARIES historyservice) generate_test(TextEventAttachmentTest SOURCES TextEventAttachmentTest.cpp LIBRARIES historyservice) generate_test(UnionFilterTest SOURCES UnionFilterTest.cpp LIBRARIES historyservice) generate_test(VoiceEventTest SOURCES VoiceEventTest.cpp LIBRARIES historyservice) # DBus based tests generate_test(ManagerTest SOURCES ManagerTest.cpp LIBRARIES historyservice USE_DBUS TASKS --task ${CMAKE_BINARY_DIR}/daemon/history-daemon --ignore-return --task-name history-daemon WAIT_FOR com.lomiri.HistoryService) generate_test(ThreadViewTest SOURCES ThreadViewTest.cpp LIBRARIES historyservice USE_DBUS TASKS --task ${CMAKE_BINARY_DIR}/daemon/history-daemon --ignore-return --task-name history-daemon WAIT_FOR com.lomiri.HistoryService) generate_test(EventViewTest SOURCES EventViewTest.cpp LIBRARIES historyservice USE_DBUS TASKS --task ${CMAKE_BINARY_DIR}/daemon/history-daemon --ignore-return --task-name history-daemon WAIT_FOR com.lomiri.HistoryService) # Telepathy-based tests generate_telepathy_test(ContactMatcherTest SOURCES ContactMatcherTest.cpp QT5_MODULES Core DBus Test Qml Contacts) history-service-0.5/tests/libhistoryservice/ContactMatcherTest.cpp000066400000000000000000000211271455450246700257120ustar00rootroot00000000000000/* * Copyright (C) 2015 Canonical, Ltd. * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 #include #include #include #include #include #include #include "telepathytest.h" #include "contactmatcher_p.h" #include "types.h" #include "phoneutils_p.h" QTCONTACTS_USE_NAMESPACE class ContactMatcherTest : public TelepathyTest { Q_OBJECT private Q_SLOTS: void initTestCase(); void init(); void clean(); void testMatchExistingContact_data(); void testMatchExistingContact(); void testContactAdded(); void testContactRemoved(); void testSynchronousContactInfoRequest(); void testWatchIdentifier(); protected: QContact createContact(const QString &firstName, const QString &lastName, const QStringList &phoneNumbers = QStringList(), const QStringList &extendedDetails = QStringList()); private: QContactManager *mContactManager; QContact mPhoneContact; QContact mExtendedContact; }; void ContactMatcherTest::initTestCase() { initialize(); mContactManager = new QContactManager("memory"); History::ContactMatcher::instance(mContactManager); // create two contacts to test mPhoneContact = createContact("Phone", "Contact", QStringList() << "123456789" << "7654321"); mExtendedContact = createContact("Extended", "Generic Contact", QStringList(), QStringList() << "123456789"); } void ContactMatcherTest::init() { // just add two telepathy mock accounts to make sure we get the addressable fields correctly addAccount("mock", "mock", "generic account"); addAccount("mock", "ofono", "phone account"); } void ContactMatcherTest::clean() { doCleanup(); } void ContactMatcherTest::testMatchExistingContact_data() { QTest::addColumn("accountId"); QTest::addColumn("identifier"); QTest::addColumn("contactId"); QTest::addColumn("phoneNumberCompare"); QTest::newRow("match exact phone id") << QString("mock/ofono/account0") << QString("123456789") << mPhoneContact.id().toString() << false; QTest::newRow("match phone number with prefix") << QString("mock/ofono/account0") << QString("+10987654321") << mPhoneContact.id().toString() << true; QTest::newRow("match exact extra id") << QString("mock/mock/account0") << QString("123456789") << mExtendedContact.id().toString() << false; } void ContactMatcherTest::testMatchExistingContact() { QFETCH(QString, accountId); QFETCH(QString, identifier); QFETCH(QString, contactId); QFETCH(bool, phoneNumberCompare); QSignalSpy contactInfoSpy(History::ContactMatcher::instance(), SIGNAL(contactInfoChanged(QString,QString,QVariantMap))); QVariantMap info = History::ContactMatcher::instance()->contactInfo(accountId, identifier); if (phoneNumberCompare) { QVERIFY(History::PhoneUtils::comparePhoneNumbers(info[History::FieldIdentifier].toString(), identifier)); } else { QCOMPARE(info[History::FieldIdentifier].toString(), identifier); } QTRY_COMPARE(contactInfoSpy.count(), 1); QCOMPARE(contactInfoSpy.first()[0].toString(), accountId); QCOMPARE(contactInfoSpy.first()[1].toString(), identifier); info = contactInfoSpy.first()[2].toMap(); QCOMPARE(info[History::FieldContactId].toString(), contactId); if (phoneNumberCompare) { QVERIFY(History::PhoneUtils::comparePhoneNumbers(info[History::FieldIdentifier].toString(), identifier)); } else { QCOMPARE(info[History::FieldIdentifier].toString(), identifier); } } void ContactMatcherTest::testContactAdded() { QSignalSpy contactInfoSpy(History::ContactMatcher::instance(), SIGNAL(contactInfoChanged(QString,QString,QVariantMap))); QString identifier("5555555"); QString accountId("mock/ofono/account0"); QVariantMap info = History::ContactMatcher::instance()->contactInfo(accountId, identifier); QCOMPARE(info[History::FieldIdentifier].toString(), identifier); QVERIFY(!info.contains(History::FieldContactId)); // now add a contact that matches this item QContact contact = createContact("Added", "Contact", QStringList() << identifier); QTRY_COMPARE(contactInfoSpy.count(), 1); QCOMPARE(contactInfoSpy.first()[0].toString(), accountId); QCOMPARE(contactInfoSpy.first()[1].toString(), identifier); QCOMPARE(contactInfoSpy.first()[2].toMap()[History::FieldContactId].toString(), contact.id().toString()); } void ContactMatcherTest::testContactRemoved() { QSignalSpy contactInfoSpy(History::ContactMatcher::instance(), SIGNAL(contactInfoChanged(QString,QString,QVariantMap))); QString identifier("6666666"); QString accountId("mock/ofono/account0"); QVariantMap info = History::ContactMatcher::instance()->contactInfo(accountId, identifier); QCOMPARE(info[History::FieldIdentifier].toString(), identifier); // now add a contact that matches this item QContact contact = createContact("Removed", "Contact", QStringList() << identifier); QTRY_COMPARE(contactInfoSpy.count(), 1); // now that the contact info is filled, remove the contact contactInfoSpy.clear(); QVERIFY(mContactManager->removeContact(contact.id())); QTRY_COMPARE(contactInfoSpy.count(), 1); QCOMPARE(contactInfoSpy.first()[0].toString(), accountId); QCOMPARE(contactInfoSpy.first()[1].toString(), identifier); QVERIFY(!contactInfoSpy.first()[2].toMap().contains(History::FieldContactId)); } void ContactMatcherTest::testSynchronousContactInfoRequest() { QString identifier("77777777"); QString accountId("mock/ofono/account0"); // now add a contact that matches this item QContact contact = createContact("Synchronous", "Contact", QStringList() << identifier); // now that the contact info is filled, remove the contact QVariantMap info = History::ContactMatcher::instance()->contactInfo(accountId, identifier, true); QCOMPARE(info[History::FieldIdentifier].toString(), identifier); QCOMPARE(info[History::FieldAccountId].toString(), accountId); QVERIFY(!info[History::FieldContactId].toString().isEmpty()); // and remove this contact to not interfere in the other tests QVERIFY(mContactManager->removeContact(contact.id())); } void ContactMatcherTest::testWatchIdentifier() { QString identifier("88888888"); QString accountId("mock/ofono/account0"); History::ContactMatcher::instance()->watchIdentifier(accountId, identifier); // now add a contact and make sure we get the contactInfoChanged signal QSignalSpy contactInfoSpy(History::ContactMatcher::instance(), SIGNAL(contactInfoChanged(QString,QString,QVariantMap))); QContact contact = createContact("Contact", "Watched", QStringList() << identifier); QTRY_COMPARE(contactInfoSpy.count(), 1); QCOMPARE(contactInfoSpy.first()[0].toString(), accountId); QCOMPARE(contactInfoSpy.first()[1].toString(), identifier); QVariantMap info = contactInfoSpy.first()[2].toMap(); QCOMPARE(info[History::FieldContactId].toString(), contact.id().toString()); QVERIFY(mContactManager->removeContact(contact.id())); } QContact ContactMatcherTest::createContact(const QString &firstName, const QString &lastName, const QStringList &phoneNumbers, const QStringList &extendedDetails) { QContact contact; QContactName name; name.setFirstName(firstName); name.setLastName(lastName); if (!contact.saveDetail(&name)) { return contact; } Q_FOREACH(const QString &number, phoneNumbers) { QContactPhoneNumber phoneNumber; phoneNumber.setNumber(number); if (!contact.saveDetail(&phoneNumber)) { return contact; } } Q_FOREACH(const QString &extended, extendedDetails) { QContactExtendedDetail extendedDetail; extendedDetail.setName("x-mock-im"); extendedDetail.setData(extended); if (!contact.saveDetail(&extendedDetail)) { return contact; } } mContactManager->saveContact(&contact); return contact; } QTEST_MAIN(ContactMatcherTest) #include "ContactMatcherTest.moc" history-service-0.5/tests/libhistoryservice/EventViewTest.cpp000066400000000000000000000274011455450246700247300ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 #include #include "eventview.h" #include "intersectionfilter.h" #include "manager.h" #include "thread.h" #include "textevent.h" #include "unionfilter.h" #include "voiceevent.h" Q_DECLARE_METATYPE(History::EventType) Q_DECLARE_METATYPE(History::MatchFlags) #define EVENT_COUNT 50 class EventViewTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void testNextPage(); void testFilter_data(); void testFilter(); void testSort(); private: void populate(); }; void EventViewTest::initTestCase() { qRegisterMetaType(); qRegisterMetaType(); populate(); } void EventViewTest::testNextPage() { // create a view to return all text threads and check that the right number of items get returned History::EventViewPtr view = History::Manager::instance()->queryEvents(History::EventTypeText); QVERIFY(view->isValid()); History::Events events = view->nextPage(); History::Events allEvents; while (events.count() > 0) { allEvents << events; events = view->nextPage(); } QCOMPARE(allEvents.count(), EVENT_COUNT * 2 + 1); // include the group text event Q_FOREACH(const History::Event &event, events) { QCOMPARE(event.type(), History::EventTypeText); } } void EventViewTest::testFilter_data() { QTest::addColumn("filterProperties"); QTest::addColumn("eventType"); QTest::addColumn("resultCount"); QTest::addColumn("firstEventProperties"); History::IntersectionFilter filter; filter.append(History::Filter(History::FieldAccountId, "account0")); filter.append(History::Filter(History::FieldThreadId, "participant0")); filter.append(History::Filter(History::FieldEventId, "event21")); History::VoiceEvent voiceEvent = History::Manager::instance()->getSingleEvent(History::EventTypeVoice, "account0", "participant0", "event21"); QVERIFY(!voiceEvent.isNull()); QTest::newRow("filter by accountId, threadId and eventId") << filter.properties() << History::EventTypeVoice << 1 << voiceEvent.properties(); filter.clear(); QStringList participants; participants << "groupParticipant1" << "groupParticipant2"; History::Thread thread = History::Manager::instance()->threadForParticipants("groupAccount", History::EventTypeText, participants, History::MatchCaseSensitive); QVERIFY(!thread.isNull()); History::TextEvent textEvent = History::Manager::instance()->getSingleEvent(History::EventTypeText, thread.accountId(), thread.threadId(), "groupEvent0"); QVERIFY(!textEvent.isNull()); filter.append(History::Filter(History::FieldAccountId, thread.accountId())); filter.append(History::Filter(History::FieldThreadId, thread.threadId())); QTest::newRow("filter for a group conversation") << filter.properties() << History::EventTypeText << 1 << textEvent.properties(); } void EventViewTest::testFilter() { QFETCH(QVariantMap, filterProperties); QFETCH(History::EventType, eventType); QFETCH(int, resultCount); QFETCH(QVariantMap, firstEventProperties); History::Filter filter = History::Filter::fromProperties(filterProperties); History::EventViewPtr view = History::Manager::instance()->queryEvents(eventType, History::Sort(History::FieldAccountId), filter); QVERIFY(view->isValid()); History::Events events = view->nextPage(); QCOMPARE(events.count(), resultCount); History::Event event = events.first(); QCOMPARE(event.properties(), firstEventProperties); // make sure no more items are returned QVERIFY(view->nextPage().isEmpty()); } void EventViewTest::testSort() { History::Sort ascendingSort(History::FieldEventId, Qt::AscendingOrder); History::EventViewPtr view = History::Manager::instance()->queryEvents(History::EventTypeText, ascendingSort); QVERIFY(view->isValid()); History::Events allEvents; History::Events events = view->nextPage(); while (!events.isEmpty()) { allEvents << events; events = view->nextPage(); } QCOMPARE(allEvents.first().eventId(), QString("event00")); QCOMPARE(allEvents.last().eventId(), QString("groupEvent0")); History::Sort descendingSort(History::FieldEventId, Qt::DescendingOrder); allEvents.clear(); view = History::Manager::instance()->queryEvents(History::EventTypeVoice, descendingSort); QVERIFY(view->isValid()); events = view->nextPage(); while (!events.isEmpty()) { allEvents << events; events = view->nextPage(); } QCOMPARE(allEvents.first().eventId(), QString("groupEvent0")); QCOMPARE(allEvents.last().eventId(), QString("event00")); } void EventViewTest::populate() { // create two threads of each type for (int i = 0; i < 2; ++i) { History::Thread voiceThread = History::Manager::instance()->threadForParticipants(QString("account%1").arg(i), History::EventTypeVoice, QStringList() << QString("participant%1").arg(i), History::MatchCaseSensitive, true); QVERIFY(!voiceThread.isNull()); History::Thread textThread = History::Manager::instance()->threadForParticipants(QString("account%1").arg(i), History::EventTypeText, QStringList() << QString("participant%1").arg(i), History::MatchCaseSensitive, true); QVERIFY(!textThread.isNull()); // now create some events for the threads History::Events events; for (int j = 0; j < EVENT_COUNT; ++j) { History::VoiceEvent voiceEvent(voiceThread.accountId(), voiceThread.threadId(), QString("event%1").arg(j, 2, 10, QChar('0')), j % 2 ? "self" : QString("participant%1").arg(i), QDateTime::currentDateTime(), j % 2, j % 2, j % 2 ? QTime(i, j, 0) : QTime()); History::TextEvent textEvent(textThread.accountId(), textThread.threadId(), QString("event%1").arg(j, 2, 10, QChar('0')), j % 2 ? "self" : QString("participant%1").arg(i), QDateTime::currentDateTime(), QDateTime::currentDateTime().addSecs(-10), j % 2, QString("Hello %1").arg(j), History::MessageTypeText, History::MessageStatusDelivered); events << voiceEvent << textEvent; } QVERIFY(History::Manager::instance()->writeEvents(events)); } // create a text thread with multiple participants QStringList participants; participants << "groupParticipant1" << "groupParticipant2"; History::Thread groupTextThread = History::Manager::instance()->threadForParticipants("groupAccount", History::EventTypeText, participants, History::MatchCaseSensitive, true); QVERIFY(!groupTextThread.isNull()); // and write a single event to it, just to make sure it works History::TextEvent groupTextEvent("groupAccount", groupTextThread.threadId(), "groupEvent0", "groupSender", QDateTime::currentDateTime(), QDateTime::currentDateTime().addSecs(-10), true, "A group message", History::MessageTypeText); QVERIFY(History::Manager::instance()->writeEvents(History::Events() << groupTextEvent)); // create a text thread with multiple participants participants.clear(); participants << "groupParticipant1" << "groupParticipant2"; History::Thread groupVoiceThread = History::Manager::instance()->threadForParticipants("groupAccount", History::EventTypeVoice, participants, History::MatchCaseSensitive, true); QVERIFY(!groupVoiceThread.isNull()); // and write a single event to it, just to make sure it works History::VoiceEvent groupVoiceEvent("groupAccount", groupVoiceThread.threadId(), "groupEvent0", "groupSender", QDateTime::currentDateTime(), true, true); QVERIFY(History::Manager::instance()->writeEvents(History::Events() << groupVoiceEvent)); } QTEST_MAIN(EventViewTest) #include "EventViewTest.moc" history-service-0.5/tests/libhistoryservice/FilterTest.cpp000066400000000000000000000363361455450246700242500ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 #include #include "filter.h" #include "intersectionfilter.h" #include "unionfilter.h" Q_DECLARE_METATYPE(History::MatchFlags) Q_DECLARE_METATYPE(History::MatchFlag) Q_DECLARE_METATYPE(History::Filter) class FilterTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void testCreateNewFilter_data(); void testCreateNewFilter(); void testSetProperties_data(); void testSetProperties(); void testToString_data(); void testToString(); void testToStringWithNotEqualsMatch(); void testToStringWithLessMatch(); void testToStringWithGreaterMatch(); void testToStringWithLessOrEqualsMatch(); void testToStringWithGreaterOrEqualsMatch(); void testToStringPrefix(); void testNullToString(); void testMatch_data(); void testMatch(); void testMatchFlags_data(); void testMatchFlags(); void testEqualsOperator(); void testAssignmentOperator(); void testIsValid_data(); void testIsValid(); void testType(); void testProperties(); void testFromProperties(); }; void FilterTest::initTestCase() { qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); } void FilterTest::testCreateNewFilter_data() { QTest::addColumn("filterProperty"); QTest::addColumn("filterValue"); QTest::addColumn("matchFlags"); QTest::newRow("string property and value") << "oneProperty" << QVariant("oneValue") << History::MatchFlags(); QStringList list; list << "oneValue" << "anotherValue" << "yetAnotherValue"; QTest::newRow("a stringlist property") << "stringListProperty" << QVariant(list) << History::MatchFlags(History::MatchCaseSensitive); QTest::newRow("combining two flags and an int property") << "intProperty" << QVariant(11) << History::MatchFlags(History::MatchPhoneNumber | History::MatchContains); } void FilterTest::testCreateNewFilter() { QFETCH(QString, filterProperty); QFETCH(QVariant, filterValue); QFETCH(History::MatchFlags, matchFlags); History::Filter filter(filterProperty, filterValue, matchFlags); QCOMPARE(filter.filterProperty(), filterProperty); QCOMPARE(filter.filterValue(), filterValue); QCOMPARE(filter.matchFlags(), matchFlags); } void FilterTest::testSetProperties_data() { QTest::addColumn("filterProperty"); QTest::addColumn("filterValue"); QTest::addColumn("matchFlags"); QTest::newRow("string property and value") << "oneProperty" << QVariant("oneValue") << History::MatchFlags(); QStringList list; list << "oneValue" << "anotherValue" << "yetAnotherValue"; QTest::newRow("a stringlist property") << "stringListProperty" << QVariant(list) << History::MatchFlags(History::MatchCaseSensitive); QTest::newRow("combining two flags and an int property") << "intProperty" << QVariant(11) << History::MatchFlags(History::MatchPhoneNumber | History::MatchContains); } void FilterTest::testSetProperties() { QFETCH(QString, filterProperty); QFETCH(QVariant, filterValue); QFETCH(History::MatchFlags, matchFlags); History::Filter filter; filter.setFilterProperty(filterProperty); QCOMPARE(filter.filterProperty(), filterProperty); filter.setFilterValue(filterValue); QCOMPARE(filter.filterValue(), filterValue); filter.setMatchFlags(matchFlags); QCOMPARE(filter.matchFlags(), matchFlags); } void FilterTest::testToString_data() { QTest::addColumn("filterProperty"); QTest::addColumn("filterValue"); QTest::addColumn("result"); QTest::newRow("string value") << "stringProperty" << QVariant("stringValue") << "stringProperty=\"stringValue\""; QTest::newRow("bool property with false value") << "boolProperty" << QVariant(false) << "boolProperty=0"; QTest::newRow("bool property with true value") << "boolProperty" << QVariant(true) << "boolProperty=1"; QTest::newRow("int property") << "intProperty" << QVariant(15) << "intProperty=15"; QTest::newRow("double property") << "doubleProperty" << QVariant(1.5) << "doubleProperty=1.5"; } void FilterTest::testToString() { QFETCH(QString, filterProperty); QFETCH(QVariant, filterValue); QFETCH(QString, result); History::Filter filter(filterProperty, filterValue); QCOMPARE(filter.toString(), result); } void FilterTest::testToStringWithNotEqualsMatch() { QString filterProperty("someProperty"); QString filterValue("someValue"); History::Filter filter(filterProperty, filterValue, History::MatchNotEquals); QCOMPARE(filter.toString(),QString("%1!=\"%2\"").arg(filterProperty,filterValue)); } void FilterTest::testToStringWithLessMatch() { QString filterProperty("someProperty"); QString filterValue("someValue"); History::Filter filter(filterProperty, filterValue, History::MatchLess); QCOMPARE(filter.toString(),QString("%1<\"%2\"").arg(filterProperty,filterValue)); } void FilterTest::testToStringWithGreaterMatch() { QString filterProperty("someProperty"); QString filterValue("someValue"); History::Filter filter(filterProperty, filterValue, History::MatchGreater); QCOMPARE(filter.toString(),QString("%1>\"%2\"").arg(filterProperty,filterValue)); } void FilterTest::testToStringWithLessOrEqualsMatch() { QString filterProperty("someProperty"); QString filterValue("someValue"); History::Filter filter(filterProperty, filterValue, History::MatchLessOrEquals); QCOMPARE(filter.toString(),QString("%1<=\"%2\"").arg(filterProperty,filterValue)); } void FilterTest::testToStringWithGreaterOrEqualsMatch() { QString filterProperty("someProperty"); QString filterValue("someValue"); History::Filter filter(filterProperty, filterValue, History::MatchGreaterOrEquals); QCOMPARE(filter.toString(),QString("%1>=\"%2\"").arg(filterProperty,filterValue)); } void FilterTest::testToStringPrefix() { QString prefix("somePrefix"); QString filterProperty("someProperty"); QString filterValue("someValue"); History::Filter filter(filterProperty, filterValue); QVERIFY(filter.toString(prefix).startsWith(QString("%1.").arg(prefix))); QVERIFY(filter.toString().startsWith(filterProperty)); } void FilterTest::testNullToString() { History::Filter filter; QVERIFY(filter.toString().isNull()); } void FilterTest::testMatch_data() { QTest::addColumn("properties"); QTest::addColumn("filterProperty"); QTest::addColumn("filterValue"); QTest::addColumn("matchFlags"); QTest::addColumn("result"); QVariantMap map; map["stringProperty"] = QString("stringValue"); QTest::newRow("simple match of a string property") << map << "stringProperty" << QVariant("stringValue") << History::MatchFlags(History::MatchCaseSensitive) << true; map.clear(); map["stringProperty"] = QString("anotherValue"); QTest::newRow("string property that should not match") << map << "stringProperty" << QVariant("stringValue") << History::MatchFlags(History::MatchCaseSensitive) << false; map.clear(); map["intProperty"] = 42; QTest::newRow("integer property") << map << "intProperty" << QVariant(42) << History::MatchFlags() << true; map.clear(); map["intProperty"] = 41; QTest::newRow("integer property that should not match") << map << "intProperty" << QVariant(42) << History::MatchFlags() << false; map.clear(); map["intProperty"] = 42; QTest::newRow("empty property") << map << "" << QVariant(42) << History::MatchFlags() << true; QTest::newRow("empty value") << map << "intProperty" << QVariant() << History::MatchFlags() << true; // FIXME: add more test cases for the match flags once they are implemented } void FilterTest::testMatch() { QFETCH(QVariantMap, properties); QFETCH(QString, filterProperty); QFETCH(QVariant, filterValue); QFETCH(History::MatchFlags, matchFlags); QFETCH(bool, result); History::Filter filter(filterProperty, filterValue, matchFlags); QCOMPARE(filter.match(properties), result); } void FilterTest::testMatchFlags_data() { QTest::addColumn("flags"); QTest::addColumn("flagToMatch"); QTest::addColumn("match"); /* MatchCaseSensitive = 0x01, MatchCaseInsensitive = 0x02, MatchContains = 0x04, MatchPhoneNumber = 0x08, MatchNotEquals = 0x10, MatchLess = 0x20, MatchGreater = 0x40, MatchLessOrEquals = 0x80, MatchGreaterOrEquals = 0x100 */ QTest::newRow("null flag") << History::MatchFlags() << History::MatchCaseSensitive << false; QTest::newRow("case sensitive alone") << History::MatchFlags(History::MatchCaseSensitive) << History::MatchCaseSensitive << true; QTest::newRow("case insensitive alone") << History::MatchFlags(History::MatchCaseInsensitive) << History::MatchCaseInsensitive << true; QTest::newRow("contains alone") << History::MatchFlags(History::MatchContains) << History::MatchContains << true; QTest::newRow("not contains alone") << History::MatchFlags(History::MatchNotEquals) << History::MatchNotEquals << true; QTest::newRow("less alone") << History::MatchFlags(History::MatchLess) << History::MatchLess << true; QTest::newRow("greater alone") << History::MatchFlags(History::MatchGreater) << History::MatchGreater << true; QTest::newRow("less or equals alone") << History::MatchFlags(History::MatchLessOrEquals) << History::MatchLessOrEquals << true; QTest::newRow("greater or equals alone") << History::MatchFlags(History::MatchGreaterOrEquals) << History::MatchGreaterOrEquals << true; QTest::newRow("phone number alone") << History::MatchFlags(History::MatchPhoneNumber) << History::MatchPhoneNumber << true; QTest::newRow("no mismatch") << History::MatchFlags(History::MatchPhoneNumber) << History::MatchContains << false; QTest::newRow("all still match one") << History::MatchFlags(History::MatchCaseInsensitive | History::MatchCaseSensitive | History::MatchContains | History::MatchPhoneNumber) << History::MatchPhoneNumber << true; } void FilterTest::testMatchFlags() { QFETCH(History::MatchFlags, flags); QFETCH(History::MatchFlag, flagToMatch); QFETCH(bool, match); QCOMPARE(flags.testFlag(flagToMatch), match); QCOMPARE((bool)(flags & flagToMatch), match); } void FilterTest::testEqualsOperator() { History::Filter filterOne("oneProperty", "oneValue"); History::Filter equal("oneProperty", "oneValue"); History::Filter differentProperty("anotherProperty", "oneValue"); History::Filter differentValue("oneProperty", "anotherValue"); QVERIFY(filterOne == equal); QVERIFY(!(filterOne == differentProperty)); QVERIFY(!(filterOne == differentValue)); QVERIFY(filterOne != differentProperty); QVERIFY(filterOne != differentValue); } void FilterTest::testAssignmentOperator() { History::Filter filter(History::FieldAccountId, "OneAccountId", History::MatchFlags(History::MatchContains | History::MatchCaseSensitive)); History::Filter other; other = filter; QVERIFY(other == filter); } void FilterTest::testIsValid_data() { QTest::addColumn("filter"); QTest::addColumn("isValid"); QTest::newRow("null filter") << History::Filter() << false; QTest::newRow("null property") << History::Filter(QString(), "Foobar") << false; QTest::newRow("null value") << History::Filter("oneProperty") << false; QTest::newRow("valid filter") << History::Filter("oneProperty", "oneValue") << true; } void FilterTest::testIsValid() { QFETCH(History::Filter, filter); QFETCH(bool, isValid); QCOMPARE(filter.isValid(), isValid); QCOMPARE(filter.isNull(), !isValid); } void FilterTest::testType() { History::Filter filter; QCOMPARE(filter.type(), History::FilterTypeStandard); } void FilterTest::testProperties() { // test an empty filter History::Filter emptyFilter; QVERIFY(emptyFilter.properties().isEmpty()); // and now a regular filter History::Filter filter("foobarProperty", "foobarValue", History::MatchCaseInsensitive); QVariantMap properties = filter.properties(); QCOMPARE(properties[History::FieldFilterType].toInt(), (int)filter.type()); QCOMPARE(properties[History::FieldFilterProperty].toString(), filter.filterProperty()); QCOMPARE(properties[History::FieldFilterValue], filter.filterValue()); QCOMPARE(properties[History::FieldMatchFlags].toInt(), (int)filter.matchFlags()); } void FilterTest::testFromProperties() { QVariantMap properties; // test an empty filter History::Filter filter = History::Filter::fromProperties(properties); QVERIFY(filter.isNull()); // and now a regular filter properties[History::FieldFilterType] = (int) History::FilterTypeStandard; properties[History::FieldFilterProperty] = "oneProperty"; properties[History::FieldFilterValue] = "oneValue"; properties[History::FieldMatchFlags] = (int) History::MatchContains; filter = History::Filter::fromProperties(properties); QCOMPARE(filter.type(), (History::FilterType)properties[History::FieldFilterType].toInt()); QCOMPARE(filter.filterProperty(), properties[History::FieldFilterProperty].toString()); QCOMPARE(filter.filterValue(), properties[History::FieldFilterValue]); QCOMPARE(filter.matchFlags(), History::MatchFlags(properties[History::FieldMatchFlags].toInt())); // test that calling fromProperties() on intersection filters works as expected History::IntersectionFilter intersectionFilter; intersectionFilter.append(History::Filter("oneProperty", "oneValue")); properties = intersectionFilter.properties(); filter = History::Filter::fromProperties(properties); QCOMPARE(filter.type(), History::FilterTypeIntersection); QCOMPARE(filter.properties(), properties); // and also on union filters History::UnionFilter unionFilter; unionFilter.append(History::Filter("oneProperty", "oneValue")); properties = unionFilter.properties(); filter = History::Filter::fromProperties(properties); QCOMPARE(filter.type(), History::FilterTypeUnion); QCOMPARE(filter.properties(), properties); } QTEST_MAIN(FilterTest) #include "FilterTest.moc" history-service-0.5/tests/libhistoryservice/IntersectionFilterTest.cpp000066400000000000000000000234271455450246700266340ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 #include #include "intersectionfilter.h" Q_DECLARE_METATYPE(History::MatchFlags) Q_DECLARE_METATYPE(History::IntersectionFilter) class IntersectionFilterTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void testSetFilters(); void testAppendFilter(); void testPrependFilter(); void testClear(); void testMatch_data(); void testMatch(); void testToStringWithNoFilters(); void testToStringWithOneFilter(); void testToStringWithManyFilters(); void testConvertToFilterAndBack(); void testIsValid_data(); void testIsValid(); void testProperties(); void testFromProperties(); }; void IntersectionFilterTest::initTestCase() { qRegisterMetaType(); qRegisterMetaType(); } void IntersectionFilterTest::testSetFilters() { // create two filters and check that they are properly set History::Filter filterOne("propertyOne", "valueOne"); History::Filter filterTwo("propertyTwo", "valueTwo"); History::IntersectionFilter intersectionFilter; intersectionFilter.setFilters(QList() << filterOne << filterTwo); QCOMPARE(intersectionFilter.filters().count(), 2); QCOMPARE(intersectionFilter.filters()[0], filterOne); QCOMPARE(intersectionFilter.filters()[1], filterTwo); } void IntersectionFilterTest::testAppendFilter() { // create two filters and check that they are properly set History::Filter filterOne("propertyOne", "valueOne"); History::Filter filterTwo("propertyTwo", "valueTwo"); History::Filter filterThree("propertyThree", "valueThree"); History::IntersectionFilter intersectionFilter; intersectionFilter.setFilters(QList() << filterOne << filterTwo); intersectionFilter.append(filterThree); QCOMPARE(intersectionFilter.filters().count(), 3); QCOMPARE(intersectionFilter.filters()[2], filterThree); } void IntersectionFilterTest::testPrependFilter() { // create two filters and check that they are properly set History::Filter filterOne("propertyOne", "valueOne"); History::Filter filterTwo("propertyTwo", "valueTwo"); History::Filter filterThree("propertyThree", "valueThree"); History::IntersectionFilter intersectionFilter; intersectionFilter.setFilters(QList() << filterOne << filterTwo); intersectionFilter.prepend(filterThree); QCOMPARE(intersectionFilter.filters().count(), 3); QCOMPARE(intersectionFilter.filters()[0], filterThree); } void IntersectionFilterTest::testClear() { // create two filters and check that they are properly set History::Filter filterOne("propertyOne", "valueOne"); History::Filter filterTwo("propertyTwo", "valueTwo"); History::IntersectionFilter intersectionFilter; intersectionFilter.setFilters(QList() << filterOne << filterTwo); intersectionFilter.clear(); QVERIFY(intersectionFilter.filters().isEmpty()); } void IntersectionFilterTest::testMatch_data() { QTest::addColumn("filterProperties"); QTest::addColumn("itemProperties"); QTest::addColumn("result"); // FIXME: take into account the match flags QVariantMap filterProperties; QVariantMap itemProperties; filterProperties["stringProperty"] = QString("stringValue"); filterProperties["intProperty"] = 10; itemProperties = filterProperties; QTest::newRow("all matching values") << filterProperties << itemProperties << true; itemProperties["intProperty"] = 11; QTest::newRow("one of the values is different") << filterProperties << itemProperties << false; itemProperties["stringProperty"] = QString("noMatch"); QTest::newRow("no match at all") << filterProperties << itemProperties << false; QTest::newRow("empty match") << QVariantMap() << itemProperties << true; } void IntersectionFilterTest::testMatch() { QFETCH(QVariantMap, filterProperties); QFETCH(QVariantMap, itemProperties); QFETCH(bool, result); QList filters; Q_FOREACH(const QString &key, filterProperties.keys()) { filters << History::Filter(key, filterProperties[key]); } History::IntersectionFilter intersectionFilter; intersectionFilter.setFilters(filters); QCOMPARE(intersectionFilter.match(itemProperties), result); } void IntersectionFilterTest::testToStringWithNoFilters() { History::IntersectionFilter filter; QVERIFY(filter.toString().isNull()); } void IntersectionFilterTest::testToStringWithOneFilter() { // test that with a single filter the result of toString() is equal to the output // of calling toString() on the filter directly History::Filter filter("aProperty", "aValue"); History::IntersectionFilter intersectionFilter; intersectionFilter.append(filter); QCOMPARE(intersectionFilter.toString(), filter.toString()); } void IntersectionFilterTest::testToStringWithManyFilters() { // check if each of the individual filters are present in the toString output History::Filter filterOne("propertyOne", "valueOne"); History::Filter filterTwo("propertyTwo", "valueTwo"); History::Filter filterThree("propertyThree", "valueThree"); History::IntersectionFilter intersectionFilter; intersectionFilter.setFilters(QList() << filterOne << filterTwo << filterThree); QString stringResult = intersectionFilter.toString(); QVERIFY(stringResult.contains(filterOne.toString())); QVERIFY(stringResult.contains(filterTwo.toString())); QVERIFY(stringResult.contains(filterThree.toString())); } void IntersectionFilterTest::testConvertToFilterAndBack() { History::Filter filterOne("propertyOne", "valueOne"); History::Filter filterTwo("propertyTwo", "valueTwo"); History::Filter filterThree("propertyThree", "valueThree"); History::IntersectionFilter intersectionFilter; intersectionFilter.setFilters(QList() << filterOne << filterTwo << filterThree); History::Filter castFilter = intersectionFilter; QCOMPARE(castFilter.toString(), intersectionFilter.toString()); QCOMPARE(castFilter.type(), History::FilterTypeIntersection); History::IntersectionFilter andBack = castFilter; QCOMPARE(andBack, intersectionFilter); QCOMPARE(andBack.toString(), intersectionFilter.toString()); } void IntersectionFilterTest::testIsValid_data() { QTest::addColumn("filter"); QTest::addColumn("isValid"); History::IntersectionFilter filter; QTest::newRow("invalid filter") << filter << false; filter.append(History::Filter()); QTest::newRow("valid filter") << filter << true; } void IntersectionFilterTest::testIsValid() { QFETCH(History::IntersectionFilter, filter); QFETCH(bool, isValid); QCOMPARE(filter.isValid(), isValid); } void IntersectionFilterTest::testProperties() { History::Filter filterOne("propertyOne", "valueOne"); History::Filter filterTwo("propertyTwo", "valueTwo"); History::Filter filterThree("propertyThree", "valueThree"); History::IntersectionFilter intersectionFilter; intersectionFilter.setFilters(QList() << filterOne << filterTwo << filterThree); QVariantMap properties = intersectionFilter.properties(); QVERIFY(!properties.isEmpty()); QVERIFY(properties.contains(History::FieldFilters)); QCOMPARE(properties[History::FieldFilterType].toInt(), (int) History::FilterTypeIntersection); QVariantList filters = properties[History::FieldFilters].toList(); QCOMPARE(filters.count(), intersectionFilter.filters().count()); QVariantMap propsOne = filters[0].toMap(); QCOMPARE(propsOne, filterOne.properties()); QVariantMap propsTwo = filters[1].toMap(); QCOMPARE(propsTwo, filterTwo.properties()); QVariantMap propsThree = filters[2].toMap(); QCOMPARE(propsThree, filterThree.properties()); // check that a null filter returns an empty QVariantMap History::IntersectionFilter nullFilter; QVERIFY(nullFilter.properties().isEmpty()); } void IntersectionFilterTest::testFromProperties() { QVariantMap properties; // check that a null filter is returned History::Filter nullFilter = History::IntersectionFilter::fromProperties(properties); QVERIFY(nullFilter.isNull()); properties[History::FieldFilterType] = (int)History::FilterTypeIntersection; QVariantList filters; for (int i = 0; i < 3; ++i) { History::Filter filter(QString("filter%1").arg(QString::number(i)), QString("value%1").arg(QString::number(i)), History::MatchCaseInsensitive); filters.append(filter.properties()); } properties[History::FieldFilters] = filters; History::Filter filter = History::IntersectionFilter::fromProperties(properties); QCOMPARE(filter.type(), History::FilterTypeIntersection); History::IntersectionFilter intersectionFilter = filter; QCOMPARE(intersectionFilter.filters().count(), filters.count()); for (int i = 0; i < filters.count(); ++i) { QCOMPARE(intersectionFilter.filters()[i].properties(), filters[i].toMap()); } } QTEST_MAIN(IntersectionFilterTest) #include "IntersectionFilterTest.moc" history-service-0.5/tests/libhistoryservice/ManagerTest.cpp000066400000000000000000000632021455450246700243650ustar00rootroot00000000000000/* * Copyright (C) 2013-2015 Canonical, Ltd. * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 #include #include "config.h" #include "eventview.h" #include "manager.h" #include "mmsstoragemanager_p.h" #include "thread.h" #include "threadview.h" #include "textevent.h" #include "voiceevent.h" Q_DECLARE_METATYPE(History::EventType) Q_DECLARE_METATYPE(History::MatchFlags) Q_DECLARE_METATYPE(History::Threads) Q_DECLARE_METATYPE(History::Events) class ManagerTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void testThreadForParticipants_data(); void testThreadForParticipants(); void testQueryEvents(); void testQueryThreads(); void testGetSingleThread(); void testWriteEvents(); void testRemoveEvents(); void testRemoveEventsByFilter(); void testGetSingleEvent(); void testRemoveThreads(); void cleanup(); void cleanupTestCase(); private: History::Manager *mManager; History::TextEvent createTextEventWithAttachment(QString accountId, QString threadId, QString eventId, QDateTime date, QString textParticipant); }; void ManagerTest::initTestCase() { qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); mManager = History::Manager::instance(); } void ManagerTest::testThreadForParticipants_data() { QTest::addColumn("accountId"); QTest::addColumn("type"); QTest::addColumn("participants"); QTest::addColumn("matchFlags"); QTest::addColumn("participantsToMatch"); QTest::newRow("text thread with one participant") << "oneAccountId" << History::EventTypeText << (QStringList() << "oneParticipant") << History::MatchFlags(History::MatchCaseSensitive) << (QStringList() << "oneParticipant"); QTest::newRow("voice thread using phone match") << "anotherAccountId" << History::EventTypeVoice << (QStringList() << "+554198765432") << History::MatchFlags(History::MatchPhoneNumber) << (QStringList() << "98765432"); } void ManagerTest::testThreadForParticipants() { QFETCH(QString, accountId); QFETCH(History::EventType, type); QFETCH(QStringList, participants); QFETCH(History::MatchFlags, matchFlags); QFETCH(QStringList, participantsToMatch); QSignalSpy spy(mManager, SIGNAL(threadsAdded(History::Threads))); History::Thread thread = mManager->threadForParticipants(accountId, type, participants, matchFlags, true); QVERIFY(!thread.isNull()); QTRY_COMPARE(spy.count(), 1); QCOMPARE(thread.accountId(), accountId); QCOMPARE(thread.type(), type); // now try to get the thread again to see if it is returned correctly History::Thread sameThread = mManager->threadForParticipants(accountId, type, participantsToMatch, matchFlags, false); QVERIFY(!sameThread.isNull()); QCOMPARE(sameThread, thread); } void ManagerTest::testQueryEvents() { // just make sure the view returned is not null // the contents of the view will be tested in its own tests History::EventViewPtr eventView = mManager->queryEvents(History::EventTypeText); QVERIFY(!eventView.isNull()); QVERIFY(eventView->isValid()); } void ManagerTest::testQueryThreads() { // just make sure the view returned is not null // the contents of the view will be tested in its own tests History::ThreadViewPtr threadView = mManager->queryThreads(History::EventTypeVoice); QVERIFY(!threadView.isNull()); QVERIFY(threadView->isValid()); } void ManagerTest::testGetSingleThread() { History::Thread thread = mManager->threadForParticipants("theAccountId", History::EventTypeText, QStringList() << "theParticipant", History::MatchCaseSensitive, true); QVERIFY(!thread.isNull()); // try getting the same thread History::Thread sameThread = mManager->getSingleThread(thread.type(), thread.accountId(), thread.threadId()); QVERIFY(sameThread == thread); } void ManagerTest::testWriteEvents() { QString textParticipant("textParticipant"); QString voiceParticipant("voiceParticipant"); // create two threads, one for voice and one for text History::Thread textThread = mManager->threadForParticipants("textAccountId", History::EventTypeText, QStringList()<< textParticipant, History::MatchCaseSensitive, true); History::Thread voiceThread = mManager->threadForParticipants("voiceAccountId", History::EventTypeVoice, QStringList()<< voiceParticipant, History::MatchCaseSensitive, true); // insert some text and voice events History::Events events; for (int i = 0; i < 50; ++i) { History::TextEvent textEvent(textThread.accountId(), textThread.threadId(), QString("eventId%1").arg(i), textParticipant, QDateTime::currentDateTime(), QDateTime::currentDateTime().addSecs(-10), true, QString("Hello world %1").arg(i), History::MessageTypeText, History::MessageStatusAccepted); events.append(textEvent); History::VoiceEvent voiceEvent(voiceThread.accountId(), voiceThread.threadId(), QString("eventId%1").arg(i), voiceParticipant, QDateTime::currentDateTime(), true, true); events.append(voiceEvent); } QSignalSpy newEventsSpy(mManager, SIGNAL(eventsAdded(History::Events))); QSignalSpy threadsModifiedSpy(mManager, SIGNAL(threadsModified(History::Threads))); QVERIFY(mManager->writeEvents(events)); QTRY_COMPARE(newEventsSpy.count(), 1); QTRY_COMPARE(threadsModifiedSpy.count(), 1); // check that the signal was emitted with the correct number of events History::Events returnedEvents = newEventsSpy.first().first().value(); // get two events to modify before sorting History::TextEvent modifiedTextEvent = events[0]; History::VoiceEvent modifiedVoiceEvent = events[1]; // just in case, sort the lists before comparing qSort(events); qSort(returnedEvents); QCOMPARE(returnedEvents, events); History::Threads returnedThreads = threadsModifiedSpy.first().first().value(); QCOMPARE(returnedThreads.count(), 2); // and now modify the events modifiedTextEvent.setNewEvent(false); modifiedTextEvent.setMessageStatus(History::MessageStatusDelivered); modifiedVoiceEvent.setNewEvent(false); QSignalSpy eventsModifiedSpy(mManager, SIGNAL(eventsModified(History::Events))); threadsModifiedSpy.clear(); events.clear(); events << modifiedTextEvent << modifiedVoiceEvent; QVERIFY(mManager->writeEvents(events)); QTRY_COMPARE(eventsModifiedSpy.count(), 1); QTRY_COMPARE(threadsModifiedSpy.count(), 1); returnedEvents = eventsModifiedSpy.first().first().value(); qDebug() << returnedEvents.first().accountId(); QCOMPARE(returnedEvents.count(), 2); returnedThreads = threadsModifiedSpy.first().first().value(); QCOMPARE(returnedThreads.count(), 2); } void ManagerTest::testRemoveEvents() { QString textParticipant("textParticipant"); QString voiceParticipant("voiceParticipant"); // create two threads, one for voice and one for text History::Thread textThread = mManager->threadForParticipants("textRemovableAccount", History::EventTypeText, QStringList()<< textParticipant, History::MatchCaseSensitive, true); History::Thread voiceThread = mManager->threadForParticipants("voiceRemovableAccount", History::EventTypeVoice, QStringList()<< voiceParticipant, History::MatchCaseSensitive, true); // insert some text and voice events History::Events events; for (int i = 0; i < 50; ++i) { QString eventId = QString("eventToBeRemoved%1").arg(i); History::TextEvent textEvent = createTextEventWithAttachment(textThread.accountId(), textThread.threadId(), eventId, QDateTime::currentDateTime(), textParticipant); events.append(textEvent); History::VoiceEvent voiceEvent(voiceThread.accountId(), voiceThread.threadId(), eventId, voiceParticipant, QDateTime::currentDateTime(), true, true); events.append(voiceEvent); } QSignalSpy eventsRemovedSpy(mManager, SIGNAL(eventsRemoved(History::Events))); QSignalSpy threadsModifiedSpy(mManager, SIGNAL(threadsModified(History::Threads))); QVERIFY(mManager->writeEvents(events)); QTRY_COMPARE(threadsModifiedSpy.count(), 1); threadsModifiedSpy.clear(); History::Events secondRemoval; secondRemoval << events.takeFirst() << events.takeLast(); QVERIFY(mManager->removeEvents(events)); QTRY_COMPARE(eventsRemovedSpy.count(), 1); QTRY_COMPARE(threadsModifiedSpy.count(), 1); History::Events removedEvents = eventsRemovedSpy.first().first().value(); History::Threads modifiedThreads = threadsModifiedSpy.first().first().value(); qSort(events); qSort(removedEvents); QCOMPARE(removedEvents, events); QCOMPARE(modifiedThreads.count(), 2); // we should have only one attachment left QString mmsStoragePath = History::MmsStorageManager::instance()->mmsStoragePath(textThread.accountId(), textThread.threadId()); auto dirInfoList = QDir(mmsStoragePath).entryInfoList(QDir::NoDotAndDotDot|QDir::AllEntries); QCOMPARE(dirInfoList.size(), 1); History::TextEvent remainingTxtEvent = (History::TextEvent) secondRemoval.at(0); QString filePath = remainingTxtEvent.attachments().at(0).filePath(); QVERIFY(QFile(filePath).exists()); // now remove the remaining events and make sure the threads get removed too QSignalSpy threadsRemovedSpy(mManager, SIGNAL(threadsRemoved(History::Threads))); eventsRemovedSpy.clear(); QVERIFY(mManager->removeEvents(secondRemoval)); QTRY_COMPARE(eventsRemovedSpy.count(), 1); QTRY_COMPARE(threadsRemovedSpy.count(), 1); removedEvents = eventsRemovedSpy.first().first().value(); History::Threads removedThreads = threadsRemovedSpy.first().first().value(); qSort(removedEvents); qSort(secondRemoval); QCOMPARE(removedEvents, secondRemoval); QCOMPARE(removedThreads.count(), 2); // now the mmsStorage should be cleaned: dirInfoList = QDir(mmsStoragePath).entryInfoList(QDir::NoDotAndDotDot|QDir::AllEntries); QCOMPARE(dirInfoList.size(), 0); } void ManagerTest::testRemoveEventsByFilter() { QString textParticipant("textParticipant"); QString voiceParticipant("voiceParticipant"); QDateTime currentDate = QDateTime::currentDateTime(); // create two threads, one for voice and one for text History::Thread textThread = mManager->threadForParticipants("textRemovableAccount", History::EventTypeText, QStringList()<< textParticipant, History::MatchCaseSensitive, true); History::Thread voiceThread = mManager->threadForParticipants("voiceRemovableAccount", History::EventTypeVoice, QStringList()<< voiceParticipant, History::MatchCaseSensitive, true); // insert some text and voice events History::Events events; History::Events eventsExpectedToBeRemoved; int daysToRemove = 10; for (int i = 0; i < 50; ++i) { QString eventId = QString("eventToBeRemoved%1").arg(i); History::TextEvent textEvent = createTextEventWithAttachment(textThread.accountId(), textThread.threadId(), eventId, currentDate.addDays(i), textParticipant); events.append(textEvent); History::VoiceEvent voiceEvent(voiceThread.accountId(), voiceThread.threadId(), eventId, voiceParticipant, currentDate.addDays(i), true, true); if (i < daysToRemove) { eventsExpectedToBeRemoved << voiceEvent; } events.append(voiceEvent); } QVERIFY(mManager->writeEvents(events)); History::Filter filter; filter.setFilterProperty(History::FieldTimestamp); filter.setFilterValue(currentDate.addDays(daysToRemove).toString("yyyy-MM-ddTHH:mm:ss.zzz")); //should delete 10 voice_events filter.setMatchFlags(History::MatchLess); auto onVoiceEventsRemoveCompleted = [this](int removedCount, bool isError) { QCOMPARE(removedCount, 10); QCOMPARE(isError, false); }; mManager->removeEvents(History::EventTypeVoice ,filter, onVoiceEventsRemoveCompleted); QTest::qWait(100); History::EventViewPtr eventView = mManager->queryEvents(History::EventTypeVoice); events = eventView->nextPage(); History::Events remainingVoiceEvents; while (events.count() > 0) { remainingVoiceEvents << events; events = eventView->nextPage(); } QCOMPARE(remainingVoiceEvents.count(), 40); // no attachment should be removed QString mmsStoragePath = History::MmsStorageManager::instance()->mmsStoragePath(textThread.accountId(), textThread.threadId()); auto dirInfoList = QDir(mmsStoragePath).entryInfoList(QDir::NoDotAndDotDot|QDir::AllEntries); QCOMPARE(dirInfoList.size(), 50); // expect removed Events not being there anymore Q_FOREACH(const History::Event &event, eventsExpectedToBeRemoved) { QCOMPARE(remainingVoiceEvents.contains(event), false); } // now remove the remaining events and make sure the threads get removed too filter.setFilterValue(currentDate.addYears(1).toString("yyyy-MM-ddTHH:mm:ss.zzz")); auto onRemainingVoiceEventsRemoveCompleted = [this](int removedCount, bool isError) { QCOMPARE(removedCount, 40); QCOMPARE(isError, false); }; mManager->removeEvents(History::EventTypeVoice ,filter, onRemainingVoiceEventsRemoveCompleted); QTest::qWait(100); //verify text events are still there QCOMPARE(mManager->getEventsCount(History::EventTypeText ,filter), 50); //remove them auto onRemainingTextEventsRemoveCompleted = [this](int removedCount, bool isError) { QCOMPARE(removedCount, 50); QCOMPARE(isError, false); }; mManager->removeEvents(History::EventTypeText ,filter, onRemainingTextEventsRemoveCompleted); QTest::qWait(100); QCOMPARE(mManager->getEventsCount(History::EventTypeText ,filter), 0); QVERIFY(!QDir(mmsStoragePath).exists()); } void ManagerTest::testGetSingleEvent() { QString textParticipant("textSingleParticipant"); QString voiceParticipant("voiceSingleParticipant"); // create two threads, one for voice and one for text History::Thread textThread = mManager->threadForParticipants("textSingleAccount", History::EventTypeText, QStringList()<< textParticipant, History::MatchCaseSensitive, true); History::Thread voiceThread = mManager->threadForParticipants("voiceSingleAccount", History::EventTypeVoice, QStringList()<< voiceParticipant, History::MatchCaseSensitive, true); // now add two events History::TextEvent textEvent(textThread.accountId(), textThread.threadId(), "singleEventId", "self", QDateTime::currentDateTime(), QDateTime::currentDateTime().addSecs(-10), true, "Hello big world!", History::MessageTypeText, History::MessageStatusPending); History::VoiceEvent voiceEvent(voiceThread.accountId(), voiceThread.threadId(), "singleEventId", "self", QDateTime::currentDateTime(), false, false, QTime(1,2,3)); QVERIFY(mManager->writeEvents(History::Events() << textEvent << voiceEvent)); // and now try to get them History::TextEvent retrievedTextEvent = mManager->getSingleEvent(History::EventTypeText, textEvent.accountId(), textEvent.threadId(), textEvent.eventId()); QVERIFY(retrievedTextEvent == textEvent); QCOMPARE(retrievedTextEvent.newEvent(), textEvent.newEvent()); QCOMPARE(retrievedTextEvent.message(), textEvent.message()); QCOMPARE(retrievedTextEvent.sentTime(), textEvent.sentTime()); QCOMPARE(retrievedTextEvent.messageType(), textEvent.messageType()); QCOMPARE(retrievedTextEvent.messageStatus(), textEvent.messageStatus()); History::VoiceEvent retrievedVoiceEvent = mManager->getSingleEvent(History::EventTypeVoice, voiceEvent.accountId(), voiceEvent.threadId(), voiceEvent.eventId()); QVERIFY(retrievedVoiceEvent == voiceEvent); QCOMPARE(retrievedVoiceEvent.newEvent(), voiceEvent.newEvent()); QCOMPARE(retrievedVoiceEvent.missed(), voiceEvent.missed()); QCOMPARE(retrievedVoiceEvent.duration(), voiceEvent.duration()); } void ManagerTest::testRemoveThreads() { // create two threads, one for voice and one for text History::Thread textThread = mManager->threadForParticipants("textThreadRemovalAccount", History::EventTypeText, QStringList()<< "textParticipant", History::MatchCaseSensitive, true); QVERIFY(!textThread.isNull()); History::Thread voiceThread = mManager->threadForParticipants("voiceThreadRemovalAccount", History::EventTypeVoice, QStringList()<< "voiceParticipant", History::MatchCaseSensitive, true); QVERIFY(!voiceThread.isNull()); History::Threads threads; threads << textThread << voiceThread; // insert some events QString textParticipant("textParticipant"); QDateTime currentDate = QDateTime::currentDateTime(); History::Events events; for (int i = 0; i < 5; ++i) { QString eventId = QString("eventToBeRemoved%1").arg(i); History::TextEvent textEvent = createTextEventWithAttachment(textThread.accountId(), textThread.threadId(), eventId, currentDate.addDays(i), textParticipant); events.append(textEvent); } QVERIFY(mManager->writeEvents(events)); QSignalSpy threadsRemovedSpy(mManager, SIGNAL(threadsRemoved(History::Threads))); QVERIFY(mManager->removeThreads(threads)); QTRY_COMPARE(threadsRemovedSpy.count(), 1); History::Threads removedThreads = threadsRemovedSpy.first().first().value(); qSort(removedThreads); qSort(threads); QCOMPARE(removedThreads, threads); //all attachments should be removed QString mmsStoragePath = History::MmsStorageManager::instance()->mmsStoragePath(textThread.accountId(), textThread.threadId()); QVERIFY(!QDir(mmsStoragePath).exists()); } void ManagerTest::cleanup() { History::Filter filter; filter.setFilterProperty(History::FieldTimestamp); filter.setFilterValue(QDateTime::currentDateTime().addYears(10).toString("yyyy-MM-ddTHH:mm:ss.zzz")); filter.setMatchFlags(History::MatchLess); int voiceEventsCount = mManager->getEventsCount(History::EventTypeVoice, filter); int textEventsCount = mManager->getEventsCount(History::EventTypeText, filter); int totalToRemove = voiceEventsCount + textEventsCount; if (totalToRemove > 0) { auto onVoiceEventsRemoveCompleted = [this, voiceEventsCount](int removedCount, bool isError) { QCOMPARE(removedCount, voiceEventsCount); QCOMPARE(isError, 0); }; auto onTextEventsRemoveCompleted = [this, textEventsCount](int removedCount, bool isError) { QCOMPARE(removedCount, textEventsCount); QCOMPARE(isError, 0); }; mManager->removeEvents(History::EventTypeVoice, filter, onVoiceEventsRemoveCompleted); mManager->removeEvents(History::EventTypeText, filter, onTextEventsRemoveCompleted); QTest::qWait(100); } } void ManagerTest::cleanupTestCase() { delete mManager; } History::TextEvent ManagerTest::createTextEventWithAttachment(QString accountId, QString threadId, QString eventId, QDateTime date, QString textParticipant) { // prepare some attachments, store them QString mmsStoragePath = History::MmsStorageManager::instance()->mmsStoragePath(accountId, threadId, eventId); QString attachmentPath = mmsStoragePath + "/dialer-app.png"; QDir dir(mmsStoragePath); if (!dir.mkpath(mmsStoragePath)) { qWarning() << "fail to create" << mmsStoragePath; } QFile::copy(QString("%1/dialer-app.png").arg(TEST_DATA_DIR), attachmentPath); History::TextEventAttachment attachment(accountId, threadId, eventId, QString("dialer-app.png"), QString("image/png"), attachmentPath) ; History::TextEventAttachments attachments; attachments << attachment; History::TextEvent textEvent(accountId, threadId, eventId, textParticipant, date, date.addSecs(-10), true, QString("Hello world %1").arg(eventId), History::MessageTypeMultiPart, History::MessageStatusDelivered, QDateTime(), QString(), History::InformationType::InformationTypeNone, attachments); return textEvent; } QTEST_MAIN(ManagerTest) #include "ManagerTest.moc" history-service-0.5/tests/libhistoryservice/MmsStorageManagerTest.cpp000066400000000000000000000232661455450246700263750ustar00rootroot00000000000000/* * Copyright (C) 2022 Ubports Foundation * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 #include #include #include "config.h" #include "mmsstoragemanager_p.h" #include "types.h" Q_DECLARE_METATYPE(History::EventType) class MmsStorageManagerTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void testMmsStoragePath(); void testSaveAttachment(); void testRemoveAttachments(); void testRemoveAttachmentsFromThreads(); void testRemoveAttachmentsFromEvents(); void testRemoveAttachmentsFromFilePaths(); void cleanup(); private: QString storeFile(const QString &accountId, const QString &threadId, const QString &eventId); }; void MmsStorageManagerTest::initTestCase() { qRegisterMetaType(); } void MmsStorageManagerTest::cleanup() { QString accountPath = History::MmsStorageManager::instance()->mmsStoragePath("accountId"); QDir(accountPath).removeRecursively(); } void MmsStorageManagerTest::testMmsStoragePath() { QString normalizedAccountId = QString(QCryptographicHash::hash("accountId", QCryptographicHash::Md5).toHex()); QString normalizedThreadId = QString(QCryptographicHash::hash("threadId", QCryptographicHash::Md5).toHex()); QString normalizedEventId = QString(QCryptographicHash::hash("eventId", QCryptographicHash::Md5).toHex()); QString rootPath = History::MmsStorageManager::instance()->storageRootPath(); QString threadPath = History::MmsStorageManager::instance()->mmsStoragePath("accountId", "threadId"); QString expectedSubPath = QString("/%1/%2").arg(normalizedAccountId, normalizedThreadId); QString expectedPath = QString("%1%2").arg(rootPath).arg(expectedSubPath); QCOMPARE(threadPath, expectedPath); threadPath = History::MmsStorageManager::instance()->mmsStoragePath("accountId", "threadId", ""); QCOMPARE(threadPath, expectedPath); QString eventPath = History::MmsStorageManager::instance()->mmsStoragePath("accountId", "threadId", "eventId"); expectedSubPath = QString("/%1/%2/%3").arg(normalizedAccountId, normalizedThreadId, normalizedEventId); expectedPath = QString("%1%2").arg(rootPath).arg(expectedSubPath); QCOMPARE(eventPath, expectedPath); } void MmsStorageManagerTest::testSaveAttachment() { QFile file(QString("%1/dialer-app.png").arg(TEST_DATA_DIR)); qint64 size = file.size(); QByteArray ba; file.open(QIODevice::ReadOnly); ba = file.readAll(); Tp::MessagePart part; part["identifier"] = QDBusVariant("dialer-app.png"); part["content"] = QDBusVariant(ba); QString path = History::MmsStorageManager::instance()->saveAttachment(part, "accountId", "threadId", "eventId"); QString eventStorage = History::MmsStorageManager::instance()->mmsStoragePath("accountId", "threadId", "eventId"); QString expectedPath = QString("%1/%2").arg(eventStorage).arg("dialer-app.png"); QFile storedFile(expectedPath); QCOMPARE(path, expectedPath); QCOMPARE(storedFile.size(), size); // check if filename strip chars part["identifier"] = QDBusVariant(""); path = History::MmsStorageManager::instance()->saveAttachment(part, "accountId", "threadId", "eventId"); eventStorage = History::MmsStorageManager::instance()->mmsStoragePath("accountId", "threadId", "eventId"); expectedPath = QString("%1/%2").arg(eventStorage).arg("dialer-app2.png"); QFile storedFile2(expectedPath); QCOMPARE(path, expectedPath); QCOMPARE(storedFile2.size(), size); // if empty identifier, filename should be a simple int value from the internal counter part["identifier"] = QDBusVariant(""); QDir dir(eventStorage); dir.setFilter( QDir::AllEntries | QDir::NoDotAndDotDot ); int dirCount = dir.count(); dirCount++; path = History::MmsStorageManager::instance()->saveAttachment(part, "accountId", "threadId", "eventId"); expectedPath = QString("%1/%2").arg(eventStorage).arg(QString::number(dirCount)); QCOMPARE(path, expectedPath); // make sure filenames are incremented dirCount++; path = History::MmsStorageManager::instance()->saveAttachment(part, "accountId", "threadId", "eventId"); expectedPath = QString("%1/%2").arg(eventStorage).arg(QString::number(dirCount)); QCOMPARE(path, expectedPath); dirCount++; part["identifier"] = QDBusVariant("<>"); path = History::MmsStorageManager::instance()->saveAttachment(part, "accountId", "threadId", "eventId"); expectedPath = QString("%1/%2").arg(eventStorage).arg(QString::number(dirCount)); QCOMPARE(path, expectedPath); } void MmsStorageManagerTest::testRemoveAttachments() { QString attachment1 = storeFile("accountId", "threadId", "eventId1"); QVERIFY(QFile(attachment1).exists()); QString attachment2 = storeFile("accountId", "threadId", "eventId2"); QVERIFY(QFile(attachment2).exists()); History::MmsStorageManager::instance()->removeAttachments("accountId", "threadId"); // thread folder should be removed QVERIFY(!QFileInfo(attachment2).dir().exists()); attachment1 = storeFile("accountId", "threadId", "eventId1"); QVERIFY(QFile(attachment1).exists()); attachment2 = storeFile("accountId", "threadId", "eventId2"); QVERIFY(QFile(attachment2).exists()); History::MmsStorageManager::instance()->removeAttachments("accountId", "threadId", "eventId1"); QVERIFY(!QFile(attachment1).exists()); } void MmsStorageManagerTest::testRemoveAttachmentsFromThreads() { QString attachment1 = storeFile("accountId", "threadId1", "eventId1"); QVERIFY(QFile(attachment1).exists()); QString attachment2 = storeFile("accountId", "threadId2", "eventId2"); QVERIFY(QFile(attachment2).exists()); QList threads; QVariantMap thread; thread[History::FieldType] = QVariant::fromValue(History::EventTypeText); thread[History::FieldAccountId] = QVariant("accountId"); thread[History::FieldThreadId] = QVariant("threadId1"); QVariantMap thread2; thread2[History::FieldType] = QVariant::fromValue(History::EventTypeText); thread2[History::FieldAccountId] = QVariant("accountId"); thread2[History::FieldThreadId] = QVariant("threadId2"); threads << thread << thread2; History::MmsStorageManager::instance()->removeAttachmentsFromThreads(threads); // no more threads directories QString accountPath = History::MmsStorageManager::instance()->mmsStoragePath("accountId"); QVERIFY(QDir(accountPath).isEmpty()); } void MmsStorageManagerTest::testRemoveAttachmentsFromEvents() { QString attachment1 = storeFile("accountId", "threadId1", "eventId1"); QVERIFY(QFile(attachment1).exists()); QString attachment2 = storeFile("accountId", "threadId2", "eventId2"); QVERIFY(QFile(attachment2).exists()); QList events; QVariantMap event; event[History::FieldType] = QVariant::fromValue(History::EventTypeText); event[History::FieldAccountId] = QVariant("accountId"); event[History::FieldThreadId] = QVariant("threadId1"); event[History::FieldEventId] = QVariant("eventId1"); events << event; History::MmsStorageManager::instance()->removeAttachmentsFromEvents(events); QVERIFY(QFileInfo(attachment1).dir().isEmpty()); QVERIFY(QFileInfo(attachment2).exists()); QVariantMap event2; event2[History::FieldType] = QVariant::fromValue(History::EventTypeText); event2[History::FieldAccountId] = QVariant("accountId"); event2[History::FieldThreadId] = QVariant("threadId2"); event2[History::FieldEventId] = QVariant("eventId2"); events << event2; History::MmsStorageManager::instance()->removeAttachmentsFromEvents(events); // no more events QVERIFY(QFileInfo(attachment1).dir().isEmpty()); QVERIFY(QFileInfo(attachment2).dir().isEmpty()); } void MmsStorageManagerTest::testRemoveAttachmentsFromFilePaths() { QList filePaths; int maxFile = 20; for (int i = 0; i < maxFile; ++i) { QString attachment = storeFile("accountId", "threadId", QString("eventId%1").arg(i)); filePaths << attachment; } QString threadPath = History::MmsStorageManager::instance()->mmsStoragePath("accountId", "threadId"); auto dirInfoList = QDir(threadPath).entryInfoList(QDir::NoDotAndDotDot|QDir::AllEntries); QCOMPARE(dirInfoList.size(), maxFile); History::MmsStorageManager::instance()->removeAttachments(filePaths); // the thread directory should have been deleted too QVERIFY(QFileInfo(threadPath).dir().isEmpty()); } QString MmsStorageManagerTest::storeFile(const QString &accountId, const QString &threadId, const QString &eventId) { // prepare some attachments, store them QString mmsStoragePath = History::MmsStorageManager::instance()->mmsStoragePath(accountId, threadId, eventId); QString attachmentPath = mmsStoragePath + "/dialer-app.png"; QDir dir(mmsStoragePath); if (!dir.mkpath(mmsStoragePath)) { qWarning() << "fail to create" << mmsStoragePath; } QFile::copy(QString("%1/dialer-app.png").arg(TEST_DATA_DIR), attachmentPath); return attachmentPath; } QTEST_MAIN(MmsStorageManagerTest) #include "MmsStorageManagerTest.moc" history-service-0.5/tests/libhistoryservice/ParticipantTest.cpp000066400000000000000000000242771455450246700253020ustar00rootroot00000000000000/* * Copyright (C) 2015 Canonical, Ltd. * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 #include #include "participant.h" #include "types.h" Q_DECLARE_METATYPE(History::Participant) class ParticipantTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void testConstructor(); void testNullParticipant(); void testIsNull_data(); void testIsNull(); void testCopyConstructor(); void testAssignmentOperator(); void testEqualsOperator_data(); void testEqualsOperator(); void testProperties(); void testFromProperties(); void testIdentifiers(); void testFromVariantList(); void testToVariantList(); void testFromVariantWithVariantList(); }; void ParticipantTest::initTestCase() { qRegisterMetaType(); } void ParticipantTest::testConstructor() { QString accountId("theAccountId"); QString identifier("theParticipantId"); QString contactId("theContactId"); QString alias("theAlias"); QString avatar("theAvatar"); uint state = History::ParticipantStateRegular; uint roles = History::ParticipantRoleMember; QVariantMap detailProperties; detailProperties["someProperty"] = "someValue"; History::Participant participant(accountId, identifier, contactId, alias, avatar, state, roles, detailProperties); QCOMPARE(participant.accountId(), accountId); QCOMPARE(participant.identifier(), identifier); QCOMPARE(participant.contactId(), contactId); QCOMPARE(participant.alias(), alias); QCOMPARE(participant.avatar(), avatar); QCOMPARE(participant.state(), state); QCOMPARE(participant.roles(), roles); QCOMPARE(participant.detailProperties(), detailProperties); } void ParticipantTest::testNullParticipant() { // check that a null participant returns true History::Participant nullParticipant; QVERIFY(nullParticipant.isNull()); } void ParticipantTest::testIsNull_data() { QTest::addColumn("accountId"); QTest::addColumn("identifier"); QTest::addColumn("isNull"); QTest::newRow("all null") << QString() << QString() << true; QTest::newRow("null accountId") << QString() << "some identifier" << true; QTest::newRow("null identifier") << "some account ID" << QString() << true; QTest::newRow("valid account and identifier") << "theAccountId" << "theIdentifier" << false; } void ParticipantTest::testIsNull() { QFETCH(QString, accountId); QFETCH(QString, identifier); QFETCH(bool, isNull); History::Participant participant(accountId, identifier); QCOMPARE(participant.isNull(), isNull); } void ParticipantTest::testCopyConstructor() { QVariantMap detailProperties; detailProperties["theProperty"] = "theValue"; History::Participant original("accountId", "identifier", "contactId", "alias", "avatar", History::ParticipantStateRegular, History::ParticipantRoleAdmin, detailProperties); History::Participant copy(original); QCOMPARE(copy.accountId(), original.accountId()); QCOMPARE(copy.identifier(), original.identifier()); QCOMPARE(copy.contactId(), original.contactId()); QCOMPARE(copy.alias(), original.alias()); QCOMPARE(copy.avatar(), original.avatar()); QCOMPARE(copy.state(), original.state()); QCOMPARE(copy.roles(), original.roles()); QCOMPARE(copy.detailProperties(), original.detailProperties()); } void ParticipantTest::testAssignmentOperator() { QVariantMap detailProperties; detailProperties["theProperty2"] = "theValue2"; History::Participant original("accountId2", "identifier2", "contactId2", "alias2", "avatar2", History::ParticipantStateRegular, History::ParticipantRoleMember, detailProperties); History::Participant copy; copy = original; QCOMPARE(copy.accountId(), original.accountId()); QCOMPARE(copy.identifier(), original.identifier()); QCOMPARE(copy.contactId(), original.contactId()); QCOMPARE(copy.alias(), original.alias()); QCOMPARE(copy.avatar(), original.avatar()); QCOMPARE(copy.state(), original.state()); QCOMPARE(copy.roles(), original.roles()); QCOMPARE(copy.detailProperties(), original.detailProperties()); } void ParticipantTest::testEqualsOperator_data() { QTest::addColumn("accountId1"); QTest::addColumn("identifier1"); QTest::addColumn("accountId2"); QTest::addColumn("identifier2"); QTest::addColumn("equals"); QTest::newRow("same participant") << "theAccountId" << "theIdentifier" << "theAccountId" << "theIdentifier" << true; QTest::newRow("different identifiers") << "theAccountId" << "theIdentifier1" << "theAccountId" << "theIdentifier2" << false; QTest::newRow("different accounts") << "theAccountId1" << "theIdentifier" << "theAccountId2" << "theIdentifier" << false; QTest::newRow("all different") << "theAccountId" << "theIdentifier" << "theAccountId2" << "theIdentifier2" << false; } void ParticipantTest::testEqualsOperator() { QFETCH(QString, accountId1); QFETCH(QString, identifier1); QFETCH(QString, accountId2); QFETCH(QString, identifier2); QFETCH(bool, equals); History::Participant participant1(accountId1, identifier1); History::Participant participant2(accountId2, identifier2); QCOMPARE((participant1 == participant2), equals); } void ParticipantTest::testProperties() { QVariantMap detailProperties; detailProperties["someDetailProperty"] = "someValue"; History::Participant participant("theAccountId", "theIdentifier", "theContactId", "theAlias", "theAvatar", History::ParticipantStateRegular, History::ParticipantRoleAdmin, detailProperties); QVariantMap properties = participant.properties(); QCOMPARE(properties[History::FieldAccountId].toString(), participant.accountId()); QCOMPARE(properties[History::FieldIdentifier].toString(), participant.identifier()); QCOMPARE(properties[History::FieldContactId].toString(), participant.contactId()); QCOMPARE(properties[History::FieldAlias].toString(), participant.alias()); QCOMPARE(properties[History::FieldAvatar].toString(), participant.avatar()); QCOMPARE(properties[History::FieldParticipantState].toUInt(), participant.state()); QCOMPARE(properties[History::FieldParticipantRoles].toUInt(), participant.roles()); QCOMPARE(properties[History::FieldDetailProperties].toMap(), participant.detailProperties()); } void ParticipantTest::testFromProperties() { QVariantMap properties; QVariantMap detailProperties; properties[History::FieldAccountId] = "someAccountId"; properties[History::FieldIdentifier] = "someIdentifier"; properties[History::FieldContactId] = "someContactId"; properties[History::FieldAlias] = "someAlias"; properties[History::FieldAvatar] = "someAvatar"; properties[History::FieldParticipantState] = History::ParticipantStateRegular; properties[History::FieldParticipantRoles] = History::ParticipantRoleAdmin; detailProperties["someDetailProperty"] = "someValue"; properties[History::FieldDetailProperties] = detailProperties; History::Participant participant = History::Participant::fromProperties(properties); QCOMPARE(participant.accountId(), properties[History::FieldAccountId].toString()); QCOMPARE(participant.identifier(), properties[History::FieldIdentifier].toString()); QCOMPARE(participant.contactId(), properties[History::FieldContactId].toString()); QCOMPARE(participant.alias(), properties[History::FieldAlias].toString()); QCOMPARE(participant.avatar(), properties[History::FieldAvatar].toString()); QCOMPARE(participant.state(), properties[History::FieldParticipantState].toUInt()); QCOMPARE(participant.roles(), properties[History::FieldParticipantRoles].toUInt()); QCOMPARE(participant.detailProperties(), properties[History::FieldDetailProperties].toMap()); } void ParticipantTest::testIdentifiers() { QStringList identifiers; identifiers << "firstId" << "secondId" << "thirdId"; History::Participants participants; Q_FOREACH(const QString &identifier, identifiers) { participants << History::Participant("theAccountId", identifier); } QCOMPARE(participants.identifiers(), identifiers); } void ParticipantTest::testFromVariantList() { QVariantList list; for (int i = 0; i < 10; ++i) { list << History::Participant("theAccountId", QString("identifier%1").arg(QString::number(i))).properties(); } History::Participants participants = History::Participants::fromVariantList(list); QCOMPARE(participants.count(), list.count()); for (int i = 0; i < participants.count(); ++i) { QCOMPARE(participants[i].properties(), list[i].toMap()); } } void ParticipantTest::testToVariantList() { History::Participants participants; for (int i = 0; i < 10; ++i) { participants << History::Participant("theAccountId", QString("identifier%1").arg(QString::number(i))); } QVariantList list = participants.toVariantList(); QCOMPARE(list.count(), participants.count()); for (int i = 0; i < list.count(); ++i) { QCOMPARE(list[i].toMap(), participants[i].properties()); } } void ParticipantTest::testFromVariantWithVariantList() { QVariantList list; for (int i = 0; i < 10; ++i) { list << History::Participant("theAccountId", QString("identifier%1").arg(QString::number(i))).properties(); } History::Participants participants = History::Participants::fromVariant(list); QCOMPARE(participants.count(), list.count()); for (int i = 0; i < participants.count(); ++i) { QCOMPARE(participants[i].properties(), list[i].toMap()); } } QTEST_MAIN(ParticipantTest) #include "ParticipantTest.moc" history-service-0.5/tests/libhistoryservice/PhoneUtilsTest.cpp000066400000000000000000000061611455450246700251060ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 #include #include "phoneutils_p.h" class PhoneUtilsTest : public QObject { Q_OBJECT private Q_SLOTS: void testIsPhoneNumber_data(); void testIsPhoneNumber(); void testComparePhoneNumbers_data(); void testComparePhoneNumbers(); }; void PhoneUtilsTest::testIsPhoneNumber_data() { QTest::addColumn("number"); QTest::addColumn("expectedResult"); QTest::newRow("simple number") << "12345678" << true; QTest::newRow("number with dash") << "1234-5678" << true; QTest::newRow("number with area code") << "(123)12345678" << true; QTest::newRow("number with extension") << "12345678#123" << true; QTest::newRow("short/emergency number") << "190" << true; QTest::newRow("non phone numbers") << "abcdefg" << false; } void PhoneUtilsTest::testIsPhoneNumber() { QFETCH(QString, number); QFETCH(bool, expectedResult); bool result = History::PhoneUtils::isPhoneNumber(number); QCOMPARE(result, expectedResult); } void PhoneUtilsTest::testComparePhoneNumbers_data() { QTest::addColumn("number1"); QTest::addColumn("number2"); QTest::addColumn("expectedResult"); QTest::newRow("string equal") << "12345678" << "12345678" << true; QTest::newRow("number with dash") << "1234-5678" << "12345678" << true; QTest::newRow("number with area code") << "12312345678" << "12345678" << true; QTest::newRow("number with extension") << "12345678#123" << "12345678" << true; QTest::newRow("both numbers with extension") << "(123)12345678#1" << "12345678#1" << true; QTest::newRow("numbers with different extension") << "1234567#1" << "1234567#2" << false; QTest::newRow("short/emergency numbers") << "190" << "190" << true; QTest::newRow("different numbers") << "12345678" << "1234567" << false; QTest::newRow("both non phone numbers") << "abcdefg" << "abcdefg" << true; QTest::newRow("different non phone numbers") << "abcdefg" << "bcdefg" << false; QTest::newRow("phone number and custom string") << "abc12345678" << "12345678" << true; // FIXME: check what other cases we need to test here" } void PhoneUtilsTest::testComparePhoneNumbers() { QFETCH(QString, number1); QFETCH(QString, number2); QFETCH(bool, expectedResult); bool result = History::PhoneUtils::comparePhoneNumbers(number1, number2); QCOMPARE(result, expectedResult); } QTEST_MAIN(PhoneUtilsTest) #include "PhoneUtilsTest.moc" history-service-0.5/tests/libhistoryservice/SortTest.cpp000066400000000000000000000133671455450246700237510ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 #include #include "sort.h" Q_DECLARE_METATYPE(Qt::SortOrder) Q_DECLARE_METATYPE(Qt::CaseSensitivity) class SortTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void testCreateNewSort_data(); void testCreateNewSort(); void testCopyConstructor(); void testSetSortProperties_data(); void testSetSortProperties(); void testFromProperties_data(); void testFromProperties(); void testFromNullProperties(); void testProperties_data(); void testProperties(); }; void SortTest::initTestCase() { qRegisterMetaType(); qRegisterMetaType(); } void SortTest::testCreateNewSort_data() { QTest::addColumn("sortField"); QTest::addColumn("sortOrder"); QTest::addColumn("caseSensitivity"); QTest::newRow("threadId field ascending case sensitive") << "threadId" << Qt::AscendingOrder << Qt::CaseSensitive; QTest::newRow("eventId field descending case insensitive") << "threadId" << Qt::DescendingOrder << Qt::CaseInsensitive; } void SortTest::testCreateNewSort() { QFETCH(QString, sortField); QFETCH(Qt::SortOrder, sortOrder); QFETCH(Qt::CaseSensitivity, caseSensitivity); History::Sort sort(sortField, sortOrder, caseSensitivity); QCOMPARE(sort.sortField(), sortField); QCOMPARE(sort.sortOrder(), sortOrder); QCOMPARE(sort.caseSensitivity(), caseSensitivity); } void SortTest::testCopyConstructor() { History::Sort sort(History::FieldCount, Qt::DescendingOrder, Qt::CaseSensitive); History::Sort otherSort(sort); QCOMPARE(otherSort.sortField(), sort.sortField()); QCOMPARE(otherSort.sortOrder(), sort.sortOrder()); QCOMPARE(otherSort.caseSensitivity(), sort.caseSensitivity()); } void SortTest::testSetSortProperties_data() { QTest::addColumn("sortField"); QTest::addColumn("sortOrder"); QTest::addColumn("caseSensitivity"); QTest::newRow("threadId field ascending case sensitive") << "threadId" << Qt::AscendingOrder << Qt::CaseSensitive; QTest::newRow("eventId field descending case insensitive") << "threadId" << Qt::DescendingOrder << Qt::CaseInsensitive; } void SortTest::testSetSortProperties() { QFETCH(QString, sortField); QFETCH(Qt::SortOrder, sortOrder); QFETCH(Qt::CaseSensitivity, caseSensitivity); History::Sort sort; sort.setSortField(sortField); QCOMPARE(sort.sortField(), sortField); sort.setSortOrder(sortOrder); QCOMPARE(sort.sortOrder(), sortOrder); sort.setCaseSensitivity(caseSensitivity); QCOMPARE(sort.caseSensitivity(), caseSensitivity); } void SortTest::testFromProperties_data() { QTest::addColumn("sortField"); QTest::addColumn("sortOrder"); QTest::addColumn("caseSensitivity"); QTest::newRow("threadId field ascending case sensitive") << "threadId" << Qt::AscendingOrder << Qt::CaseSensitive; QTest::newRow("eventId field descending case insensitive") << "threadId" << Qt::DescendingOrder << Qt::CaseInsensitive; } void SortTest::testFromProperties() { QFETCH(QString, sortField); QFETCH(Qt::SortOrder, sortOrder); QFETCH(Qt::CaseSensitivity, caseSensitivity); QVariantMap properties; properties[History::FieldSortField] = sortField; properties[History::FieldSortOrder] = (int) sortOrder; properties[History::FieldCaseSensitivity] = (int) caseSensitivity; History::Sort sort = History::Sort::fromProperties(properties); sort.setSortField(sortField); QCOMPARE(sort.sortField(), sortField); sort.setSortOrder(sortOrder); QCOMPARE(sort.sortOrder(), sortOrder); sort.setCaseSensitivity(caseSensitivity); QCOMPARE(sort.caseSensitivity(), caseSensitivity); } void SortTest::testFromNullProperties() { History::Sort nullSort; History::Sort sort = History::Sort::fromProperties(QVariantMap()); QCOMPARE(sort.sortField(), nullSort.sortField()); QCOMPARE(sort.sortOrder(), nullSort.sortOrder()); QCOMPARE(sort.caseSensitivity(), nullSort.caseSensitivity()); } void SortTest::testProperties_data() { QTest::addColumn("sortField"); QTest::addColumn("sortOrder"); QTest::addColumn("caseSensitivity"); QTest::newRow("threadId field ascending case sensitive") << "threadId" << Qt::AscendingOrder << Qt::CaseSensitive; QTest::newRow("eventId field descending case insensitive") << "threadId" << Qt::DescendingOrder << Qt::CaseInsensitive; } void SortTest::testProperties() { QFETCH(QString, sortField); QFETCH(Qt::SortOrder, sortOrder); QFETCH(Qt::CaseSensitivity, caseSensitivity); History::Sort sort(sortField, sortOrder, caseSensitivity); QVariantMap properties = sort.properties(); QCOMPARE(properties[History::FieldSortField].toString(), sortField); QCOMPARE(properties[History::FieldSortOrder].toInt(), (int) sortOrder); QCOMPARE(properties[History::FieldCaseSensitivity].toInt(), (int) caseSensitivity); } QTEST_MAIN(SortTest) #include "SortTest.moc" history-service-0.5/tests/libhistoryservice/TextEventAttachmentTest.cpp000066400000000000000000000223611455450246700267530ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 #include #include "thread.h" #include "textevent.h" #include "voiceevent.h" Q_DECLARE_METATYPE(History::AttachmentFlags) Q_DECLARE_METATYPE(History::AttachmentFlag) class TextEventAttachmentTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void testCreateNewTextEventAttachment_data(); void testCreateNewTextEventAttachment(); void testFromProperties(); void testCopyConstructor(); void testAssignment(); void testEquals_data(); void testEquals(); void testIsNull(); }; void TextEventAttachmentTest::initTestCase() { qRegisterMetaType(); qRegisterMetaType(); } void TextEventAttachmentTest::testCreateNewTextEventAttachment_data() { QTest::addColumn("accountId"); QTest::addColumn("threadId"); QTest::addColumn("eventId"); QTest::addColumn("attachmentId"); QTest::addColumn("contentType"); QTest::addColumn("filePath"); QTest::addColumn("status"); QTest::newRow("regular attachment") << "someaccountid" << "somethreadid" << "someeventid" << "someattachmentid" << "image/x-jpeg" << "/some/file/path.jpg" << History::AttachmentFlags(History::AttachmentDownloaded); QTest::newRow("no content type and pending attachment") << "anotheraccountid" << "anotherthreadid" << "anothereventid" << "anotherattachmentid" << "" << "/another/file/path.jpg" << History::AttachmentFlags(History::AttachmentPending); QTest::newRow("attachment with error") << "yetanotheraccountid" << "yetanotherthreadid" << "yetanothereventid" << "yetanotherattachmentid" << "" << "/some/file/path" << History::AttachmentFlags(History::AttachmentError); } void TextEventAttachmentTest::testCreateNewTextEventAttachment() { QFETCH(QString, accountId); QFETCH(QString, threadId); QFETCH(QString, eventId); QFETCH(QString, attachmentId); QFETCH(QString, contentType); QFETCH(QString, filePath); QFETCH(History::AttachmentFlags, status); History::TextEventAttachment attachment(accountId, threadId, eventId, attachmentId, contentType, filePath, status); QCOMPARE(attachment.accountId(), accountId); QCOMPARE(attachment.threadId(), threadId); QCOMPARE(attachment.eventId(), eventId); QCOMPARE(attachment.attachmentId(), attachmentId); QCOMPARE(attachment.contentType(), contentType); QCOMPARE(attachment.filePath(), filePath); QCOMPARE(attachment.status(), status); QVariantMap properties = attachment.properties(); QCOMPARE(properties[History::FieldAccountId].toString(), accountId); QCOMPARE(properties[History::FieldThreadId].toString(), threadId); QCOMPARE(properties[History::FieldEventId].toString(), eventId); QCOMPARE(properties[History::FieldAttachmentId].toString(), attachmentId); QCOMPARE(properties[History::FieldContentType].toString(), contentType); QCOMPARE(properties[History::FieldFilePath].toString(), filePath); QCOMPARE(properties[History::FieldStatus].toInt(), (int) status); } void TextEventAttachmentTest::testFromProperties() { QVariantMap properties; properties[History::FieldAccountId] = "someAccountId"; properties[History::FieldThreadId] = "someThreadId"; properties[History::FieldEventId] = "someEventId"; properties[History::FieldAttachmentId] = "someAttachmentId"; properties[History::FieldContentType] = "someContentType"; properties[History::FieldFilePath] = "/some/file/path"; properties[History::FieldStatus] = (int) History::AttachmentDownloaded; History::TextEventAttachment attachment = History::TextEventAttachment::fromProperties(properties); QCOMPARE(attachment.accountId(), properties[History::FieldAccountId].toString()); QCOMPARE(attachment.threadId(), properties[History::FieldThreadId].toString()); QCOMPARE(attachment.eventId(), properties[History::FieldEventId].toString()); QCOMPARE(attachment.attachmentId(), properties[History::FieldAttachmentId].toString()); QCOMPARE(attachment.contentType(), properties[History::FieldContentType].toString()); QCOMPARE(attachment.filePath(), properties[History::FieldFilePath].toString()); QCOMPARE(attachment.status(), (History::AttachmentFlags) properties[History::FieldStatus].toInt()); // now load from an empty map History::TextEventAttachment emptyAttachment = History::TextEventAttachment::fromProperties(QVariantMap()); QVERIFY(emptyAttachment.isNull()); } void TextEventAttachmentTest::testCopyConstructor() { History::TextEventAttachment attachment("oneAccountId", "oneThreadId", "oneEventId", "oneAttachmentId", "oneContentType", "/one/file/path", History::AttachmentPending); History::TextEventAttachment copy(attachment); QCOMPARE(copy.accountId(), attachment.accountId()); QCOMPARE(copy.threadId(), attachment.threadId()); QCOMPARE(copy.eventId(), attachment.eventId()); QCOMPARE(copy.attachmentId(), attachment.attachmentId()); QCOMPARE(copy.contentType(), attachment.contentType()); QCOMPARE(copy.filePath(), attachment.filePath()); QCOMPARE(copy.status(), attachment.status()); } void TextEventAttachmentTest::testAssignment() { History::TextEventAttachment attachment("oneAccountId", "oneThreadId", "oneEventId", "oneAttachmentId", "oneContentType", "/one/file/path", History::AttachmentPending); History::TextEventAttachment copy; copy = attachment; QCOMPARE(copy.accountId(), attachment.accountId()); QCOMPARE(copy.threadId(), attachment.threadId()); QCOMPARE(copy.eventId(), attachment.eventId()); QCOMPARE(copy.attachmentId(), attachment.attachmentId()); QCOMPARE(copy.contentType(), attachment.contentType()); QCOMPARE(copy.filePath(), attachment.filePath()); QCOMPARE(copy.status(), attachment.status()); } void TextEventAttachmentTest::testEquals_data() { QTest::addColumn("accountId"); QTest::addColumn("threadId"); QTest::addColumn("eventId"); QTest::addColumn("attachmentId"); QTest::addColumn("secondAccountId"); QTest::addColumn("secondThreadId"); QTest::addColumn("secondEventId"); QTest::addColumn("secondAttachmentId"); QTest::addColumn("result"); QTest::newRow("equal") << "someaccountid" << "somethreadid" << "someeventid" << "someattachmentid" << "someaccountid" << "somethreadid" << "someeventid" << "someattachmentid" << true; QTest::newRow("different accountId") << "someaccountid" << "somethreadid" << "someeventid" << "someattachmentid" << "otheraccountid" << "somethreadid" << "someeventid" << "someattachmentid" << false; QTest::newRow("different threadId") << "someaccountid" << "somethreadid" << "someeventid" << "someattachmentid" << "someaccountid" << "otherthreadid" << "someeventid" << "someattachmentid" << false; QTest::newRow("different eventId") << "someaccountid" << "somethreadid" << "someeventid" << "someattachmentid" << "someaccountid" << "somethreadid" << "othereventid" << "someattachmentid" << false; QTest::newRow("different attachmentId") << "someaccountid" << "somethreadid" << "someeventid" << "someattachmentid" << "someaccountid" << "somethreadid" << "someeventid" << "otherattachmentid" << false; } void TextEventAttachmentTest::testEquals() { QFETCH(QString, accountId); QFETCH(QString, threadId); QFETCH(QString, eventId); QFETCH(QString, attachmentId); QFETCH(QString, secondAccountId); QFETCH(QString, secondThreadId); QFETCH(QString, secondEventId); QFETCH(QString, secondAttachmentId); QFETCH(bool, result); History::TextEventAttachment attachment(accountId, threadId, eventId, attachmentId, "oneContentType", "/one/file/path", History::AttachmentPending); History::TextEventAttachment anotherAttachment(secondAccountId, secondThreadId, secondEventId, secondAttachmentId, "anotherContentType", "/different/file/path", History::AttachmentDownloaded); QCOMPARE(attachment == anotherAttachment, result); } void TextEventAttachmentTest::testIsNull() { History::TextEventAttachment attachment; QVERIFY(attachment.isNull()); } QTEST_MAIN(TextEventAttachmentTest) #include "TextEventAttachmentTest.moc" history-service-0.5/tests/libhistoryservice/TextEventTest.cpp000066400000000000000000000475071455450246700247530ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 #include #include "textevent.h" class TextEventTest : public QObject { Q_OBJECT private Q_SLOTS: void testCreateNewEvent_data(); void testCreateNewEvent(); void testCastToEventAndBack(); void testFromProperties_data(); void testFromProperties(); void testFromNullProperties(); void testProperties_data(); void testProperties(); void testSetProperties(); void testNoSentDateTime(); private: History::Participants participantsFromIdentifiers(const QString &accountId, const QStringList &identifiers); }; void TextEventTest::testCreateNewEvent_data() { QTest::addColumn("accountId"); QTest::addColumn("threadId"); QTest::addColumn("eventId"); QTest::addColumn("senderId"); QTest::addColumn("timestamp"); QTest::addColumn("sentTime"); QTest::addColumn("newEvent"); QTest::addColumn("message"); QTest::addColumn("messageType"); QTest::addColumn("messageStatus"); QTest::addColumn("readTimestamp"); QTest::addColumn("subject"); QTest::addColumn("informationType"); QTest::addColumn("participants"); QTest::newRow("unread message") << "testAccountId" << "testThreadId" << "testEventId" << "testSenderId" << QDateTime::currentDateTime().addDays(-10) << QDateTime::currentDateTime() << true << "One Test Message" << (int)History::MessageTypeText << 0 << QDateTime::currentDateTime().addDays(-5) << "Test Subject" << (int) History::InformationTypeJoined << (QStringList() << "testParticipant"); QTest::newRow("read message") << "testAccountId2" << "testThreadId2" << "testEventId2" << "testSenderId2" << QDateTime::currentDateTime().addDays(-10) << QDateTime::currentDateTime() << false << "One Test Message" << (int)History::MessageTypeText << 0 << QDateTime::currentDateTime().addDays(-5) << "Test Subject 2" << (int) History::InformationTypeLeaving << (QStringList() << "testParticipant2"); QTest::newRow("message status") << "testAccountId" << "testThreadId" << "testEventId" << "testSenderId" << QDateTime::currentDateTime().addDays(-10) << QDateTime::currentDateTime() << true << "One Test Message" << (int)History::MessageTypeText << (int)History::MessageStatusAccepted << QDateTime::currentDateTime().addDays(-5) << "Test Subject 3" << (int) History::InformationTypeSelfLeaving << (QStringList() << "testParticipant"); QTest::newRow("multi party message") << "testAccountId" << "testThreadId" << "testEventId" << "testSenderId" << QDateTime::currentDateTime().addDays(-10) << QDateTime::currentDateTime() << true << "One Test Message" << (int)History::MessageTypeMultiPart << 0 << QDateTime::currentDateTime().addDays(-5) << QString() << (int) History::InformationTypeNone << (QStringList() << "testParticipant"); QTest::newRow("multiple participants") << "testAccountId2" << "testThreadId2" << "testEventId2" << "testSenderId2" << QDateTime::currentDateTime().addDays(-7) << QDateTime::currentDateTime() << true << "One Test Message 2" << (int)History::MessageTypeText << 0 << QDateTime::currentDateTime().addDays(-4) << QString() << (int) History::InformationTypeNone << (QStringList() << "one" << "two" << "three" << "four"); } void TextEventTest::testCreateNewEvent() { QFETCH(QString, accountId); QFETCH(QString, threadId); QFETCH(QString, eventId); QFETCH(QString, senderId); QFETCH(QDateTime, timestamp); QFETCH(QDateTime, sentTime); QFETCH(bool, newEvent); QFETCH(QString, message); QFETCH(int, messageType); QFETCH(int, messageStatus); QFETCH(QDateTime, readTimestamp); QFETCH(QString, subject); QFETCH(int, informationType); QFETCH(QStringList, participants); History::TextEvent event(accountId, threadId, eventId, senderId, timestamp, sentTime, newEvent, message, (History::MessageType)messageType, (History::MessageStatus)messageStatus, readTimestamp, subject, (History::InformationType) informationType, History::TextEventAttachments(), participantsFromIdentifiers(accountId, participants)); // check that the values are properly set QCOMPARE(event.accountId(), accountId); QCOMPARE(event.threadId(), threadId); QCOMPARE(event.eventId(), eventId); QCOMPARE(event.senderId(), senderId); QCOMPARE(event.timestamp(), timestamp); QCOMPARE(event.sentTime(), sentTime); QCOMPARE(event.newEvent(), newEvent); QCOMPARE(event.message(), message); QCOMPARE(event.messageType(), (History::MessageType)messageType); QCOMPARE(event.messageStatus(), (History::MessageStatus)messageStatus); QCOMPARE(event.readTimestamp(), readTimestamp); QCOMPARE(event.subject(), subject); QCOMPARE(event.participants().identifiers(), participants); } void TextEventTest::testCastToEventAndBack() { History::TextEvent textEvent("oneAccountId", "oneThreadId", "oneEventId", "oneSender", QDateTime::currentDateTime(), QDateTime::currentDateTime(), true, "Hello", History::MessageTypeText); // test the copy constructor History::Event historyEvent(textEvent); QVERIFY(historyEvent == textEvent); History::TextEvent castBack(historyEvent); QVERIFY(castBack == textEvent); // and now the assignment operator History::Event anotherEvent; anotherEvent = textEvent; QVERIFY(anotherEvent == textEvent); History::TextEvent backAgain; backAgain = anotherEvent; QVERIFY(backAgain == textEvent); } void TextEventTest::testFromProperties_data() { QTest::addColumn("accountId"); QTest::addColumn("threadId"); QTest::addColumn("eventId"); QTest::addColumn("senderId"); QTest::addColumn("timestamp"); QTest::addColumn("newEvent"); QTest::addColumn("message"); QTest::addColumn("messageType"); QTest::addColumn("messageStatus"); QTest::addColumn("readTimestamp"); QTest::addColumn("subject"); QTest::addColumn("informationType"); QTest::addColumn("participants"); QTest::newRow("unread message") << "testAccountId" << "testThreadId" << "testEventId" << "testSenderId" << QDateTime::currentDateTime().addDays(-10) << true << "One Test Message" << (int)History::MessageTypeText << 0 << QDateTime::currentDateTime().addDays(-5) << "Test Subject" << (int) History::InformationTypeJoined << (QStringList() << "testParticipant"); QTest::newRow("read message") << "testAccountId2" << "testThreadId2" << "testEventId2" << "testSenderId2" << QDateTime::currentDateTime().addDays(-10) << false << "One Test Message" << (int)History::MessageTypeText << 0 << QDateTime::currentDateTime().addDays(-5) << "Test Subject 2" << (int) History::InformationTypeNone << (QStringList() << "testParticipant2"); QTest::newRow("message status") << "testAccountId" << "testThreadId" << "testEventId" << "testSenderId" << QDateTime::currentDateTime().addDays(-10) << true << "One Test Message" << (int)History::MessageTypeText << (int)History::MessageStatusAccepted << QDateTime::currentDateTime().addDays(-5) << "Test Subject 3" << (int) History::InformationTypeNone << (QStringList() << "testParticipant"); QTest::newRow("multi party message") << "testAccountId" << "testThreadId" << "testEventId" << "testSenderId" << QDateTime::currentDateTime().addDays(-10) << true << "One Test Message" << (int)History::MessageTypeMultiPart << 0 << QDateTime::currentDateTime().addDays(-5) << QString() << (int) History::InformationTypeNone << (QStringList() << "testParticipant"); QTest::newRow("multiple participants") << "testAccountId2" << "testThreadId2" << "testEventId2" << "testSenderId2" << QDateTime::currentDateTime().addDays(-7) << true << "One Test Message 2" << (int)History::MessageTypeText << 0 << QDateTime::currentDateTime().addDays(-4) << QString() << (int) History::InformationTypeNone << (QStringList() << "one" << "two" << "three" << "four"); } void TextEventTest::testFromProperties() { QFETCH(QString, accountId); QFETCH(QString, threadId); QFETCH(QString, eventId); QFETCH(QString, senderId); QFETCH(QDateTime, timestamp); QFETCH(bool, newEvent); QFETCH(QString, message); QFETCH(int, messageType); QFETCH(int, messageStatus); QFETCH(QDateTime, readTimestamp); QFETCH(QString, subject); QFETCH(int, informationType); QFETCH(QStringList, participants); QVariantMap properties; properties[History::FieldAccountId] = accountId; properties[History::FieldThreadId] = threadId; properties[History::FieldEventId] = eventId; properties[History::FieldSenderId] = senderId; properties[History::FieldTimestamp] = timestamp.toString("yyyy-MM-ddTHH:mm:ss.zzz"); properties[History::FieldSentTime] = timestamp.toString("yyyy-MM-ddTHH:mm:ss.zzz"); properties[History::FieldNewEvent] = newEvent; properties[History::FieldMessage] = message; properties[History::FieldMessageType] = messageType; properties[History::FieldMessageStatus] = messageStatus; properties[History::FieldReadTimestamp] = readTimestamp.toString("yyyy-MM-ddTHH:mm:ss.zzz"); properties[History::FieldSubject] = subject; properties[History::FieldInformationType] = informationType; properties[History::FieldParticipants] = participantsFromIdentifiers(accountId, participants).toVariantList(); History::TextEvent textEvent = History::TextEvent::fromProperties(properties); QCOMPARE(textEvent.accountId(), accountId); QCOMPARE(textEvent.threadId(), threadId); QCOMPARE(textEvent.eventId(), eventId); QCOMPARE(textEvent.senderId(), senderId); QCOMPARE(textEvent.timestamp().toString(Qt::ISODate), timestamp.toString(Qt::ISODate)); QCOMPARE(textEvent.sentTime().toString(Qt::ISODate), timestamp.toString(Qt::ISODate)); QCOMPARE(textEvent.newEvent(), newEvent); QCOMPARE(textEvent.message(), message); QCOMPARE(textEvent.messageType(), (History::MessageType) messageType); QCOMPARE(textEvent.messageStatus(), (History::MessageStatus) messageStatus); QCOMPARE(textEvent.readTimestamp().toString(Qt::ISODate), readTimestamp.toString(Qt::ISODate)); QCOMPARE(textEvent.subject(), subject); QCOMPARE(textEvent.informationType(), (History::InformationType) informationType); QCOMPARE(textEvent.participants().identifiers(), participants); } void TextEventTest::testFromNullProperties() { // just to make sure, test that calling ::fromProperties() on an empty map returns a null event History::Event nullEvent = History::TextEvent::fromProperties(QVariantMap()); QVERIFY(nullEvent.isNull()); QCOMPARE(nullEvent.type(), History::EventTypeNull); } void TextEventTest::testProperties_data() { QTest::addColumn("accountId"); QTest::addColumn("threadId"); QTest::addColumn("eventId"); QTest::addColumn("senderId"); QTest::addColumn("timestamp"); QTest::addColumn("sentTime"); QTest::addColumn("newEvent"); QTest::addColumn("message"); QTest::addColumn("messageType"); QTest::addColumn("messageStatus"); QTest::addColumn("readTimestamp"); QTest::addColumn("subject"); QTest::addColumn("informationType"); QTest::addColumn("participants"); QTest::newRow("unread message") << "testAccountId" << "testThreadId" << "testEventId" << "testSenderId" << QDateTime::currentDateTime().addDays(-10) << QDateTime::currentDateTime() << true << "One Test Message" << (int)History::MessageTypeText << 0 << QDateTime::currentDateTime().addDays(-5) << "Test Subject" << (int) History::InformationTypeNone << (QStringList() << "testParticipant"); QTest::newRow("read message") << "testAccountId2" << "testThreadId2" << "testEventId2" << "testSenderId2" << QDateTime::currentDateTime().addDays(-10) << QDateTime::currentDateTime() << false << "One Test Message" << (int)History::MessageTypeText << 0 << QDateTime::currentDateTime().addDays(-5) << "Test Subject 2" << (int) History::InformationTypeNone << (QStringList() << "testParticipant2"); QTest::newRow("message status") << "testAccountId" << "testThreadId" << "testEventId" << "testSenderId" << QDateTime::currentDateTime().addDays(-10) << QDateTime::currentDateTime() << true << "One Test Message" << (int)History::MessageTypeText << (int)History::MessageStatusAccepted << QDateTime::currentDateTime().addDays(-5) << "Test Subject 3" << (int) History::InformationTypeNone << (QStringList() << "testParticipant"); QTest::newRow("multi party message") << "testAccountId" << "testThreadId" << "testEventId" << "testSenderId" << QDateTime::currentDateTime().addDays(-10) << QDateTime::currentDateTime() << true << "One Test Message" << (int)History::MessageTypeMultiPart << 0 << QDateTime::currentDateTime().addDays(-5) << QString() << (int) History::InformationTypeNone << (QStringList() << "testParticipant"); QTest::newRow("multiple participants") << "testAccountId2" << "testThreadId2" << "testEventId2" << "testSenderId2" << QDateTime::currentDateTime().addDays(-7) << QDateTime::currentDateTime() << true << "One Test Message 2" << (int)History::MessageTypeText << 0 << QDateTime::currentDateTime().addDays(-4) << QString() << (int) History::InformationTypeNone << (QStringList() << "one" << "two" << "three" << "four"); } void TextEventTest::testProperties() { QFETCH(QString, accountId); QFETCH(QString, threadId); QFETCH(QString, eventId); QFETCH(QString, senderId); QFETCH(QDateTime, timestamp); QFETCH(QDateTime, sentTime); QFETCH(bool, newEvent); QFETCH(QString, message); QFETCH(int, messageType); QFETCH(int, messageStatus); QFETCH(QDateTime, readTimestamp); QFETCH(QString, subject); QFETCH(int, informationType); QFETCH(QStringList, participants); History::TextEvent event(accountId, threadId, eventId, senderId, timestamp, sentTime, newEvent, message, (History::MessageType)messageType, (History::MessageStatus)messageStatus, readTimestamp, subject, History::InformationTypeNone, History::TextEventAttachments(), participantsFromIdentifiers(accountId, participants)); QVariantMap properties = event.properties(); QCOMPARE(properties[History::FieldAccountId].toString(), accountId); QCOMPARE(properties[History::FieldThreadId].toString(), threadId); QCOMPARE(properties[History::FieldEventId].toString(), eventId); QCOMPARE(properties[History::FieldSenderId].toString(), senderId); QCOMPARE(properties[History::FieldTimestamp].toString(), timestamp.toString("yyyy-MM-ddTHH:mm:ss.zzz")); QCOMPARE(properties[History::FieldSentTime].toString(), sentTime.toString("yyyy-MM-ddTHH:mm:ss.zzz")); QCOMPARE(properties[History::FieldNewEvent].toBool(), newEvent); QCOMPARE(properties[History::FieldMessage].toString(), message); QCOMPARE(properties[History::FieldMessageType].toInt(), messageType); QCOMPARE(properties[History::FieldMessageStatus].toInt(), messageStatus); QCOMPARE(properties[History::FieldReadTimestamp].toString(), readTimestamp.toString("yyyy-MM-ddTHH:mm:ss.zzz")); QCOMPARE(properties[History::FieldSubject].toString(), subject); QCOMPARE(properties[History::FieldInformationType].toInt(), informationType); QCOMPARE(History::Participants::fromVariantList(properties[History::FieldParticipants].toList()).identifiers(), participants); } void TextEventTest::testSetProperties() { History::TextEvent textEvent("oneAccountId", "oneThreadId", "oneEventId", "oneSender", QDateTime::currentDateTime(), QDateTime::currentDateTime(), true, "Hello", History::MessageTypeText); QDateTime readTimestamp = QDateTime::currentDateTime(); QDateTime sentTime = QDateTime::currentDateTime(); History::MessageStatus status = History::MessageStatusDelivered; bool newEvent = false; textEvent.setReadTimestamp(readTimestamp); textEvent.setMessageStatus(status); textEvent.setNewEvent(newEvent); textEvent.setSentTime(sentTime); QCOMPARE(textEvent.readTimestamp(), readTimestamp); QCOMPARE(textEvent.messageStatus(), status); QCOMPARE(textEvent.newEvent(), newEvent); QCOMPARE(textEvent.sentTime(), sentTime); } void TextEventTest::testNoSentDateTime() { History::TextEvent textEvent("oneAccountId", "oneThreadId", "oneEventId", "oneSender", QDateTime::currentDateTime(), true, "Hello", History::MessageTypeText); // expect sentTime to be equals to receivedTime if not set QCOMPARE(textEvent.sentTime(), textEvent.timestamp()); } History::Participants TextEventTest::participantsFromIdentifiers(const QString &accountId, const QStringList &identifiers) { History::Participants participants; Q_FOREACH(const QString &identifier, identifiers) { participants << History::Participant(accountId, identifier); } return participants; } QTEST_MAIN(TextEventTest) #include "TextEventTest.moc" history-service-0.5/tests/libhistoryservice/ThreadTest.cpp000066400000000000000000000347031455450246700242260ustar00rootroot00000000000000/* * Copyright (C) 2013-2015 Canonical, Ltd. * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 #include #include "thread.h" #include "textevent.h" #include "voiceevent.h" Q_DECLARE_METATYPE(History::EventType) Q_DECLARE_METATYPE(History::Thread) class ThreadTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void testCreateNewThread_data(); void testCreateNewThread(); void testFromProperties_data(); void testFromProperties(); void testFromNullProperties(); void testProperties_data(); void testProperties(); void testIsNull_data(); void testIsNull(); void testEqualsOperator_data(); void testEqualsOperator(); void testCopyConstructor(); void testAssignmentOperator(); private: History::Participants participantsFromIdentifiers(const QString &accountId, const QStringList &identifiers); }; void ThreadTest::initTestCase() { qRegisterMetaType(); qRegisterMetaType(); } void ThreadTest::testCreateNewThread_data() { QTest::addColumn("accountId"); QTest::addColumn("threadId"); QTest::addColumn("type"); QTest::addColumn("participants"); QTest::addColumn("count"); QTest::addColumn("unreadCount"); QTest::newRow("voice thread with count, unread count and one participant") << "someaccountid" << "somethreadid" << History::EventTypeVoice << (QStringList() << "someparticipant") << 10 << 4; QTest::newRow("voice thread with zero messages and two participants") << "anotheraccountid" << "anotherthreadid" << History::EventTypeVoice << (QStringList() << "someparticipant" << "anotherparticipant") << 0 << 0; QTest::newRow("text thread with count, unread count and one participant") << "somevoiceaccountid" << "somevoicethreadid" << History::EventTypeText << (QStringList() << "somevoiceparticipant") << 10 << 4; QTest::newRow("voice thread with zero messages and two participants") << "anothervoiceaccountid" << "anothervoicethreadid" << History::EventTypeText << (QStringList() << "someparticipant" << "anotherparticipant") << 0 << 0; } void ThreadTest::testCreateNewThread() { QFETCH(QString, accountId); QFETCH(QString, threadId); QFETCH(History::EventType, type); QFETCH(QStringList, participants); QFETCH(int, count); QFETCH(int, unreadCount); History::Event event; switch (type) { case History::EventTypeText: // the eventId doesn´t really matter here, just faking a random one to not use always the same event = History::TextEvent(accountId, threadId, QString("eventId%1").arg(QString::number(qrand() % 1024)), participants[0], QDateTime::currentDateTime(),QDateTime::currentDateTime().addSecs(-10), false, "Random Message", History::MessageTypeText); break; case History::EventTypeVoice: event = History::VoiceEvent(accountId, threadId, QString("eventId%1").arg(QString::number(qrand() % 1024)), participants[0], QDateTime::currentDateTime(), false, false, QTime(1,2,3)); break; case History::EventTypeNull: break; } History::Thread threadItem(accountId, threadId, type, participantsFromIdentifiers(accountId, participants), event.timestamp(), event, count, unreadCount); QCOMPARE(threadItem.accountId(), accountId); QCOMPARE(threadItem.threadId(), threadId); QCOMPARE(threadItem.type(), type); QCOMPARE(threadItem.participants().identifiers(), participants); QCOMPARE(threadItem.timestamp(), event.timestamp()); QCOMPARE(threadItem.lastEvent(), event); QCOMPARE(threadItem.count(), count); QCOMPARE(threadItem.unreadCount(), unreadCount); QVERIFY(threadItem.lastEvent() == event); } void ThreadTest::testFromProperties_data() { QTest::addColumn("accountId"); QTest::addColumn("threadId"); QTest::addColumn("type"); QTest::addColumn("participants"); QTest::addColumn("count"); QTest::addColumn("unreadCount"); QTest::newRow("voice thread with count, unread count and one participant") << "someaccountid" << "somethreadid" << History::EventTypeVoice << (QStringList() << "someparticipant") << 10 << 4; QTest::newRow("voice thread with zero messages and two participants") << "anotheraccountid" << "anotherthreadid" << History::EventTypeVoice << (QStringList() << "someparticipant" << "anotherparticipant") << 0 << 0; QTest::newRow("text thread with count, unread count and one participant") << "somevoiceaccountid" << "somevoicethreadid" << History::EventTypeText << (QStringList() << "somevoiceparticipant") << 10 << 4; QTest::newRow("voice thread with zero messages and two participants") << "anothervoiceaccountid" << "anothervoicethreadid" << History::EventTypeText << (QStringList() << "someparticipant" << "anotherparticipant") << 0 << 0; } void ThreadTest::testFromProperties() { QFETCH(QString, accountId); QFETCH(QString, threadId); QFETCH(History::EventType, type); QFETCH(QStringList, participants); QFETCH(int, count); QFETCH(int, unreadCount); History::Event event; switch (type) { case History::EventTypeText: // the eventId doesn´t really matter here, just faking a random one to not use always the same event = History::TextEvent(accountId, threadId, QString("eventId%1").arg(QString::number(qrand() % 1024)), participants[0], QDateTime::currentDateTime(), QDateTime::currentDateTime().addSecs(-10), false, "Random Message", History::MessageTypeText); break; case History::EventTypeVoice: event = History::VoiceEvent(accountId, threadId, QString("eventId%1").arg(QString::number(qrand() % 1024)), participants[0], QDateTime::currentDateTime(), false, false, QTime(1,2,3)); break; case History::EventTypeNull: break; } QVariantMap properties = event.properties(); properties[History::FieldAccountId] = accountId; properties[History::FieldThreadId] = threadId; properties[History::FieldType] = (int) type; properties[History::FieldParticipants] = participantsFromIdentifiers(accountId, participants).toVariantList(); properties[History::FieldCount] = count; properties[History::FieldUnreadCount] = unreadCount; History::Thread thread = History::Thread::fromProperties(properties); QCOMPARE(thread.accountId(), accountId); QCOMPARE(thread.threadId(), threadId); QCOMPARE(thread.type(), type); QCOMPARE(thread.participants().identifiers(), participants); QCOMPARE(thread.timestamp(), event.timestamp()); QCOMPARE(thread.count(), count); QCOMPARE(thread.unreadCount(), unreadCount); QVERIFY(thread.lastEvent() == event); } void ThreadTest::testFromNullProperties() { History::Thread thread = History::Thread::fromProperties(QVariantMap()); QVERIFY(thread.isNull()); } void ThreadTest::testProperties_data() { QTest::addColumn("accountId"); QTest::addColumn("threadId"); QTest::addColumn("type"); QTest::addColumn("participants"); QTest::addColumn("count"); QTest::addColumn("unreadCount"); QTest::newRow("voice thread with count, unread count and one participant") << "someaccountid" << "somethreadid" << History::EventTypeVoice << (QStringList() << "someparticipant") << 10 << 4; QTest::newRow("voice thread with zero messages and two participants") << "anotheraccountid" << "anotherthreadid" << History::EventTypeVoice << (QStringList() << "someparticipant" << "anotherparticipant") << 0 << 0; QTest::newRow("text thread with count, unread count and one participant") << "somevoiceaccountid" << "somevoicethreadid" << History::EventTypeText << (QStringList() << "somevoiceparticipant") << 10 << 4; QTest::newRow("voice thread with zero messages and two participants") << "anothervoiceaccountid" << "anothervoicethreadid" << History::EventTypeText << (QStringList() << "someparticipant" << "anotherparticipant") << 0 << 0; } void ThreadTest::testProperties() { QFETCH(QString, accountId); QFETCH(QString, threadId); QFETCH(History::EventType, type); QFETCH(QStringList, participants); QFETCH(int, count); QFETCH(int, unreadCount); History::Event event; switch (type) { case History::EventTypeText: // the eventId doesn´t really matter here, just faking a random one to not use always the same event = History::TextEvent(accountId, threadId, QString("eventId%1").arg(QString::number(qrand() % 1024)), participants[0], QDateTime::currentDateTime(), QDateTime::currentDateTime().addSecs(-10), false, "Random Message", History::MessageTypeText); break; case History::EventTypeVoice: event = History::VoiceEvent(accountId, threadId, QString("eventId%1").arg(QString::number(qrand() % 1024)), participants[0], QDateTime::currentDateTime(), false, false, QTime(1,2,3)); break; case History::EventTypeNull: break; } History::Thread threadItem(accountId, threadId, type, participantsFromIdentifiers(accountId, participants), event.timestamp(), event, count, unreadCount); QVariantMap properties = threadItem.properties(); QCOMPARE(properties[History::FieldAccountId].toString(), accountId); QCOMPARE(properties[History::FieldThreadId].toString(), threadId); QCOMPARE(properties[History::FieldType].toInt(), (int)type); QCOMPARE(History::Participants::fromVariantList(properties[History::FieldParticipants].toList()).identifiers(), participants); QCOMPARE(properties[History::FieldCount].toInt(), count); QCOMPARE(properties[History::FieldUnreadCount].toInt(), unreadCount); } void ThreadTest::testIsNull_data() { QTest::addColumn("thread"); QTest::addColumn("isNull"); History::Participants participants; participants << History::Participant("AccountId","Foo") << History::Participant("AccountId","Bar"); QTest::newRow("empty thread") << History::Thread() << true; QTest::newRow("empty accountId") << History::Thread(QString(), "threadId" , History::EventTypeText, participants) << false; QTest::newRow("empty threadId") << History::Thread("AccountId", QString(), History::EventTypeVoice, participants) << false; QTest::newRow("empty participants") << History::Thread("AccountId", "ThreadId", History::EventTypeText, History::Participants()) << false; QTest::newRow("construct empty thread") << History::Thread(QString(), QString(), History::EventTypeNull, History::Participants()) << true; } void ThreadTest::testIsNull() { QFETCH(History::Thread, thread); QFETCH(bool, isNull); QCOMPARE(thread.isNull(), isNull); } void ThreadTest::testEqualsOperator_data() { QTest::addColumn("firstAccountId"); QTest::addColumn("firstThreadId"); QTest::addColumn("firstType"); QTest::addColumn("secondAccountId"); QTest::addColumn("secondThreadId"); QTest::addColumn("secondType"); QTest::addColumn("result"); QTest::newRow("equal threads") << "theAccountId" << "theThreadId" << History::EventTypeText << "theAccountId" << "theThreadId" << History::EventTypeText << true; QTest::newRow("different types") << "oneAccountId" << "oneThreadId" << History::EventTypeVoice << "oneAccountId" << "oneThreadId" << History::EventTypeText << false; QTest::newRow("different account IDs") << "firstAccountId" << "theThreadId" << History::EventTypeVoice << "secondAccountId" << "theThreadId" << History::EventTypeVoice << false; QTest::newRow("different thread IDs") << "oneAccountId" << "firstThreadId" << History::EventTypeText << "oneAccountId" << "secondThreadId" << History::EventTypeText << false; } void ThreadTest::testEqualsOperator() { QFETCH(QString, firstAccountId); QFETCH(QString, firstThreadId); QFETCH(History::EventType, firstType); QFETCH(QString, secondAccountId); QFETCH(QString, secondThreadId); QFETCH(History::EventType, secondType); QFETCH(bool, result); History::Thread firstThread(firstAccountId, firstThreadId, firstType, History::Participants()); History::Thread secondThread(secondAccountId, secondThreadId, secondType, History::Participants()); QVERIFY((firstThread == secondThread) == result); } void ThreadTest::testCopyConstructor() { History::Thread thread("OneAccountId", "OneThreadId", History::EventTypeText, participantsFromIdentifiers("OneAccountId", QStringList() << "Foo" << "Bar")); History::Thread copy(thread); QVERIFY(thread == copy); } void ThreadTest::testAssignmentOperator() { History::Thread thread("OneAccountId", "OneThreadId", History::EventTypeText, participantsFromIdentifiers("OneAccountId", QStringList() << "Foo" << "Bar")); History::Thread other; other = thread; QVERIFY(other == thread); } History::Participants ThreadTest::participantsFromIdentifiers(const QString &accountId, const QStringList &identifiers) { History::Participants participants; Q_FOREACH(const QString &identifier, identifiers) { participants << History::Participant(accountId, identifier); } return participants; } QTEST_MAIN(ThreadTest) #include "ThreadTest.moc" history-service-0.5/tests/libhistoryservice/ThreadViewTest.cpp000066400000000000000000000122421455450246700250530ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 #include #include "manager.h" #include "thread.h" #include "threadview.h" #include "textevent.h" #include "unionfilter.h" #include "voiceevent.h" Q_DECLARE_METATYPE(History::EventType) Q_DECLARE_METATYPE(History::MatchFlags) Q_DECLARE_METATYPE(History::Threads) Q_DECLARE_METATYPE(History::Events) #define THREAD_COUNT 50 class ThreadViewTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void testNextPage(); void testFilter(); void testSort(); private: void populate(); }; void ThreadViewTest::initTestCase() { qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); populate(); } void ThreadViewTest::testNextPage() { // create a view to return all text threads and check that the right number of items get returned History::ThreadViewPtr view = History::Manager::instance()->queryThreads(History::EventTypeText); QVERIFY(view->isValid()); History::Threads threads = view->nextPage(); History::Threads allThreads; while (threads.count() > 0) { allThreads << threads; threads = view->nextPage(); } QCOMPARE(allThreads.count(), THREAD_COUNT); Q_FOREACH(const History::Thread &thread, threads) { QCOMPARE(thread.type(), History::EventTypeText); } } void ThreadViewTest::testFilter() { History::UnionFilter filter; filter.append(History::Filter(History::FieldAccountId, "account10")); filter.append(History::Filter(History::FieldAccountId, "account35")); History::ThreadViewPtr view = History::Manager::instance()->queryThreads(History::EventTypeVoice, History::Sort(History::FieldAccountId), filter); QVERIFY(view->isValid()); History::Threads threads = view->nextPage(); QCOMPARE(threads.count(), 2); QCOMPARE(threads.first().accountId(), QString("account10")); QCOMPARE(threads.first().type(), History::EventTypeVoice); QCOMPARE(threads.last().accountId(), QString("account35")); QCOMPARE(threads.last().type(), History::EventTypeVoice); // make sure no more items are returned QVERIFY(view->nextPage().isEmpty()); } void ThreadViewTest::testSort() { History::Sort ascendingSort(History::FieldAccountId, Qt::AscendingOrder); History::ThreadViewPtr view = History::Manager::instance()->queryThreads(History::EventTypeText, ascendingSort); QVERIFY(view->isValid()); History::Threads allThreads; History::Threads threads = view->nextPage(); while (!threads.isEmpty()) { allThreads << threads; threads = view->nextPage(); } QCOMPARE(allThreads.first().accountId(), QString("account00")); QCOMPARE(allThreads.last().accountId(), QString("account%1").arg(THREAD_COUNT-1)); History::Sort descendingSort(History::FieldAccountId, Qt::DescendingOrder); allThreads.clear(); view = History::Manager::instance()->queryThreads(History::EventTypeVoice, descendingSort); QVERIFY(view->isValid()); threads = view->nextPage(); while (!threads.isEmpty()) { allThreads << threads; threads = view->nextPage(); } QCOMPARE(allThreads.first().accountId(), QString("account%1").arg(THREAD_COUNT-1)); QCOMPARE(allThreads.last().accountId(), QString("account00")); } void ThreadViewTest::populate() { // create voice threads for (int i = 0; i < THREAD_COUNT; ++i) { History::Manager::instance()->threadForParticipants(QString("account%1").arg(i, 2, 10, QChar('0')), History::EventTypeVoice, QStringList() << QString("participant%1").arg(i, 2, 10, QChar('0')), History::MatchCaseSensitive, true); } // and the text threads for (int i = 0; i < THREAD_COUNT; ++i) { History::Manager::instance()->threadForParticipants(QString("account%1").arg(i, 2, 10, QChar('0')), History::EventTypeText, QStringList() << QString("participant%1").arg(i, 2, 10, QChar('0')), History::MatchCaseSensitive, true); } } QTEST_MAIN(ThreadViewTest) #include "ThreadViewTest.moc" history-service-0.5/tests/libhistoryservice/UnionFilterTest.cpp000066400000000000000000000226511455450246700252540ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 #include #include "unionfilter.h" Q_DECLARE_METATYPE(History::MatchFlags) Q_DECLARE_METATYPE(History::UnionFilter) class UnionFilterTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void testSetFilters(); void testAppendFilter(); void testPrependFilter(); void testClear(); void testMatch_data(); void testMatch(); void testToStringWithNoFilters(); void testToStringWithOneFilter(); void testToStringWithManyFilters(); void testConvertToFilterAndBack(); void testIsValid_data(); void testIsValid(); void testProperties(); void testFromProperties(); }; void UnionFilterTest::initTestCase() { qRegisterMetaType(); qRegisterMetaType(); } void UnionFilterTest::testSetFilters() { // create two filters and check that they are properly set History::Filter filterOne("propertyOne", "valueOne"); History::Filter filterTwo("propertyTwo", "valueTwo"); History::UnionFilter unionFilter; unionFilter.setFilters(QList() << filterOne << filterTwo); QCOMPARE(unionFilter.filters().count(), 2); QCOMPARE(unionFilter.filters()[0], filterOne); QCOMPARE(unionFilter.filters()[1], filterTwo); } void UnionFilterTest::testAppendFilter() { // create two filters and check that they are properly set History::Filter filterOne("propertyOne", "valueOne"); History::Filter filterTwo("propertyTwo", "valueTwo"); History::Filter filterThree("propertyThree", "valueThree"); History::UnionFilter unionFilter; unionFilter.setFilters(QList() << filterOne << filterTwo); unionFilter.append(filterThree); QCOMPARE(unionFilter.filters().count(), 3); QCOMPARE(unionFilter.filters()[2], filterThree); } void UnionFilterTest::testPrependFilter() { // create two filters and check that they are properly set History::Filter filterOne("propertyOne", "valueOne"); History::Filter filterTwo("propertyTwo", "valueTwo"); History::Filter filterThree("propertyThree", "valueThree"); History::UnionFilter unionFilter; unionFilter.setFilters(QList() << filterOne << filterTwo); unionFilter.prepend(filterThree); QCOMPARE(unionFilter.filters().count(), 3); QCOMPARE(unionFilter.filters()[0], filterThree); } void UnionFilterTest::testClear() { // create two filters and check that they are properly set History::Filter filterOne("propertyOne", "valueOne"); History::Filter filterTwo("propertyTwo", "valueTwo"); History::UnionFilter unionFilter; unionFilter.setFilters(QList() << filterOne << filterTwo); unionFilter.clear(); QVERIFY(unionFilter.filters().isEmpty()); } void UnionFilterTest::testMatch_data() { QTest::addColumn("filterProperties"); QTest::addColumn("itemProperties"); QTest::addColumn("result"); // FIXME: take into account the match flags QVariantMap filterProperties; QVariantMap itemProperties; filterProperties["stringProperty"] = QString("stringValue"); filterProperties["intProperty"] = 10; itemProperties = filterProperties; QTest::newRow("all matching values") << filterProperties << itemProperties << true; itemProperties["intProperty"] = 11; QTest::newRow("one of the values match") << filterProperties << itemProperties << true; itemProperties["stringProperty"] = QString("noMatch"); QTest::newRow("no match") << filterProperties << itemProperties << false; QTest::newRow("empty match") << QVariantMap() << itemProperties << true; } void UnionFilterTest::testMatch() { QFETCH(QVariantMap, filterProperties); QFETCH(QVariantMap, itemProperties); QFETCH(bool, result); QList filters; Q_FOREACH(const QString &key, filterProperties.keys()) { filters << History::Filter(key, filterProperties[key]); } History::UnionFilter unionFilter; unionFilter.setFilters(filters); QCOMPARE(unionFilter.match(itemProperties), result); } void UnionFilterTest::testToStringWithNoFilters() { History::UnionFilter filter; QVERIFY(filter.toString().isNull()); } void UnionFilterTest::testToStringWithOneFilter() { // test that with a single filter the result of toString() is equal to the output // of calling toString() on the filter directly History::Filter filter("aProperty", "aValue"); History::UnionFilter unionFilter; unionFilter.append(filter); QCOMPARE(unionFilter.toString(), filter.toString()); } void UnionFilterTest::testToStringWithManyFilters() { // check if all the individual filters are present in the toString() call History::Filter filterOne("propertyOne", "valueOne"); History::Filter filterTwo("propertyTwo", "valueTwo"); History::Filter filterThree("propertyThree", "valueThree"); History::UnionFilter unionFilter; unionFilter.setFilters(QList() << filterOne << filterTwo << filterThree); QString stringResult = unionFilter.toString(); QVERIFY(stringResult.contains(filterOne.toString())); QVERIFY(stringResult.contains(filterTwo.toString())); QVERIFY(stringResult.contains(filterThree.toString())); // check that the filter starts with two parenthesis (one for the block and one for the first filter // and that it also ends with two parentesis QVERIFY(stringResult.startsWith("((")); QVERIFY(stringResult.endsWith("))")); } void UnionFilterTest::testConvertToFilterAndBack() { History::Filter filterOne("propertyOne", "valueOne"); History::Filter filterTwo("propertyTwo", "valueTwo"); History::Filter filterThree("propertyThree", "valueThree"); History::UnionFilter unionFilter; unionFilter.setFilters(QList() << filterOne << filterTwo << filterThree); History::Filter castFilter = unionFilter; QCOMPARE(castFilter.toString(), unionFilter.toString()); QCOMPARE(castFilter.type(), History::FilterTypeUnion); History::UnionFilter andBack = castFilter; QCOMPARE(andBack, unionFilter); QCOMPARE(andBack.toString(), unionFilter.toString()); } void UnionFilterTest::testIsValid_data() { QTest::addColumn("filter"); QTest::addColumn("isValid"); History::UnionFilter filter; QTest::newRow("invalid filter") << filter << false; filter.append(History::Filter()); QTest::newRow("valid filter") << filter << true; } void UnionFilterTest::testIsValid() { QFETCH(History::UnionFilter, filter); QFETCH(bool, isValid); QCOMPARE(filter.isValid(), isValid); } void UnionFilterTest::testProperties() { History::Filter filterOne("propertyOne", "valueOne"); History::Filter filterTwo("propertyTwo", "valueTwo"); History::Filter filterThree("propertyThree", "valueThree"); History::UnionFilter unionFilter; unionFilter.setFilters(QList() << filterOne << filterTwo << filterThree); QVariantMap properties = unionFilter.properties(); QVERIFY(!properties.isEmpty()); QVERIFY(properties.contains(History::FieldFilters)); QCOMPARE(properties[History::FieldFilterType].toInt(), (int) History::FilterTypeUnion); QVariantList filters = properties[History::FieldFilters].toList(); QCOMPARE(filters.count(), unionFilter.filters().count()); QVariantMap propsOne = filters[0].toMap(); QCOMPARE(propsOne, filterOne.properties()); QVariantMap propsTwo = filters[1].toMap(); QCOMPARE(propsTwo, filterTwo.properties()); QVariantMap propsThree = filters[2].toMap(); QCOMPARE(propsThree, filterThree.properties()); // check that a null filter returns an empty QVariantMap History::UnionFilter nullFilter; QVERIFY(nullFilter.properties().isEmpty()); } void UnionFilterTest::testFromProperties() { QVariantMap properties; // check that a null filter is returned History::Filter nullFilter = History::UnionFilter::fromProperties(properties); QVERIFY(nullFilter.isNull()); properties[History::FieldFilterType] = (int)History::FilterTypeUnion; QVariantList filters; for (int i = 0; i < 3; ++i) { History::Filter filter(QString("filter%1").arg(QString::number(i)), QString("value%1").arg(QString::number(i)), History::MatchCaseInsensitive); filters.append(filter.properties()); } properties[History::FieldFilters] = filters; History::Filter filter = History::UnionFilter::fromProperties(properties); QCOMPARE(filter.type(), History::FilterTypeUnion); History::UnionFilter unionFilter = filter; QCOMPARE(unionFilter.filters().count(), filters.count()); for (int i = 0; i < filters.count(); ++i) { QCOMPARE(unionFilter.filters()[i].properties(), filters[i].toMap()); } } QTEST_MAIN(UnionFilterTest) #include "UnionFilterTest.moc" history-service-0.5/tests/libhistoryservice/VoiceEventTest.cpp000066400000000000000000000261061455450246700250640ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 #include #include "voiceevent.h" class VoiceEventTest : public QObject { Q_OBJECT private Q_SLOTS: void testCreateNewEvent_data(); void testCreateNewEvent(); void testCastToEventAndBack(); void testFromProperties_data(); void testFromProperties(); void testFromNullProperties(); void testProperties_data(); void testProperties(); private: History::Participants participantsFromIdentifiers(const QString &accountId, const QStringList &identifiers); }; void VoiceEventTest::testCreateNewEvent_data() { QTest::addColumn("accountId"); QTest::addColumn("threadId"); QTest::addColumn("eventId"); QTest::addColumn("senderId"); QTest::addColumn("timestamp"); QTest::addColumn("newEvent"); QTest::addColumn("missed"); QTest::addColumn("duration"); QTest::addColumn("remoteParticipant"); QTest::addColumn("participants"); QTest::newRow("unread missed call") << "testAccountId" << "testThreadId" << "testEventId" << "testSenderId" << QDateTime::currentDateTime().addDays(-10) << true << true << QTime(0, 0, 0) << QString("remoteParticipant") << (QStringList() << "testSenderId"); QTest::newRow("missed call") << "testAccountId2" << "testThreadId2" << "testEventId2" << "testSenderId2" << QDateTime::currentDateTime().addDays(-5) << false << true << QTime(0, 0, 0) << QString("remoteParticipant2") << (QStringList() << "testSenderId2"); QTest::newRow("not missed call") << "testAccountId" << "testThreadId" << "testEventId" << "testSenderId" << QDateTime::currentDateTime().addDays(-10) << false << false << QTime(1, 2, 3) << QString("remoteParticipant") << (QStringList() << "testSenderId"); } void VoiceEventTest::testCreateNewEvent() { QFETCH(QString, accountId); QFETCH(QString, threadId); QFETCH(QString, eventId); QFETCH(QString, senderId); QFETCH(QDateTime, timestamp); QFETCH(bool, newEvent); QFETCH(bool, missed); QFETCH(QTime, duration); QFETCH(QString, remoteParticipant); QFETCH(QStringList, participants); History::VoiceEvent event(accountId, threadId, eventId, senderId, timestamp, newEvent, missed, duration, remoteParticipant, participantsFromIdentifiers(accountId, participants)); // check that the values are properly set QCOMPARE(event.accountId(), accountId); QCOMPARE(event.threadId(), threadId); QCOMPARE(event.eventId(), eventId); QCOMPARE(event.senderId(), senderId); QCOMPARE(event.timestamp(), timestamp); QCOMPARE(event.newEvent(), newEvent); QCOMPARE(event.missed(), missed); QCOMPARE(event.duration(), duration); QCOMPARE(event.remoteParticipant(), remoteParticipant); QCOMPARE(event.participants().identifiers(), participants); } void VoiceEventTest::testCastToEventAndBack() { QString accountId("oneAccountId"); History::VoiceEvent voiceEvent(accountId, "oneThreadId", "oneEventId", "oneSender", QDateTime::currentDateTime(), true, true, QTime(1,2,3), "remoteParticipant", participantsFromIdentifiers(accountId, QStringList() << "oneParticipant")); // test the copy constructor History::Event historyEvent(voiceEvent); QVERIFY(historyEvent == voiceEvent); History::VoiceEvent castBack(historyEvent); QVERIFY(castBack == voiceEvent); // and now the assignment operator History::Event anotherEvent; anotherEvent = voiceEvent; QVERIFY(anotherEvent == voiceEvent); History::VoiceEvent backAgain; backAgain = anotherEvent; QVERIFY(backAgain == voiceEvent); } void VoiceEventTest::testFromProperties_data() { QTest::addColumn("accountId"); QTest::addColumn("threadId"); QTest::addColumn("eventId"); QTest::addColumn("senderId"); QTest::addColumn("timestamp"); QTest::addColumn("newEvent"); QTest::addColumn("missed"); QTest::addColumn("duration"); QTest::addColumn("participants"); QTest::newRow("unread missed call") << "testAccountId" << "testThreadId" << "testEventId" << "testSenderId" << QDateTime::currentDateTime().addDays(-10) << true << true << QTime(0, 0, 0) << (QStringList() << "testParticipant"); QTest::newRow("missed call") << "testAccountId2" << "testThreadId2" << "testEventId2" << "testSenderId2" << QDateTime::currentDateTime().addDays(-5) << false << true << QTime(0, 0, 0) << (QStringList() << "testParticipant2"); QTest::newRow("not missed call") << "testAccountId" << "testThreadId" << "testEventId" << "testSenderId" << QDateTime::currentDateTime().addDays(-10) << false << false << QTime(1, 2, 3) << (QStringList() << "testParticipant"); } void VoiceEventTest::testFromProperties() { QFETCH(QString, accountId); QFETCH(QString, threadId); QFETCH(QString, eventId); QFETCH(QString, senderId); QFETCH(QDateTime, timestamp); QFETCH(bool, newEvent); QFETCH(bool, missed); QFETCH(QTime, duration); QFETCH(QStringList, participants); QVariantMap properties; properties[History::FieldAccountId] = accountId; properties[History::FieldThreadId] = threadId; properties[History::FieldEventId] = eventId; properties[History::FieldSenderId] = senderId; properties[History::FieldTimestamp] = timestamp.toString(Qt::ISODate); properties[History::FieldNewEvent] = newEvent; properties[History::FieldMissed] = missed; properties[History::FieldDuration] = QTime(0,0,0,0).secsTo(duration); properties[History::FieldParticipants] = participantsFromIdentifiers(accountId, participants).toVariantList(); History::VoiceEvent voiceEvent = History::VoiceEvent::fromProperties(properties); QCOMPARE(voiceEvent.accountId(), accountId); QCOMPARE(voiceEvent.threadId(), threadId); QCOMPARE(voiceEvent.eventId(), eventId); QCOMPARE(voiceEvent.senderId(), senderId); QCOMPARE(voiceEvent.timestamp().toString(Qt::ISODate), timestamp.toString(Qt::ISODate)); QCOMPARE(voiceEvent.newEvent(), newEvent); QCOMPARE(voiceEvent.missed(), missed); QCOMPARE(voiceEvent.duration(), duration); QCOMPARE(voiceEvent.participants().identifiers(), participants); } void VoiceEventTest::testFromNullProperties() { // just to make sure, test that calling ::fromProperties() on an empty map returns a null event History::Event nullEvent = History::VoiceEvent::fromProperties(QVariantMap()); QVERIFY(nullEvent.isNull()); QCOMPARE(nullEvent.type(), History::EventTypeNull); } void VoiceEventTest::testProperties_data() { QTest::addColumn("accountId"); QTest::addColumn("threadId"); QTest::addColumn("eventId"); QTest::addColumn("senderId"); QTest::addColumn("timestamp"); QTest::addColumn("newEvent"); QTest::addColumn("missed"); QTest::addColumn("duration"); QTest::addColumn("remoteParticipant"); QTest::addColumn("participants"); QTest::newRow("unread missed call") << "testAccountId" << "testThreadId" << "testEventId" << "testSenderId" << QDateTime::currentDateTime().addDays(-10) << true << true << QTime(0, 0, 0) << QString("remoteParticipant") << (QStringList() << "testParticipant"); QTest::newRow("missed call") << "testAccountId2" << "testThreadId2" << "testEventId2" << "testSenderId2" << QDateTime::currentDateTime().addDays(-5) << false << true << QTime(0, 0, 0) << QString("remoteParticipant2") << (QStringList() << "testParticipant2"); QTest::newRow("not missed call") << "testAccountId" << "testThreadId" << "testEventId" << "testSenderId" << QDateTime::currentDateTime().addDays(-10) << false << false << QTime(1, 2, 3) << QString("remoteParticipant3") << (QStringList() << "testParticipant3"); } void VoiceEventTest::testProperties() { QFETCH(QString, accountId); QFETCH(QString, threadId); QFETCH(QString, eventId); QFETCH(QString, senderId); QFETCH(QDateTime, timestamp); QFETCH(bool, newEvent); QFETCH(bool, missed); QFETCH(QTime, duration); QFETCH(QString, remoteParticipant); QFETCH(QStringList, participants); History::VoiceEvent event(accountId, threadId, eventId, senderId, timestamp, newEvent, missed, duration, remoteParticipant, participantsFromIdentifiers(accountId, participants)); // check that the values are properly set QVariantMap properties = event.properties(); QCOMPARE(properties[History::FieldAccountId].toString(), accountId); QCOMPARE(properties[History::FieldThreadId].toString(), threadId); QCOMPARE(properties[History::FieldEventId].toString(), eventId); QCOMPARE(properties[History::FieldSenderId].toString(), senderId); QCOMPARE(properties[History::FieldTimestamp].toString(), timestamp.toString("yyyy-MM-ddTHH:mm:ss.zzz")); QCOMPARE(properties[History::FieldNewEvent].toBool(), newEvent); QCOMPARE(properties[History::FieldMissed].toBool(), missed); QCOMPARE(QTime(0,0).addSecs(properties[History::FieldDuration].toInt()), duration); QCOMPARE(properties[History::FieldRemoteParticipant].toString(), remoteParticipant); QCOMPARE(History::Participants::fromVariant(properties[History::FieldParticipants]).identifiers(), participants); } History::Participants VoiceEventTest::participantsFromIdentifiers(const QString &accountId, const QStringList &identifiers) { History::Participants participants; Q_FOREACH(const QString &identifier, identifiers) { participants << History::Participant(accountId, identifier); } return participants; } QTEST_MAIN(VoiceEventTest) #include "VoiceEventTest.moc" history-service-0.5/tests/plugins/000077500000000000000000000000001455450246700173545ustar00rootroot00000000000000history-service-0.5/tests/plugins/CMakeLists.txt000066400000000000000000000000311455450246700221060ustar00rootroot00000000000000add_subdirectory(sqlite) history-service-0.5/tests/plugins/sqlite/000077500000000000000000000000001455450246700206555ustar00rootroot00000000000000history-service-0.5/tests/plugins/sqlite/CMakeLists.txt000066400000000000000000000010501455450246700234110ustar00rootroot00000000000000include_directories( ${CMAKE_SOURCE_DIR}/plugins/sqlite ${CMAKE_SOURCE_DIR}/src ${CMAKE_CURRENT_BINARY_DIR} ) generate_test(SqlitePluginTest SOURCES SqlitePluginTest.cpp LIBRARIES historyservice sqlitehistoryplugin QT5_MODULES Core DBus Test Sql) generate_test(SqliteThreadViewTest SOURCES SqliteThreadViewTest.cpp LIBRARIES historyservice sqlitehistoryplugin QT5_MODULES Core DBus Test Sql) generate_test(SqliteEventViewTest SOURCES SqliteEventViewTest.cpp LIBRARIES historyservice sqlitehistoryplugin QT5_MODULES Core DBus Test Sql) history-service-0.5/tests/plugins/sqlite/SqliteEventViewTest.cpp000066400000000000000000000303721455450246700253240ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 #include #include "sqlitehistoryplugin.h" #include "sqlitedatabase.h" #include "sqlitehistoryeventview.h" #include "textevent.h" #include "voiceevent.h" #include "intersectionfilter.h" Q_DECLARE_METATYPE(History::EventType) Q_DECLARE_METATYPE(History::MatchFlags) #define EVENT_COUNT 50 class SqliteEventViewTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void testNextPage(); void testFilter(); void testSort(); void testSortWithMultipleFields(); void testFilterWithValueToExclude(); void testFilterWithFilterLessThan(); void testFilterWithFilterGreaterThan(); void testFilterWithFilterGreaterOrEqualsThan(); void testFilterWithFilterLessOrEqualsThan(); private: SQLiteHistoryPlugin *mPlugin; void populateDatabase(); }; void SqliteEventViewTest::initTestCase() { qRegisterMetaType(); qRegisterMetaType(); qputenv("HISTORY_SQLITE_DBPATH", ":memory:"); mPlugin = new SQLiteHistoryPlugin(this); populateDatabase(); } void SqliteEventViewTest::testNextPage() { // create a view to return all text threads and check that the right number of items get returned History::PluginEventView *view = mPlugin->queryEvents(History::EventTypeText); QVERIFY(view->IsValid()); QList events = view->NextPage(); QList allEvents; while (events.count() > 0) { allEvents << events; events = view->NextPage(); } QCOMPARE(allEvents.count(), EVENT_COUNT * 2); Q_FOREACH(const QVariantMap &event, events) { QCOMPARE(event[History::FieldType].toInt(), (int) History::EventTypeText); } delete view; } void SqliteEventViewTest::testFilter() { History::IntersectionFilter filter; filter.append(History::Filter(History::FieldAccountId, "account0")); filter.append(History::Filter(History::FieldThreadId, "participant0")); filter.append(History::Filter(History::FieldEventId, "event21")); History::PluginEventView *view = mPlugin->queryEvents(History::EventTypeVoice, History::Sort(History::FieldAccountId), filter); QVERIFY(view->IsValid()); QList events = view->NextPage(); QCOMPARE(events.count(), 1); QVariantMap event = events.first(); QCOMPARE(event[History::FieldAccountId].toString(), QString("account0")); QCOMPARE(event[History::FieldType].toInt(), (int) History::EventTypeVoice); QCOMPARE(event[History::FieldThreadId].toString(), QString("participant0")); QCOMPARE(event[History::FieldEventId].toString(), QString("event21")); // make sure no more items are returned QVERIFY(view->NextPage().isEmpty()); delete view; } void SqliteEventViewTest::testFilterWithValueToExclude() { History::Sort ascendingSort(History::FieldEventId, Qt::AscendingOrder); History::IntersectionFilter filter; filter.append(History::Filter(History::FieldAccountId, "account0")); filter.append(History::Filter(History::FieldMessageStatus, History::MessageStatusTemporarilyFailed, History::MatchNotEquals)); History::PluginEventView *view = mPlugin->queryEvents(History::EventTypeText, ascendingSort, filter); QVERIFY(view->IsValid()); QList events = view->NextPage(); while (!events.isEmpty()) { for (const auto& event : events) { QVERIFY(event[History::FieldMessageStatus].toInt() != (int) History::MessageStatusTemporarilyFailed); } events = view->NextPage(); } delete view; } void SqliteEventViewTest::testFilterWithFilterLessThan() { History::Sort ascendingSort(History::FieldEventId, Qt::AscendingOrder); History::IntersectionFilter filter; QDateTime queryTime = QDateTime::currentDateTime().addDays(10); filter.append(History::Filter(History::FieldAccountId, "account0")); filter.append(History::Filter(History::FieldTimestamp, queryTime, History::MatchLess)); History::PluginEventView *view = mPlugin->queryEvents(History::EventTypeText, ascendingSort, filter); QVERIFY(view->IsValid()); QList events = view->NextPage(); while (!events.isEmpty()) { for (const auto& event : events) { QVERIFY(event[History::FieldTimestamp].toDateTime() < queryTime); } events = view->NextPage(); } delete view; } void SqliteEventViewTest::testFilterWithFilterGreaterThan() { History::Sort ascendingSort(History::FieldEventId, Qt::AscendingOrder); History::IntersectionFilter filter; QDateTime queryTime = QDateTime::currentDateTime().addDays(10); filter.append(History::Filter(History::FieldAccountId, "account0")); filter.append(History::Filter(History::FieldTimestamp, queryTime, History::MatchGreater)); History::PluginEventView *view = mPlugin->queryEvents(History::EventTypeText, ascendingSort, filter); QVERIFY(view->IsValid()); QList events = view->NextPage(); while (!events.isEmpty()) { for (const auto& event : events) { QVERIFY(event[History::FieldTimestamp].toDateTime() > queryTime); } events = view->NextPage(); } delete view; } void SqliteEventViewTest::testFilterWithFilterGreaterOrEqualsThan() { History::Sort ascendingSort(History::FieldEventId, Qt::AscendingOrder); History::IntersectionFilter filter; QDateTime queryTime = QDateTime::currentDateTime().addDays(10); filter.append(History::Filter(History::FieldAccountId, "account0")); filter.append(History::Filter(History::FieldTimestamp, queryTime, History::MatchGreaterOrEquals)); History::PluginEventView *view = mPlugin->queryEvents(History::EventTypeText, ascendingSort, filter); QVERIFY(view->IsValid()); QList events = view->NextPage(); while (!events.isEmpty()) { for (const auto& event : events) { QVERIFY(event[History::FieldTimestamp].toDateTime() >= queryTime); } events = view->NextPage(); } delete view; } void SqliteEventViewTest::testFilterWithFilterLessOrEqualsThan() { History::Sort ascendingSort(History::FieldEventId, Qt::AscendingOrder); History::IntersectionFilter filter; QDateTime queryTime = QDateTime::currentDateTime().addDays(10); filter.append(History::Filter(History::FieldAccountId, "account0")); filter.append(History::Filter(History::FieldTimestamp, queryTime, History::MatchLessOrEquals)); History::PluginEventView *view = mPlugin->queryEvents(History::EventTypeText, ascendingSort, filter); QVERIFY(view->IsValid()); QList events = view->NextPage(); while (!events.isEmpty()) { for (const auto& event : events) { QVERIFY(event[History::FieldTimestamp].toDateTime() <= queryTime); } events = view->NextPage(); } delete view; } void SqliteEventViewTest::testSort() { History::Sort ascendingSort(History::FieldEventId, Qt::AscendingOrder); History::PluginEventView *view = mPlugin->queryEvents(History::EventTypeText, ascendingSort); QVERIFY(view->IsValid()); QList allEvents; QList events = view->NextPage(); while (!events.isEmpty()) { allEvents << events; events = view->NextPage(); } QCOMPARE(allEvents.first()[History::FieldEventId].toString(), QString("event00")); QCOMPARE(allEvents.last()[History::FieldEventId].toString(), QString("event%1").arg(EVENT_COUNT-1)); delete view; History::Sort descendingSort(History::FieldEventId, Qt::DescendingOrder); allEvents.clear(); view = mPlugin->queryEvents(History::EventTypeVoice, descendingSort); QVERIFY(view->IsValid()); events = view->NextPage(); while (!events.isEmpty()) { allEvents << events; events = view->NextPage(); } QCOMPARE(allEvents.first()[History::FieldEventId].toString(), QString("event%1").arg(EVENT_COUNT-1)); QCOMPARE(allEvents.last()[History::FieldEventId].toString(), QString("event00")); delete view; } void SqliteEventViewTest::testSortWithMultipleFields() { History::Sort ascendingSort(QString("%1, %2").arg(History::FieldAccountId).arg(History::FieldEventId), Qt::AscendingOrder); //History::Sort ascendingSort(QString("%1").arg(History::FieldEventId), Qt::AscendingOrder); History::PluginEventView *view = mPlugin->queryEvents(History::EventTypeText, ascendingSort); QVERIFY(view->IsValid()); QList allEvents; QList events = view->NextPage(); while (!events.isEmpty()) { allEvents << events; events = view->NextPage(); } QCOMPARE(allEvents[0][History::FieldEventId].toString(), QString("event00")); QCOMPARE(allEvents[0][History::FieldAccountId].toString(), QString("account0")); QCOMPARE(allEvents[1][History::FieldEventId].toString(), QString("event01")); QCOMPARE(allEvents[1][History::FieldAccountId].toString(), QString("account0")); delete view; } void SqliteEventViewTest::populateDatabase() { mPlugin->beginBatchOperation(); QDateTime currentDate = QDateTime::currentDateTime(); // create two threads of each type for (int i = 1; i >= 0; --i) { QVariantMap voiceThread = mPlugin->createThreadForParticipants(QString("account%1").arg(i), History::EventTypeVoice, QStringList() << QString("participant%1").arg(i)); // now create some events for this thread for (int j = 0; j < EVENT_COUNT; ++j) { History::VoiceEvent voiceEvent(voiceThread[History::FieldAccountId].toString(), voiceThread[History::FieldThreadId].toString(), QString("event%1").arg(j, 2, 10, QChar('0')), j % 2 ? "self" : QString("participant%1").arg(i), currentDate.addDays(j), j % 2, j % 2, j % 2 ? QTime(i, j, 0) : QTime()); QCOMPARE(mPlugin->writeVoiceEvent(voiceEvent.properties()), History::EventWriteCreated); } QVariantMap textThread = mPlugin->createThreadForParticipants(QString("account%1").arg(i), History::EventTypeText, QStringList() << QString("participant%1").arg(i)); for (int j = 0; j < EVENT_COUNT; ++j) { History::TextEvent textEvent(textThread[History::FieldAccountId].toString(), textThread[History::FieldThreadId].toString(), QString("event%1").arg(j, 2, 10, QChar('0')), j % 2 ? "self" : QString("participant%1").arg(i), currentDate.addDays(j), currentDate.addDays(j).addSecs(-10), j % 2, QString("Hello %1").arg(j), History::MessageTypeText, j % 2 ? History::MessageStatusDelivered : History::MessageStatusTemporarilyFailed); QCOMPARE(mPlugin->writeTextEvent(textEvent.properties()), History::EventWriteCreated); } } mPlugin->endBatchOperation(); } QTEST_MAIN(SqliteEventViewTest) #include "SqliteEventViewTest.moc" history-service-0.5/tests/plugins/sqlite/SqlitePluginTest.cpp000066400000000000000000001506231455450246700246500ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 #include #include "sqlitehistoryplugin.h" #include "sqlitedatabase.h" #include "sqlitehistorythreadview.h" #include "sqlitehistoryeventview.h" #include "textevent.h" #include "texteventattachment.h" #include "voiceevent.h" #include "intersectionfilter.h" #include "unionfilter.h" Q_DECLARE_METATYPE(History::EventType) Q_DECLARE_METATYPE(History::MatchFlags) class SqlitePluginTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void testCreateThread_data(); void testCreateThread(); void testThreadForParticipants_data(); void testThreadForParticipants(); void testEmptyThreadForParticipants(); void testGetSingleThread(); void testGetFilePaths(); void testRemoveThread(); void testBatchOperation(); void testRollback(); void testQueryThreads(); void testQueryEvents(); void testWriteTextEvent_data(); void testWriteTextEvent(); void testModifyTextEvent(); void testRemoveTextEvent(); void testWriteVoiceEvent_data(); void testWriteVoiceEvent(); void testModifyVoiceEvent(); void testRemoveVoiceEvent(); void testRemoveEvents(); void testEventsForThread(); void testGetSingleEvent_data(); void testGetSingleEvent(); void testFilterToString_data(); void testFilterToString(); void testEscapeFilterValue_data(); void testEscapeFilterValue(); void testUnreadCount(); void testMigrateAccountIds(); private: SQLiteHistoryPlugin *mPlugin; }; void SqlitePluginTest::initTestCase() { qRegisterMetaType(); qRegisterMetaType(); qputenv("HISTORY_SQLITE_DBPATH", ":memory:"); mPlugin = new SQLiteHistoryPlugin(this); } void SqlitePluginTest::testCreateThread_data() { QTest::addColumn("accountId"); QTest::addColumn("eventType"); QTest::addColumn("participants"); QTest::newRow("voice thread with one participant") << "oneAccount" << History::EventTypeVoice << (QStringList() << "oneParticipant"); QTest::newRow("text thread with multiple participants") << "oneAccount" << History::EventTypeText << (QStringList() << "first" << "second"); } void SqlitePluginTest::testCreateThread() { QFETCH(QString, accountId); QFETCH(History::EventType, eventType); QFETCH(QStringList, participants); SQLiteDatabase::instance()->reopen(); QVariantMap thread = mPlugin->createThreadForParticipants(accountId, eventType, participants); // check that the variant map is properly filled QCOMPARE(thread[History::FieldAccountId].toString(), accountId); QCOMPARE(thread[History::FieldType].toInt(), (int) eventType); QCOMPARE(History::Participants::fromVariantList(thread[History::FieldParticipants].toList()).identifiers(), participants); QVERIFY(!thread[History::FieldThreadId].toString().isEmpty()); // now check that the thread is properly saved in the database QSqlQuery query(SQLiteDatabase::instance()->database()); query.prepare("SELECT * FROM threads WHERE accountId=:accountId AND type=:type"); query.bindValue(":accountId", accountId); query.bindValue(":type", (int) eventType); QVERIFY(query.exec()); int count = 0; while (query.next()) { count++; QCOMPARE(query.value("accountId"), thread[History::FieldAccountId]); QCOMPARE(query.value("threadId"), thread[History::FieldThreadId]); QCOMPARE(query.value("type"), thread[History::FieldType]); QCOMPARE(query.value("count").toInt(), 0); QCOMPARE(query.value("unreadCount").toInt(), 0); } QCOMPARE(count, 1); // and make sure all the participants are saved correctly query.prepare("SELECT * FROM thread_participants"); QVERIFY(query.exec()); count = 0; while (query.next()) { count++; QCOMPARE(query.value("accountId"), thread[History::FieldAccountId]); QCOMPARE(query.value("threadId"), thread[History::FieldThreadId]); QCOMPARE(query.value("type"), thread[History::FieldType]); QVERIFY(participants.contains(query.value("participantId").toString())); } QCOMPARE(count, participants.count()); } void SqlitePluginTest::testThreadForParticipants_data() { QTest::addColumn("accountId"); QTest::addColumn("eventType"); QTest::addColumn("participants"); QTest::addColumn("matchFlags"); QTest::addColumn("participantsToMatch"); QTest::newRow("voice thread with one participant") << "oneAccount" << History::EventTypeVoice << (QStringList() << "oneParticipant") << History::MatchFlags(History::MatchCaseSensitive) << (QStringList() << "oneParticipant"); QTest::newRow("text thread with multiple participants") << "oneAccount" << History::EventTypeText << (QStringList() << "first" << "second" << "third") << History::MatchFlags(History::MatchCaseSensitive) << (QStringList() << "second" << "first" << "third"); QTest::newRow("phone number match with one participant") << "thePhoneAccount" << History::EventTypeVoice << (QStringList() << "+12345678901") << History::MatchFlags(History::MatchPhoneNumber) << (QStringList() << "2345678901"); QTest::newRow("phone number match with multiple participants") << "phoneAccount" << History::EventTypeText << (QStringList() << "12345678" << "+19999999999") << History::MatchFlags(History::MatchPhoneNumber) << (QStringList() << "+554112345678" << "9999999"); } void SqlitePluginTest::testThreadForParticipants() { QFETCH(QString, accountId); QFETCH(History::EventType, eventType); QFETCH(QStringList, participants); QFETCH(History::MatchFlags, matchFlags); QFETCH(QStringList, participantsToMatch); SQLiteDatabase::instance()->reopen(); QVariantMap thread = mPlugin->createThreadForParticipants(accountId, eventType, participants); // there is no need to check the results of createThreadForParticipants, they are tested in another function // just check if the resulting thread is not empty QVERIFY(!thread.isEmpty()); // now try to fetch the thread for the given participants QVariantMap retrievedThread = mPlugin->threadForParticipants(accountId, eventType, participantsToMatch, matchFlags); QVERIFY(!retrievedThread.isEmpty()); QCOMPARE(retrievedThread[History::FieldAccountId], thread[History::FieldAccountId]); QCOMPARE(retrievedThread[History::FieldThreadId], thread[History::FieldThreadId]); QCOMPARE(retrievedThread[History::FieldType], thread[History::FieldType]); QCOMPARE(retrievedThread[History::FieldCount], thread[History::FieldCount]); QCOMPARE(retrievedThread[History::FieldUnreadCount], thread[History::FieldUnreadCount]); } void SqlitePluginTest::testEmptyThreadForParticipants() { QVariantMap thread = mPlugin->threadForParticipants("randomAccount", History::EventTypeText, QStringList()); QVERIFY(thread.isEmpty()); } void SqlitePluginTest::testGetSingleThread() { // reset the database SQLiteDatabase::instance()->reopen(); // create the thread that we will retrieve later QVariantMap thread = mPlugin->createThreadForParticipants("theAccountId", History::EventTypeText, QStringList() << "theParticipant"); QVERIFY(!thread.isEmpty()); // now create some other threads just to make sure the correct one is retrieved mPlugin->createThreadForParticipants("theAccountId", History::EventTypeText, QStringList() << "otherParticipant"); mPlugin->createThreadForParticipants("theAccountId", History::EventTypeVoice, QStringList() << "theParticipant"); mPlugin->createThreadForParticipants("otherAccount", History::EventTypeText, QStringList() << "theParticipant"); // and now retrieve the thread QVariantMap retrievedThread = mPlugin->getSingleThread(History::EventTypeText, "theAccountId", thread[History::FieldThreadId].toString()); QCOMPARE(retrievedThread[History::FieldAccountId], thread[History::FieldAccountId]); QCOMPARE(retrievedThread[History::FieldThreadId], thread[History::FieldThreadId]); QCOMPARE(retrievedThread[History::FieldType], thread[History::FieldType]); QCOMPARE(retrievedThread[History::FieldCount], thread[History::FieldCount]); QCOMPARE(retrievedThread[History::FieldUnreadCount], thread[History::FieldUnreadCount]); // FIXME: check that the last event data is also present } void SqlitePluginTest::testRemoveThread() { // reset the database SQLiteDatabase::instance()->reopen(); QVariantMap thread = mPlugin->createThreadForParticipants("oneAccountId", History::EventTypeText, QStringList() << "oneParticipant"); QVERIFY(!thread.isEmpty()); QVERIFY(mPlugin->removeThread(thread)); // now check that the thread was really removed QSqlQuery query(SQLiteDatabase::instance()->database()); QVERIFY(query.exec("SELECT count(*) FROM threads")); QVERIFY(query.next()); QCOMPARE(query.value(0).toInt(), 0); // and check that the participants were also removed QVERIFY(query.exec("SELECT count(*) FROM thread_participants")); QVERIFY(query.next()); QCOMPARE(query.value(0).toInt(), 0); } void SqlitePluginTest::testBatchOperation() { // clear the database SQLiteDatabase::instance()->reopen(); QSqlQuery query(SQLiteDatabase::instance()->database()); QVERIFY(mPlugin->beginBatchOperation()); QVERIFY(query.exec("UPDATE schema_version SET version=123")); QVERIFY(mPlugin->endBatchOperation()); // check that the data was actually written QVERIFY(query.exec("SELECT version FROM schema_version")); QVERIFY(query.next()); QCOMPARE(query.value(0).toInt(), 123); } void SqlitePluginTest::testRollback() { // clear the database SQLiteDatabase::instance()->reopen(); QSqlQuery query(SQLiteDatabase::instance()->database()); QVERIFY(query.exec("SELECT version FROM schema_version")); QVERIFY(query.next()); int version = query.value(0).toInt(); QVERIFY(mPlugin->beginBatchOperation()); QVERIFY(query.exec("UPDATE schema_version SET version=255")); QVERIFY(mPlugin->rollbackBatchOperation()); // check that the steps were reverted QVERIFY(query.exec("SELECT version FROM schema_version")); QVERIFY(query.next()); QCOMPARE(query.value(0).toInt(), version); } void SqlitePluginTest::testQueryThreads() { // just make sure the returned view is of the correct type. The views are going to be tested in their own tests History::PluginThreadView *view = mPlugin->queryThreads(History::EventTypeVoice); QVERIFY(view); QVERIFY(dynamic_cast(view)); view->deleteLater(); } void SqlitePluginTest::testQueryEvents() { // just make sure the returned view is of the correct type. The views are going to be tested in their own tests History::PluginEventView *view = mPlugin->queryEvents(History::EventTypeText); QVERIFY(view); QVERIFY(dynamic_cast(view)); view->deleteLater(); } void SqlitePluginTest::testWriteTextEvent_data() { QTest::addColumn("event"); // for test purposes, the threadId == senderId to make it easier QTest::newRow("new text event with pending flag") << History::TextEvent("oneAccountId", "theSender", "oneEventId", "theSender", QDateTime::currentDateTime(), QDateTime::currentDateTime(), true, "Hello World!", History::MessageTypeText, History::MessageStatusPending).properties(); QTest::newRow("text event with valid read timestamp") << History::TextEvent("otherAccountId", "otherSender", "otherEventId", "otherSender", QDateTime::currentDateTime(), QDateTime::currentDateTime(), false, "Hi Again!", History::MessageTypeText, History::MessageStatusDelivered, QDateTime::currentDateTime()).properties(); QTest::newRow("text event with no sent time") << History::TextEvent("noSentAccountId", "noSentSender", "noSentEventId", "noSentSender", QDateTime::currentDateTime(), false, "Hi Again!", History::MessageTypeText, History::MessageStatusDelivered, QDateTime::currentDateTime()).properties(); History::TextEventAttachments attachments; attachments << History::TextEventAttachment("mmsAccountId", "mmsSender", "mmsEventId", "mmsAttachment1", "text/plain", "/the/file/path", History::AttachmentDownloaded); QTest::newRow("text event with attachments") << History::TextEvent("mmsAccountId", "mmsSender", "mmsEventId", "mmsSender", QDateTime::currentDateTime(), QDateTime::currentDateTime(), false, "Hello with attachments", History::MessageTypeMultiPart, History::MessageStatusDelivered, QDateTime::currentDateTime(), "The Subject", History::InformationTypeNone, attachments).properties(); } void SqlitePluginTest::testWriteTextEvent() { QFETCH(QVariantMap, event); // clear the database SQLiteDatabase::instance()->reopen(); // create the thread QVariantMap thread = mPlugin->createThreadForParticipants(event[History::FieldAccountId].toString(), History::EventTypeText, QStringList() << event[History::FieldSenderId].toString()); QVERIFY(!thread.isEmpty()); // write the text event History::EventWriteResult result = mPlugin->writeTextEvent(event); QCOMPARE(result, History::EventWriteCreated); // check that the event is properly written to the database QSqlQuery query(SQLiteDatabase::instance()->database()); QVERIFY(query.exec("SELECT * FROM text_events")); int count = 0; while(query.next()) { count++; QCOMPARE(query.value("accountId"), event[History::FieldAccountId]); QCOMPARE(query.value("threadId"), event[History::FieldThreadId]); QCOMPARE(query.value("eventId"), event[History::FieldEventId]); QCOMPARE(query.value("senderId"), event[History::FieldSenderId]); QCOMPARE(mPlugin->toLocalTimeString(query.value("timestamp").toDateTime()), event[History::FieldTimestamp].toString()); QCOMPARE(mPlugin->toLocalTimeString(query.value("sentTime").toDateTime()), event[History::FieldSentTime].toString()); QCOMPARE(query.value("newEvent"), event[History::FieldNewEvent]); QCOMPARE(query.value("message"), event[History::FieldMessage]); QCOMPARE(query.value("messageType"), event[History::FieldMessageType]); QCOMPARE(query.value("messageStatus"), event[History::FieldMessageStatus]); QCOMPARE(mPlugin->toLocalTimeString(query.value("readTimestamp").toDateTime()), event[History::FieldReadTimestamp].toString()); QCOMPARE(query.value("subject"), event[History::FieldSubject]); } // check that only one event got written QCOMPARE(count, 1); // check that the attachments got saved, if any if (event[History::FieldMessageType].toInt() == History::MessageTypeMultiPart) { QVariantMap attachment = event[History::FieldAttachments].value >()[0]; QVERIFY(query.exec("SELECT * FROM text_event_attachments")); int count = 0; while(query.next()) { count++; QCOMPARE(query.value("accountId"), attachment[History::FieldAccountId]); QCOMPARE(query.value("threadId"), attachment[History::FieldThreadId]); QCOMPARE(query.value("eventId"), attachment[History::FieldEventId]); QCOMPARE(query.value("attachmentId"), attachment[History::FieldAttachmentId]); QCOMPARE(query.value("contentType"), attachment[History::FieldContentType]); QCOMPARE(query.value("filePath"), attachment[History::FieldFilePath]); QCOMPARE(query.value("status"), attachment[History::FieldStatus]); } QCOMPARE(count, 1); } // and check that the thread's last item got updated thread = mPlugin->getSingleThread(History::EventTypeText, thread[History::FieldAccountId].toString(), thread[History::FieldThreadId].toString()); QCOMPARE(thread[History::FieldAccountId], event[History::FieldAccountId]); QCOMPARE(thread[History::FieldThreadId], event[History::FieldThreadId]); QCOMPARE(thread[History::FieldEventId], event[History::FieldEventId]); QCOMPARE(thread[History::FieldSenderId], event[History::FieldSenderId]); QCOMPARE(thread[History::FieldTimestamp], event[History::FieldTimestamp]); QCOMPARE(thread[History::FieldNewEvent], event[History::FieldNewEvent]); QCOMPARE(thread[History::FieldMessage], event[History::FieldMessage]); QCOMPARE(thread[History::FieldMessageType], event[History::FieldMessageType]); QCOMPARE(thread[History::FieldMessageStatus], event[History::FieldMessageStatus]); QCOMPARE(thread[History::FieldReadTimestamp], event[History::FieldReadTimestamp]); } void SqlitePluginTest::testModifyTextEvent() { // clear the database SQLiteDatabase::instance()->reopen(); QVariantMap thread = mPlugin->createThreadForParticipants("theAccountId", History::EventTypeText, QStringList() << "theParticipant"); QVERIFY(!thread.isEmpty()); History::TextEventAttachment attachment(thread[History::FieldAccountId].toString(), thread[History::FieldThreadId].toString(), thread[History::FieldEventId].toString(), "theAttachmentId", "text/plain", "/file/path"); History::TextEvent textEvent(thread[History::FieldAccountId].toString(), thread[History::FieldThreadId].toString(), "theEventId", "theParticipant", QDateTime::currentDateTime(), QDateTime::currentDateTime(), true, "Hi there!", History::MessageTypeMultiPart, History::MessageStatusPending, QDateTime::currentDateTime(), "theSubject", History::InformationTypeNone, History::TextEventAttachments() << attachment); QCOMPARE(mPlugin->writeTextEvent(textEvent.properties()), History::EventWriteCreated); // now modify the fields that can be modified in the class textEvent.setNewEvent(false); textEvent.setMessageStatus(History::MessageStatusDelivered); textEvent.setReadTimestamp(QDateTime::currentDateTime()); QCOMPARE(mPlugin->writeTextEvent(textEvent.properties()), History::EventWriteModified); // and check that the data is actually up-to-date in the database QVariantMap event = textEvent.properties(); QSqlQuery query(SQLiteDatabase::instance()->database()); QVERIFY(query.exec("SELECT * FROM text_events")); int count = 0; while(query.next()) { count++; QCOMPARE(query.value("accountId"), event[History::FieldAccountId]); QCOMPARE(query.value("threadId"), event[History::FieldThreadId]); QCOMPARE(query.value("eventId"), event[History::FieldEventId]); QCOMPARE(query.value("senderId"), event[History::FieldSenderId]); QCOMPARE(mPlugin->toLocalTimeString(query.value("timestamp").toDateTime()), event[History::FieldTimestamp].toString()); QCOMPARE(mPlugin->toLocalTimeString(query.value("sentTime").toDateTime()), event[History::FieldSentTime].toString()); QCOMPARE(query.value("newEvent"), event[History::FieldNewEvent]); QCOMPARE(query.value("message"), event[History::FieldMessage]); QCOMPARE(query.value("messageType"), event[History::FieldMessageType]); QCOMPARE(query.value("messageStatus"), event[History::FieldMessageStatus]); QCOMPARE(mPlugin->toLocalTimeString(query.value("readTimestamp").toDateTime()), event[History::FieldReadTimestamp].toString()); QCOMPARE(query.value("subject"), event[History::FieldSubject]); } // check that only one event got written QCOMPARE(count, 1); } void SqlitePluginTest::testRemoveTextEvent() { // clear the database SQLiteDatabase::instance()->reopen(); QVariantMap thread = mPlugin->createThreadForParticipants("theAccountId", History::EventTypeText, QStringList() << "theParticipant"); QVERIFY(!thread.isEmpty()); History::TextEvent textEvent(thread[History::FieldAccountId].toString(), thread[History::FieldThreadId].toString(), "theSenderId", "theEventId", QDateTime::currentDateTime(), QDateTime::currentDateTime(), true, "Hello World!", History::MessageTypeText, History::MessageStatusPending); QCOMPARE(mPlugin->writeTextEvent(textEvent.properties()), History::EventWriteCreated); // now remove the item and check that it is really removed from the database QVERIFY(mPlugin->removeTextEvent(textEvent.properties())); // check that the event was removed from the database QSqlQuery query(SQLiteDatabase::instance()->database()); QVERIFY(query.exec("SELECT count(*) FROM text_events")); QVERIFY(query.next()); QCOMPARE(query.value(0).toInt(), 0); } void SqlitePluginTest::testWriteVoiceEvent_data() { QTest::addColumn("event"); // for test purposes, the threadId == senderId to make it easier QTest::newRow("missed call") << History::VoiceEvent("theAccountId", "theSenderId", "theEventId", "theSenderId", QDateTime::currentDateTime(), true, true).properties(); QTest::newRow("incoming call") << History::VoiceEvent("otherAccountId", "otherSenderId", "otherEventId", "otherSenderId", QDateTime::currentDateTime(), false, false, QTime(0,10,30)).properties(); } void SqlitePluginTest::testWriteVoiceEvent() { QFETCH(QVariantMap, event); // clear the database SQLiteDatabase::instance()->reopen(); // create the thread QVariantMap thread = mPlugin->createThreadForParticipants(event[History::FieldAccountId].toString(), History::EventTypeVoice, QStringList() << event[History::FieldSenderId].toString()); QVERIFY(!thread.isEmpty()); // write the voice event History::EventWriteResult result = mPlugin->writeVoiceEvent(event); QCOMPARE(result, History::EventWriteCreated); // check that the event is properly written to the database QSqlQuery query(SQLiteDatabase::instance()->database()); QVERIFY(query.exec("SELECT * FROM voice_events")); int count = 0; while(query.next()) { count++; QCOMPARE(query.value("accountId"), event[History::FieldAccountId]); QCOMPARE(query.value("threadId"), event[History::FieldThreadId]); QCOMPARE(query.value("eventId"), event[History::FieldEventId]); QCOMPARE(query.value("senderId"), event[History::FieldSenderId]); QCOMPARE(mPlugin->toLocalTimeString(query.value("timestamp").toDateTime()), event[History::FieldTimestamp].toString()); QCOMPARE(query.value("newEvent"), event[History::FieldNewEvent]); QCOMPARE(query.value("missed"), event[History::FieldMissed]); QCOMPARE(query.value("duration"), event[History::FieldDuration]); } // check that only one event got written QCOMPARE(count, 1); // and check that the thread's last item got updated thread = mPlugin->getSingleThread(History::EventTypeVoice, thread[History::FieldAccountId].toString(), thread[History::FieldThreadId].toString()); QCOMPARE(thread[History::FieldAccountId], event[History::FieldAccountId]); QCOMPARE(thread[History::FieldThreadId], event[History::FieldThreadId]); QCOMPARE(thread[History::FieldEventId], event[History::FieldEventId]); QCOMPARE(thread[History::FieldSenderId], event[History::FieldSenderId]); QCOMPARE(thread[History::FieldTimestamp], event[History::FieldTimestamp]); QCOMPARE(thread[History::FieldNewEvent], event[History::FieldNewEvent]); QCOMPARE(thread[History::FieldMissed], event[History::FieldMissed]); QCOMPARE(thread[History::FieldDuration], event[History::FieldDuration]); } void SqlitePluginTest::testModifyVoiceEvent() { // clear the database SQLiteDatabase::instance()->reopen(); QVariantMap thread = mPlugin->createThreadForParticipants("theAccountId", History::EventTypeVoice, QStringList() << "theParticipant"); QVERIFY(!thread.isEmpty()); History::VoiceEvent voiceEvent(thread[History::FieldAccountId].toString(), thread[History::FieldThreadId].toString(), "theEventId", "theParticipant", QDateTime::currentDateTime(), true, true, QTime(0, 1, 2)); QCOMPARE(mPlugin->writeVoiceEvent(voiceEvent.properties()), History::EventWriteCreated); // now modify the fields that can be modified in the class voiceEvent.setNewEvent(false); QCOMPARE(mPlugin->writeVoiceEvent(voiceEvent.properties()), History::EventWriteModified); // and check that the data is actually up-to-date in the database QVariantMap event = voiceEvent.properties(); QSqlQuery query(SQLiteDatabase::instance()->database()); QVERIFY(query.exec("SELECT * FROM voice_events")); int count = 0; while(query.next()) { count++; QCOMPARE(query.value("accountId"), event[History::FieldAccountId]); QCOMPARE(query.value("threadId"), event[History::FieldThreadId]); QCOMPARE(query.value("eventId"), event[History::FieldEventId]); QCOMPARE(query.value("senderId"), event[History::FieldSenderId]); QCOMPARE(mPlugin->toLocalTimeString(query.value("timestamp").toDateTime()), event[History::FieldTimestamp].toString()); QCOMPARE(query.value("newEvent"), event[History::FieldNewEvent]); QCOMPARE(query.value("missed"), event[History::FieldMissed]); QCOMPARE(query.value("duration"), event[History::FieldDuration]); } // check that only one event got written QCOMPARE(count, 1); } void SqlitePluginTest::testRemoveVoiceEvent() { // clear the database SQLiteDatabase::instance()->reopen(); QVariantMap thread = mPlugin->createThreadForParticipants("theAccountId", History::EventTypeVoice, QStringList() << "theParticipant"); QVERIFY(!thread.isEmpty()); History::VoiceEvent voiceEvent(thread[History::FieldAccountId].toString(), thread[History::FieldThreadId].toString(), "theSenderId", "theEventId", QDateTime::currentDateTime(), true, true, QTime(0, 5, 10)); QCOMPARE(mPlugin->writeVoiceEvent(voiceEvent.properties()), History::EventWriteCreated); // now remove the item and check that it is really removed from the database QVERIFY(mPlugin->removeVoiceEvent(voiceEvent.properties())); // check that the event was removed from the database QSqlQuery query(SQLiteDatabase::instance()->database()); QVERIFY(query.exec("SELECT count(*) FROM voice_events")); QVERIFY(query.next()); QCOMPARE(query.value(0).toInt(), 0); } void SqlitePluginTest::testRemoveEvents() { // clear the database SQLiteDatabase::instance()->reopen(); QVariantMap thread = mPlugin->createThreadForParticipants("theAccountId", History::EventTypeVoice, QStringList() << "theParticipant"); QVERIFY(!thread.isEmpty()); History::VoiceEvent voiceEvent(thread[History::FieldAccountId].toString(), thread[History::FieldThreadId].toString(), "theEventId", "theParticipant", QDateTime::currentDateTime().addDays(-10), true, false, QTime(0, 5, 10)); QCOMPARE(mPlugin->writeVoiceEvent(voiceEvent.properties()), History::EventWriteCreated); History::VoiceEvent voiceEvent2(thread[History::FieldAccountId].toString(), thread[History::FieldThreadId].toString(), "theEventId2", "theParticipant", QDateTime::currentDateTime().addDays(-1), true, false, QTime(0, 6, 10)); QCOMPARE(mPlugin->writeVoiceEvent(voiceEvent2.properties()), History::EventWriteCreated); History::Filter filter; filter.setFilterProperty(History::FieldTimestamp); filter.setFilterValue(QDateTime::currentDateTime().addDays(-5).toString("yyyy-MM-ddTHH:mm:ss.zzz")); filter.setMatchFlags(History::MatchLess); // remove events before 5 days and check that voiceEvent is really removed from the database and that only voiceEvent2 is still there QCOMPARE(mPlugin->removeEvents(History::EventTypeVoice, filter), 1); QSqlQuery query(SQLiteDatabase::instance()->database()); QVERIFY(query.exec("SELECT count(*) FROM voice_events")); QVERIFY(query.next()); QCOMPARE(query.value(0).toInt(), 1); QVERIFY(query.exec("SELECT * FROM voice_events")); QVERIFY(query.next()); QCOMPARE(query.value("eventId"), "theEventId2"); // we should still have a thread QVERIFY(query.exec("SELECT count(*) FROM threads")); QVERIFY(query.next()); QCOMPARE(query.value(0).toInt(), 1); // now check if we remove the last event filter.setFilterValue(QDateTime::currentDateTime().toString("yyyy-MM-ddTHH:mm:ss.zzz")); QCOMPARE(mPlugin->removeEvents(History::EventTypeVoice, filter), 1); QVERIFY(query.exec("SELECT count(*) FROM voice_events")); QVERIFY(query.next()); QCOMPARE(query.value(0).toInt(), 0); // it should remove the thread too QVERIFY(query.exec("SELECT count(*) FROM threads")); QVERIFY(query.next()); QCOMPARE(query.value(0).toInt(), 0); } void SqlitePluginTest::testGetFilePaths() { // clear the database SQLiteDatabase::instance()->reopen(); QVariantMap textThread = mPlugin->createThreadForParticipants("theAccountId", History::EventTypeText, QStringList() << "theParticipant"); QVERIFY(!textThread.isEmpty()); History::TextEventAttachment attachment(textThread[History::FieldAccountId].toString(), textThread[History::FieldThreadId].toString(), QString("textEventId"), QString("attachment"), "text/plain", "/some/file/path"); History::TextEvent textEvent(textThread[History::FieldAccountId].toString(), textThread[History::FieldThreadId].toString(), QString("textEventId"), "textParticipant", QDateTime::currentDateTime(), QDateTime::currentDateTime().addSecs(-10), true, "Hello World!", History::MessageTypeMultiPart, History::MessageStatusPending, QDateTime::currentDateTime(), "theSubject", History::InformationTypeNone, History::TextEventAttachments() << attachment); QCOMPARE(mPlugin->writeTextEvent(textEvent.properties()), History::EventWriteCreated); History::Filter filter; filter.setFilterProperty(History::FieldTimestamp); filter.setFilterValue(QDateTime::currentDateTime().addDays(5).toString("yyyy-MM-ddTHH:mm:ss.zzz")); filter.setMatchFlags(History::MatchLess); QList filePaths = mPlugin->attachmentFilePathsForFilter(filter); QCOMPARE(filePaths.count(), 1); QCOMPARE(filePaths.first(), "/some/file/path"); } void SqlitePluginTest::testEventsForThread() { // clear the database SQLiteDatabase::instance()->reopen(); // test text events QVariantMap textThread = mPlugin->createThreadForParticipants("textAccountId", History::EventTypeText, QStringList() << "textParticipant"); QVERIFY(!textThread.isEmpty()); // now insert 50 events for (int i = 0; i < 50; ++i) { History::TextEventAttachment attachment(textThread[History::FieldAccountId].toString(), textThread[History::FieldThreadId].toString(), QString("textEventId%1").arg(QString::number(i)), QString("attachment%1").arg(i), "text/plain", "/some/file/path"); History::TextEvent textEvent(textThread[History::FieldAccountId].toString(), textThread[History::FieldThreadId].toString(), QString("textEventId%1").arg(QString::number(i)), "textParticipant", QDateTime::currentDateTime(), QDateTime::currentDateTime().addSecs(-10), true, "Hello World!", History::MessageTypeMultiPart, History::MessageStatusPending, QDateTime::currentDateTime(), "theSubject", History::InformationTypeNone, History::TextEventAttachments() << attachment); QCOMPARE(mPlugin->writeTextEvent(textEvent.properties()), History::EventWriteCreated); } QList textEvents = mPlugin->eventsForThread(textThread); QCOMPARE(textEvents.count(), 50); Q_FOREACH(const QVariantMap &textEvent, textEvents) { QCOMPARE(textEvent[History::FieldAccountId], textThread[History::FieldAccountId]); QCOMPARE(textEvent[History::FieldThreadId], textThread[History::FieldThreadId]); QCOMPARE(textEvent[History::FieldType], textThread[History::FieldType]); } // test voice events QVariantMap voiceThread = mPlugin->createThreadForParticipants("voiceAccountId", History::EventTypeVoice, QStringList() << "voiceParticipant"); QVERIFY(!voiceThread.isEmpty()); // now insert 50 events for (int i = 0; i < 50; ++i) { History::VoiceEvent voiceEvent(voiceThread[History::FieldAccountId].toString(), voiceThread[History::FieldThreadId].toString(), QString("voiceEventId%1").arg(QString::number(i)), "voiceParticipant", QDateTime::currentDateTime(), true, false, QTime(0, i, i)); QCOMPARE(mPlugin->writeVoiceEvent(voiceEvent.properties()), History::EventWriteCreated); } QList voiceEvents = mPlugin->eventsForThread(voiceThread); QCOMPARE(voiceEvents.count(), 50); Q_FOREACH(const QVariantMap &voiceEvent, voiceEvents) { QCOMPARE(voiceEvent[History::FieldAccountId], voiceThread[History::FieldAccountId]); QCOMPARE(voiceEvent[History::FieldThreadId], voiceThread[History::FieldThreadId]); QCOMPARE(voiceEvent[History::FieldType], voiceThread[History::FieldType]); } } void SqlitePluginTest::testGetSingleEvent_data() { QTest::addColumn("event"); // for test purposes, the threadId == senderId to make it easier QTest::newRow("new text event with pending flag") << History::TextEvent("oneAccountId", "theSender", "oneEventId", "theSender", QDateTime::currentDateTime(), QDateTime::currentDateTime(), true, "Hello World!", History::MessageTypeText, History::MessageStatusPending).properties(); QTest::newRow("text event with valid read timestamp") << History::TextEvent("otherAccountId", "otherSender", "otherEventId", "otherSender", QDateTime::currentDateTime(), QDateTime::currentDateTime(), false, "Hi Again!", History::MessageTypeText, History::MessageStatusDelivered, QDateTime::currentDateTime()).properties(); QTest::newRow("text event with no sent time") << History::TextEvent("noSentAccountId", "noSentSender", "noSentEventId", "noSentSender", QDateTime::currentDateTime(), false, "Hi Again!", History::MessageTypeText, History::MessageStatusDelivered, QDateTime::currentDateTime()).properties(); History::TextEventAttachments attachments; attachments << History::TextEventAttachment("mmsAccountId", "mmsSender", "mmsEventId", "mmsAttachment1", "text/plain", "/the/file/path", History::AttachmentDownloaded); QTest::newRow("text event with attachments") << History::TextEvent("mmsAccountId", "mmsSender", "mmsEventId", "mmsSender", QDateTime::currentDateTime(), QDateTime::currentDateTime(), false, "Hello with attachments", History::MessageTypeMultiPart, History::MessageStatusDelivered, QDateTime::currentDateTime(), "The Subject", History::InformationTypeNone, attachments).properties(); QTest::newRow("missed call") << History::VoiceEvent("theAccountId", "theSenderId", "theEventId", "theSenderId", QDateTime::currentDateTime(), true, true).properties(); QTest::newRow("incoming call") << History::VoiceEvent("otherAccountId", "otherSenderId", "otherEventId", "otherSenderId", QDateTime::currentDateTime(), false, false, QTime(0,10,30)).properties(); } void SqlitePluginTest::testGetSingleEvent() { QFETCH(QVariantMap, event); // clear the database SQLiteDatabase::instance()->reopen(); History::EventType type = (History::EventType) event[History::FieldType].toInt(); // create the thread QVariantMap thread = mPlugin->createThreadForParticipants(event[History::FieldAccountId].toString(), type, QStringList() << event[History::FieldSenderId].toString()); QVERIFY(!thread.isEmpty()); // write the event switch (type) { case History::EventTypeText: QCOMPARE(mPlugin->writeTextEvent(event), History::EventWriteCreated); break; case History::EventTypeVoice: QCOMPARE(mPlugin->writeVoiceEvent(event), History::EventWriteCreated); break; case History::EventTypeNull: break; } QVariantMap retrievedEvent = mPlugin->getSingleEvent(type, event[History::FieldAccountId].toString(), event[History::FieldThreadId].toString(), event[History::FieldEventId].toString()); QCOMPARE(retrievedEvent[History::FieldAccountId], event[History::FieldAccountId]); QCOMPARE(retrievedEvent[History::FieldThreadId], event[History::FieldThreadId]); QCOMPARE(retrievedEvent[History::FieldEventId], event[History::FieldEventId]); QCOMPARE(retrievedEvent[History::FieldSenderId], event[History::FieldSenderId]); QCOMPARE(retrievedEvent[History::FieldTimestamp], event[History::FieldTimestamp]); QCOMPARE(retrievedEvent[History::FieldNewEvent], event[History::FieldNewEvent]); switch (type) { case History::EventTypeText: QCOMPARE(retrievedEvent[History::FieldMessage], event[History::FieldMessage]); QCOMPARE(retrievedEvent[History::FieldMessageType], event[History::FieldMessageType]); QCOMPARE(retrievedEvent[History::FieldMessageStatus], event[History::FieldMessageStatus]); QCOMPARE(retrievedEvent[History::FieldReadTimestamp], event[History::FieldReadTimestamp]); QCOMPARE(retrievedEvent[History::FieldSentTime], event[History::FieldSentTime]); break; case History::EventTypeVoice: QCOMPARE(retrievedEvent[History::FieldMissed], event[History::FieldMissed]); QCOMPARE(retrievedEvent[History::FieldDuration], event[History::FieldDuration]); break; case History::EventTypeNull: break; } } void SqlitePluginTest::testFilterToString_data() { QTest::addColumn("filterProperties"); QTest::addColumn("filterValues"); QTest::addColumn("propertyPrefix"); QTest::addColumn("resultString"); History::Filter filter; QVariantMap filterValues; filter.setFilterProperty("testProperty"); filter.setFilterValue("stringValue"); filterValues[":filterValue0"] = filter.filterValue(); QTest::newRow("simple string filter") << filter.properties() << filterValues << QString() << "testProperty=:filterValue0"; filter.setFilterValue(12345); filterValues[":filterValue0"] = filter.filterValue(); QTest::newRow("simple integer filter") << filter.properties() << filterValues << QString() << "testProperty=:filterValue0"; filter.setFilterValue(true); filterValues[":filterValue0"] = filter.filterValue(); QTest::newRow("simple true boolean filter") << filter.properties() << filterValues << QString() << "testProperty=:filterValue0"; filter.setFilterValue(false); filterValues[":filterValue0"] = filter.filterValue(); QTest::newRow("simple false boolean filter") << filter.properties() << filterValues << QString() << "testProperty=:filterValue0"; filter.setFilterValue(12345); filterValues[":filterValue0"] = filter.filterValue(); QTest::newRow("filter with a prefix") << filter.properties() << filterValues << QString("prefix") << "prefix.testProperty=:filterValue0"; filter.setFilterValue(12345); filter.setMatchFlags(History::MatchLess); filterValues[":filterValue0"] = filter.filterValue(); QTest::newRow("filter less") << filter.properties() << filterValues << QString() << "testProperty<:filterValue0"; filter.setFilterValue(12345); filter.setMatchFlags(History::MatchGreater); filterValues[":filterValue0"] = filter.filterValue(); QTest::newRow("filter greather") << filter.properties() << filterValues << QString() << "testProperty>:filterValue0"; filter.setFilterValue(12345); filter.setMatchFlags(History::MatchLessOrEquals); filterValues[":filterValue0"] = filter.filterValue(); QTest::newRow("filter less or equals") << filter.properties() << filterValues << QString() << "testProperty<=:filterValue0"; filter.setFilterValue(12345); filter.setMatchFlags(History::MatchGreaterOrEquals); filterValues[":filterValue0"] = filter.filterValue(); QTest::newRow("filter greater or equals") << filter.properties() << filterValues << QString() << "testProperty>=:filterValue0"; filter.setFilterValue(12345); filter.setMatchFlags(History::MatchNotEquals); filterValues[":filterValue0"] = filter.filterValue(); QTest::newRow("filter not equals") << filter.properties() << filterValues << QString() << "testProperty!=:filterValue0"; filter.setMatchFlags(History::MatchContains); filter.setFilterValue("partialString"); filterValues.clear(); QTest::newRow("match contains") << filter.properties() << filterValues << QString() << "testProperty LIKE '\%partialString\%' ESCAPE '\\'"; filter.setFilterValue("%"); filterValues.clear(); QTest::newRow("partial match escaped") << filter.properties() << filterValues << QString() << "testProperty LIKE '\%\\\%\%' ESCAPE '\\'"; History::IntersectionFilter intersectionFilter; filter.setMatchFlags(History::MatchFlags()); filter.setFilterValue(12345); intersectionFilter.append(filter); filter.setFilterValue(true); intersectionFilter.append(filter); filter.setFilterValue("a string"); intersectionFilter.append(filter); filterValues.clear(); filterValues[":filterValue0"] = 12345; filterValues[":filterValue1"] = true; filterValues[":filterValue2"] = "a string"; QTest::newRow("intersection filter") << intersectionFilter.properties() << filterValues << QString() << "( (testProperty=:filterValue0) AND (testProperty=:filterValue1) AND (testProperty=:filterValue2) )"; History::UnionFilter unionFilter; filter.setFilterValue(12345); unionFilter.append(filter); filter.setFilterValue(true); unionFilter.append(filter); filter.setFilterValue("a string"); unionFilter.append(filter); filterValues.clear(); filterValues[":filterValue0"] = 12345; filterValues[":filterValue1"] = true; filterValues[":filterValue2"] = "a string"; QTest::newRow("union filter") << unionFilter.properties() << filterValues << QString() << "( (testProperty=:filterValue0) OR (testProperty=:filterValue1) OR (testProperty=:filterValue2) )"; } void SqlitePluginTest::testFilterToString() { QFETCH(QVariantMap, filterProperties); QFETCH(QVariantMap, filterValues); QFETCH(QString, propertyPrefix); QFETCH(QString, resultString); QVariantMap resultValues; QString result = mPlugin->filterToString(History::Filter::fromProperties(filterProperties), resultValues, propertyPrefix); QCOMPARE(result, resultString); QCOMPARE(resultValues, filterValues); } void SqlitePluginTest::testEscapeFilterValue_data() { QTest::addColumn("originalString"); QTest::addColumn("escapedString"); QTest::newRow("backslash") << QString("\\") << QString("\\\\"); QTest::newRow("single quote") << QString("'") << QString("''"); QTest::newRow("percent") << QString("%") << QString("\\%"); QTest::newRow("underscore") << QString("_") << QString("\\_"); QTest::newRow("string with all of that") << QString("\\0\"'%_bla") << QString("\\\\0\"''\\%\\_bla"); } void SqlitePluginTest::testEscapeFilterValue() { QFETCH(QString, originalString); QFETCH(QString, escapedString); QCOMPARE(mPlugin->escapeFilterValue(originalString), escapedString); } void SqlitePluginTest::testUnreadCount() { // clear the database SQLiteDatabase::instance()->reopen(); // create the thread QVariantMap thread = mPlugin->createThreadForParticipants("theAccountId", History::EventTypeText, QStringList() << "theParticipant"); QVERIFY(!thread.isEmpty()); // write two text events History::TextEvent textEvent(thread[History::FieldAccountId].toString(), thread[History::FieldThreadId].toString(), "theEventId", "theParticipant", QDateTime::currentDateTime(), QDateTime::currentDateTime(), true, "Hi there!", History::MessageTypeMultiPart, History::MessageStatusPending, QDateTime::currentDateTime(), "theSubject", History::InformationTypeNone); QCOMPARE(mPlugin->writeTextEvent(textEvent.properties()), History::EventWriteCreated); History::TextEvent textEvent2(thread[History::FieldAccountId].toString(), thread[History::FieldThreadId].toString(), "theEventId2", "theParticipant", QDateTime::currentDateTime(), QDateTime::currentDateTime(), true, "Hi there!", History::MessageTypeMultiPart, History::MessageStatusPending, QDateTime::currentDateTime(), "theSubject", History::InformationTypeNone); QCOMPARE(mPlugin->writeTextEvent(textEvent2.properties()), History::EventWriteCreated); // we have 2 unread messages QCOMPARE(mPlugin->unreadCount(History::EventTypeText), 2); // remove one QVERIFY(mPlugin->removeTextEvent(textEvent.properties())); // now we should have 1 unread QCOMPARE(mPlugin->unreadCount(History::EventTypeText), 1); } void SqlitePluginTest::testMigrateAccountIds() { // reset the database SQLiteDatabase::instance()->reopen(); QSqlQuery query(SQLiteDatabase::instance()->database()); QVariantMap thread = mPlugin->createThreadForParticipants("ofono/ofono/account0", History::EventTypeText, QStringList() << "oneParticipant"); History::TextEvent textEvent(thread[History::FieldAccountId].toString(), thread[History::FieldThreadId].toString(), "theSenderId", "theEventId", QDateTime::currentDateTime(), QDateTime::currentDateTime(), true, "Hello World!", History::MessageTypeText, History::MessageStatusPending); QCOMPARE(mPlugin->writeTextEvent(textEvent.properties()), History::EventWriteCreated); thread = mPlugin->createThreadForParticipants("ofono/ofono/account0", History::EventTypeVoice, QStringList() << "oneParticipant"); History::VoiceEvent voiceEvent(thread[History::FieldAccountId].toString(), thread[History::FieldThreadId].toString(), "theEventId", "oneParticipant", QDateTime::currentDateTime(), true, true, QTime(0, 1, 2)); QCOMPARE(mPlugin->writeVoiceEvent(voiceEvent.properties()), History::EventWriteCreated); // non ofono accountId, nothing done mPlugin->verifyAccount("fakeAccountId"); QVERIFY(query.exec("SELECT count(*) FROM threads WHERE accountId = 'ofono/ofono/account0'")); QVERIFY(query.next()); QCOMPARE(query.value(0).toInt(), 2); // same accountId, nothing done mPlugin->verifyAccount("ofono/ofono/account0"); QVERIFY(query.exec("SELECT count(*) FROM threads WHERE accountId = 'ofono/ofono/account0'")); QVERIFY(query.next()); QCOMPARE(query.value(0).toInt(), 2); // accountId change, migration done mPlugin->verifyAccount("ofono/ofono/ril_0"); QVERIFY(query.exec("SELECT count(*) FROM threads WHERE accountId = 'ofono/ofono/ril_0'")); QVERIFY(query.next()); QCOMPARE(query.value(0).toInt(), 2); QVERIFY(query.exec("SELECT count(*) FROM thread_participants WHERE accountId = 'ofono/ofono/ril_0'")); QVERIFY(query.next()); QCOMPARE(query.value(0).toInt(), 2); QVERIFY(query.exec("SELECT count(*) FROM text_events WHERE accountId = 'ofono/ofono/ril_0'")); QVERIFY(query.next()); QCOMPARE(query.value(0).toInt(), 1); QVERIFY(query.exec("SELECT count(*) FROM voice_events WHERE accountId = 'ofono/ofono/ril_0'")); QVERIFY(query.next()); QCOMPARE(query.value(0).toInt(), 1); // multiple accountId to change, migration done mPlugin->createThreadForParticipants("ofono/ofono/other_0", History::EventTypeVoice, QStringList() << "oneParticipant"); mPlugin->verifyAccount("ofono/ofono/quectelqmi_0"); QVERIFY(query.exec("SELECT count(*) FROM threads WHERE accountId = 'ofono/ofono/quectelqmi_0'")); QVERIFY(query.next()); QCOMPARE(query.value(0).toInt(), 3); } QTEST_MAIN(SqlitePluginTest) #include "SqlitePluginTest.moc" history-service-0.5/tests/plugins/sqlite/SqliteThreadViewTest.cpp000066400000000000000000000122071455450246700254470ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 #include #include "sqlitehistoryplugin.h" #include "sqlitehistorythreadview.h" #include "sqlitedatabase.h" #include "textevent.h" #include "voiceevent.h" #include "unionfilter.h" Q_DECLARE_METATYPE(History::EventType) Q_DECLARE_METATYPE(History::MatchFlags) #define THREAD_COUNT 50 class SqliteThreadViewTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void testNextPage(); void testFilter(); void testSort(); private: SQLiteHistoryPlugin *mPlugin; void populateDatabase(); }; void SqliteThreadViewTest::initTestCase() { qRegisterMetaType(); qRegisterMetaType(); qputenv("HISTORY_SQLITE_DBPATH", ":memory:"); mPlugin = new SQLiteHistoryPlugin(this); populateDatabase(); } void SqliteThreadViewTest::testNextPage() { // create a view to return all text threads and check that the right number of items get returned History::PluginThreadView *view = mPlugin->queryThreads(History::EventTypeText); QVERIFY(view->IsValid()); QList threads = view->NextPage(); QList allThreads; while (threads.count() > 0) { allThreads << threads; threads = view->NextPage(); } QCOMPARE(allThreads.count(), THREAD_COUNT); Q_FOREACH(const QVariantMap &thread, threads) { QCOMPARE(thread[History::FieldType].toInt(), (int) History::EventTypeText); } delete view; } void SqliteThreadViewTest::testFilter() { History::UnionFilter filter; filter.append(History::Filter(History::FieldAccountId, "account10")); filter.append(History::Filter(History::FieldAccountId, "account35")); History::PluginThreadView *view = mPlugin->queryThreads(History::EventTypeVoice, History::Sort(History::FieldAccountId), filter); QVERIFY(view->IsValid()); QList threads = view->NextPage(); QCOMPARE(threads.count(), 2); QCOMPARE(threads.first()[History::FieldAccountId].toString(), QString("account10")); QCOMPARE(threads.first()[History::FieldType].toInt(), (int) History::EventTypeVoice); QCOMPARE(threads.last()[History::FieldAccountId].toString(), QString("account35")); QCOMPARE(threads.last()[History::FieldType].toInt(), (int) History::EventTypeVoice); // make sure no more items are returned QVERIFY(view->NextPage().isEmpty()); delete view; } void SqliteThreadViewTest::testSort() { History::Sort ascendingSort(History::FieldAccountId, Qt::AscendingOrder); History::PluginThreadView *view = mPlugin->queryThreads(History::EventTypeText, ascendingSort); QVERIFY(view->IsValid()); QList allThreads; QList threads = view->NextPage(); while (!threads.isEmpty()) { allThreads << threads; threads = view->NextPage(); } QCOMPARE(allThreads.first()[History::FieldAccountId].toString(), QString("account00")); QCOMPARE(allThreads.last()[History::FieldAccountId].toString(), QString("account%1").arg(THREAD_COUNT-1)); delete view; History::Sort descendingSort(History::FieldAccountId, Qt::DescendingOrder); allThreads.clear(); view = mPlugin->queryThreads(History::EventTypeVoice, descendingSort); QVERIFY(view->IsValid()); threads = view->NextPage(); while (!threads.isEmpty()) { allThreads << threads; threads = view->NextPage(); } QCOMPARE(allThreads.first()[History::FieldAccountId].toString(), QString("account%1").arg(THREAD_COUNT-1)); QCOMPARE(allThreads.last()[History::FieldAccountId].toString(), QString("account00")); delete view; } void SqliteThreadViewTest::populateDatabase() { mPlugin->beginBatchOperation(); // create voice threads for (int i = 0; i < THREAD_COUNT; ++i) { mPlugin->createThreadForParticipants(QString("account%1").arg(i, 2, 10, QChar('0')), History::EventTypeVoice, QStringList() << QString("participant%1").arg(i, 2, 10, QChar('0'))); } // and the text threads for (int i = 0; i < THREAD_COUNT; ++i) { mPlugin->createThreadForParticipants(QString("account%1").arg(i, 2, 10, QChar('0')), History::EventTypeText, QStringList() << QString("participant%1").arg(i, 2, 10, QChar('0'))); } mPlugin->endBatchOperation(); } QTEST_MAIN(SqliteThreadViewTest) #include "SqliteThreadViewTest.moc" history-service-0.5/tools/000077500000000000000000000000001455450246700156715ustar00rootroot00000000000000history-service-0.5/tools/CMakeLists.txt000066400000000000000000000001341455450246700204270ustar00rootroot00000000000000add_subdirectory(reader) add_subdirectory(maketextevents) add_subdirectory(makevoiceevents) history-service-0.5/tools/maketextevents/000077500000000000000000000000001455450246700207405ustar00rootroot00000000000000history-service-0.5/tools/maketextevents/CMakeLists.txt000066400000000000000000000005301455450246700234760ustar00rootroot00000000000000set(maketextevents_SRCS main.cpp) include_directories( ${CMAKE_SOURCE_DIR}/src ) add_executable(history-maketextevents ${maketextevents_SRCS}) qt5_use_modules(history-maketextevents Core) target_link_libraries(history-maketextevents historyservice) install(TARGETS history-maketextevents RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) history-service-0.5/tools/maketextevents/main.cpp000066400000000000000000000061511455450246700223730ustar00rootroot00000000000000/* * Copyright (C) 2014 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as * published by the Free Software Foundation. * * 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 . * * Authored by Kyle Nitzsche * */ #include "manager.h" #include "types.h" #include "textevent.h" #include "texteventattachment.h" #include #include #include #include using namespace History; int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); QStringList args = QCoreApplication::arguments(); if (args.size() == 1) { qDebug() << "Usage: maketextevents accountId"; return 1; } //TODO let the user pass numbers as args so they can corrspond to //the user's contacts QString number0 = "1234567890"; QString number1 = "01234456789"; QString number2 = "0987654321"; QString number; int idx = 0; QDateTime datetime = QDateTime::currentDateTime(); QDateTime sentTime = datetime.addSecs(-10); int control = 0; while (idx < 50) { idx ++; if (control == 0) { qDebug() << "=== control is 0"; number = number0; control++; } else if (control == 1) { qDebug() << "=== control is 1"; number = number1; control++; } else { qDebug() << "=== control is else"; control = 0; number = number2; datetime = datetime.addDays(-1); } const QString accountId = args.at(1); Thread thread = Manager::instance()->threadForParticipants(accountId, EventTypeText, QStringList() << number, MatchCaseSensitive, true); const QString threadId = thread.threadId(); const QString eventId = QString::number(QDateTime::currentMSecsSinceEpoch()); const QString sender = number; TextEvent ev(accountId, threadId, eventId, sender, datetime, sentTime, true, QString::fromStdString("the text message"), MessageTypeText, MessageStatusUnknown, datetime, QString::fromStdString("the subject")); QList events; events.append(ev); Manager::instance()->writeEvents(events); } } history-service-0.5/tools/makevoiceevents/000077500000000000000000000000001455450246700210615ustar00rootroot00000000000000history-service-0.5/tools/makevoiceevents/CMakeLists.txt000066400000000000000000000005361455450246700236250ustar00rootroot00000000000000set(makevoiceevents_SRCS main.cpp) include_directories( ${CMAKE_SOURCE_DIR}/src ) add_executable(history-makevoiceevents ${makevoiceevents_SRCS}) qt5_use_modules(history-makevoiceevents Core) target_link_libraries(history-makevoiceevents historyservice) install(TARGETS history-makevoiceevents RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) history-service-0.5/tools/makevoiceevents/main.cpp000066400000000000000000000057161455450246700225220ustar00rootroot00000000000000/* * Copyright (C) 2014 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as * published by the Free Software Foundation. * * 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 . * * Authored by Kyle Nitzsche * */ #include "manager.h" #include "types.h" #include "voiceevent.h" #include #include #include #include using namespace History; int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); QStringList args = QCoreApplication::arguments(); if (args.size() == 1) { qDebug() << "Usage: makevoiceevents accountId"; return 1; } //TODO let the user pass numbers as args so they can corrspond to //the user's contacts QString number0 = "1234567890"; QString number1 = "0987654321"; QString number2 = "3216540987"; QString number; int idx = 0; int control = 0; const QString accountId = args.at(1); QString sender; QDateTime datetime = QDateTime::currentDateTime(); while (idx < 50) { QList participants_; idx ++; bool missed; if (control == 0) { qDebug() << "=== control is 0"; number = number0; control++; missed = false; sender = "self"; } else if (control == 1) { qDebug() << "=== control is 1"; number = number1; control++; missed = true; sender = number; } else { qDebug() << "=== control is else"; control = 0; number = number2; datetime = datetime.addDays(-1); missed = false; sender = number; } Thread thread = Manager::instance()->threadForParticipants(accountId, EventTypeVoice, QStringList() << number, MatchCaseSensitive, true); const QString eventId = QString::number(QDateTime::currentMSecsSinceEpoch()); bool newEvent = true; const QTime duration = QTime(0,10,20); VoiceEvent ev(accountId, thread.threadId(), eventId, sender, datetime, newEvent, missed, duration, number, thread.participants()); QList events; events.append(ev); Manager::instance()->writeEvents(events); } } history-service-0.5/tools/reader/000077500000000000000000000000001455450246700171335ustar00rootroot00000000000000history-service-0.5/tools/reader/CMakeLists.txt000066400000000000000000000003341455450246700216730ustar00rootroot00000000000000set(reader_SRCS main.cpp) include_directories( ${CMAKE_SOURCE_DIR}/src ) add_executable(history-reader ${reader_SRCS}) qt5_use_modules(history-reader Core) target_link_libraries(history-reader historyservice) history-service-0.5/tools/reader/main.cpp000066400000000000000000000107531455450246700205710ustar00rootroot00000000000000/* * Copyright (C) 2013-2015 Canonical, Ltd. * * Authors: * Gustavo Pichorim Boiko * * This file is part of history-service. * * history-service 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; version 3. * * history-service 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 "manager.h" #include "eventview.h" #include "filter.h" #include "intersectionfilter.h" #include "thread.h" #include "threadview.h" #include "textevent.h" #include "voiceevent.h" #include #include void printEvent(const History::Event &event) { QString extraInfo; History::TextEvent textEvent; History::VoiceEvent voiceEvent; switch (event.type()) { case History::EventTypeText: textEvent = event; extraInfo = QString("message: %1").arg(textEvent.message()); break; case History::EventTypeVoice: voiceEvent = event; extraInfo = QString("missed: %1\n duration: %2").arg(voiceEvent.missed() ? "yes" : "no", voiceEvent.duration().toString()); break; case History::EventTypeNull: extraInfo = QString("Null event"); break; } qDebug() << qPrintable(QString(" * Event: accountId: %1\n threadId: %2\n eventId: %3\n senderId: %4\n timestamp: %5\n newEvent: %6") .arg(event.accountId(), event.threadId(), event.eventId(), event.senderId(), event.timestamp().toString(), event.newEvent() ? "yes" : "no")); qDebug() << qPrintable(QString(" %1").arg(extraInfo)); } void printThread(const History::Thread &thread) { QString type = "Unknown"; switch (thread.type()) { case History::EventTypeText: type = "Text"; break; case History::EventTypeVoice: type = "Voice"; break; case History::EventTypeNull: type = "Null"; break; } qDebug() << qPrintable(QString("- %1 thread - accountId: %2 threadId: %3 count: %4 unreadCount: %5").arg(type, thread.accountId(), thread.threadId(), QString::number(thread.count()), QString::number(thread.unreadCount()))); qDebug() << qPrintable(QString(" Participants: %1").arg(thread.participants().identifiers().join(", "))); if (!thread.lastEvent().isNull()) { qDebug() << " Last event:"; printEvent(thread.lastEvent()); } qDebug() << " All events:"; } int main(int argc, char **argv) { QCoreApplication app(argc, argv); History::Manager *manager = History::Manager::instance(); QList eventTypes; eventTypes << History::EventTypeText << History::EventTypeVoice; Q_FOREACH(History::EventType type, eventTypes) { History::ThreadViewPtr view = manager->queryThreads(type); History::Threads threads = view->nextPage(); while (!threads.isEmpty()) { Q_FOREACH(const History::Thread &thread, threads) { printThread(thread); // now print the events for this thread History::IntersectionFilter filter; filter.append(History::Filter(History::FieldThreadId, thread.threadId())); filter.append(History::Filter(History::FieldAccountId, thread.accountId())); History::EventViewPtr eventView = manager->queryEvents(type, History::Sort(), filter); History::Events events = eventView->nextPage(); while (!events.isEmpty()) { Q_FOREACH(const History::Event &event, events) { printEvent(event); } events = eventView->nextPage(); } } threads = view->nextPage(); } } }