From 32b27c53e03d3e1f25230efc48610efeab31f9a2 Mon Sep 17 00:00:00 2001 From: Rene Treffer Date: Wed, 2 Apr 2014 22:00:32 +0200 Subject: Initial commit --- AndroidManifest.xml | 13 + LICENCE | 4 + LICENCE_APACHE | 178 +++++++++++ LICENCE_LGPL2.1 | 503 +++++++++++++++++++++++++++++ LICENCE_WTFPL | 13 + README.md | 8 + build.gradle | 34 ++ build.xml | 92 ++++++ proguard-project.txt | 0 project.properties | 2 + src/de/measite/minidns/Client.java | 161 ++++++++++ src/de/measite/minidns/DNSMessage.java | 507 ++++++++++++++++++++++++++++++ src/de/measite/minidns/Question.java | 62 ++++ src/de/measite/minidns/Record.java | 293 +++++++++++++++++ src/de/measite/minidns/record/A.java | 37 +++ src/de/measite/minidns/record/AAAA.java | 43 +++ src/de/measite/minidns/record/CNAME.java | 44 +++ src/de/measite/minidns/record/Data.java | 16 + src/de/measite/minidns/record/NS.java | 12 + src/de/measite/minidns/record/SRV.java | 74 +++++ src/de/measite/minidns/util/NameUtil.java | 77 +++++ 21 files changed, 2173 insertions(+) create mode 100644 AndroidManifest.xml create mode 100644 LICENCE create mode 100644 LICENCE_APACHE create mode 100644 LICENCE_LGPL2.1 create mode 100644 LICENCE_WTFPL create mode 100644 README.md create mode 100644 build.gradle create mode 100644 build.xml create mode 100644 proguard-project.txt create mode 100644 project.properties create mode 100644 src/de/measite/minidns/Client.java create mode 100644 src/de/measite/minidns/DNSMessage.java create mode 100644 src/de/measite/minidns/Question.java create mode 100644 src/de/measite/minidns/Record.java create mode 100644 src/de/measite/minidns/record/A.java create mode 100644 src/de/measite/minidns/record/AAAA.java create mode 100644 src/de/measite/minidns/record/CNAME.java create mode 100644 src/de/measite/minidns/record/Data.java create mode 100644 src/de/measite/minidns/record/NS.java create mode 100644 src/de/measite/minidns/record/SRV.java create mode 100644 src/de/measite/minidns/util/NameUtil.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml new file mode 100644 index 00000000..6c9477d9 --- /dev/null +++ b/AndroidManifest.xml @@ -0,0 +1,13 @@ + + + + + + + + diff --git a/LICENCE b/LICENCE new file mode 100644 index 00000000..4c2ee7c0 --- /dev/null +++ b/LICENCE @@ -0,0 +1,4 @@ +This software may be used under the terms of (at your choice) +- LGPL version 2 (or later) (see LICENCE_LGPL2.1 for details) +- Apache Software licence (see LICENCE_APACHE for details) +- WTFPL (see LICENCE_WTFPL for details) diff --git a/LICENCE_APACHE b/LICENCE_APACHE new file mode 100644 index 00000000..e454a525 --- /dev/null +++ b/LICENCE_APACHE @@ -0,0 +1,178 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + diff --git a/LICENCE_LGPL2.1 b/LICENCE_LGPL2.1 new file mode 100644 index 00000000..51a70cae --- /dev/null +++ b/LICENCE_LGPL2.1 @@ -0,0 +1,503 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +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 this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +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 +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser 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 Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "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 +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY 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 +LIBRARY (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 LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey 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 library 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; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! + diff --git a/LICENCE_WTFPL b/LICENCE_WTFPL new file mode 100644 index 00000000..652d3783 --- /dev/null +++ b/LICENCE_WTFPL @@ -0,0 +1,13 @@ + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + Version 2, December 2004 + + Copyright (C) 2014 Rene Treffer + + Everyone is permitted to copy and distribute verbatim or modified + copies of this license document, and changing it is allowed as long + as the name is changed. + + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. You just DO WHAT THE FUCK YOU WANT TO. diff --git a/README.md b/README.md new file mode 100644 index 00000000..99ba78b3 --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +MiniDNS +------- + +MiniDNS is a minial dns client library for android. It can parse a basic set +of resource records (A, AAAA, NS, SRV) and is easy to use and extend. + +This library is not intended to be used as a DNS server. You might want to +look into dnsjava for such functionality. diff --git a/build.gradle b/build.gradle new file mode 100644 index 00000000..2a17c6ae --- /dev/null +++ b/build.gradle @@ -0,0 +1,34 @@ +buildscript { + repositories { + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:0.9.0' + } +} + +apply plugin: 'android-library' + +android { + compileSdkVersion 9 + buildToolsVersion '19.0.3' + + // NOTE: We are using the old folder structure to also support Eclipse + sourceSets { + main { + manifest.srcFile 'AndroidManifest.xml' + java.srcDirs = ['src'] + resources.srcDirs = ['src'] + aidl.srcDirs = ['src'] + renderscript.srcDirs = ['src'] + res.srcDirs = ['res'] + assets.srcDirs = ['assets'] + } + } + + // Do not abort build if lint finds errors + lintOptions { + abortOnError false + } +} diff --git a/build.xml b/build.xml new file mode 100644 index 00000000..ed7657f2 --- /dev/null +++ b/build.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/proguard-project.txt b/proguard-project.txt new file mode 100644 index 00000000..e69de29b diff --git a/project.properties b/project.properties new file mode 100644 index 00000000..7096c5ce --- /dev/null +++ b/project.properties @@ -0,0 +1,2 @@ +target=android-9 +android.library=true diff --git a/src/de/measite/minidns/Client.java b/src/de/measite/minidns/Client.java new file mode 100644 index 00000000..c32ec3a1 --- /dev/null +++ b/src/de/measite/minidns/Client.java @@ -0,0 +1,161 @@ +package de.measite.minidns; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.LineNumberReader; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.HashSet; +import java.util.Random; + +import de.measite.minidns.Record.CLASS; +import de.measite.minidns.Record.TYPE; + +/** + * A minimal DNS client for SRV/A/AAAA/NS and CNAME lookups, with IDN support. + * This circumvents the missing javax.naming package on android. + */ +public class Client { + + /** + * The internal random class for sequence generation. + */ + protected Random random; + + /** + * Create a new DNS client. + */ + public Client() { + try { + random = SecureRandom.getInstance("SHA1PRNG"); + } catch (NoSuchAlgorithmException e1) { + random = new SecureRandom(); + } + } + + /** + * Query a nameserver for a single entry. + * @param name The DNS name to request. + * @param type The DNS type to request (SRV, A, AAAA, ...). + * @param clazz The class of the request (usually IN for Internet). + * @param host The DNS server host. + * @return + * @throws IOException On IO Errors. + */ + public DNSMessage query(String name, TYPE type, CLASS clazz, String host) + throws IOException + { + Question q = new Question(); + q.setClazz(clazz); + q.setType(type); + q.setName(name); + return query(q, host); + } + + /** + * Query the system nameserver for a single entry. + * @param name The DNS name to request. + * @param type The DNS type to request (SRV, A, AAAA, ...). + * @param clazz The class of the request (usually IN for Internet). + * @return The DNSMessage reply or null. + */ + public DNSMessage query(String name, TYPE type, CLASS clazz) + { + Question q = new Question(); + q.setClazz(clazz); + q.setType(type); + q.setName(name); + return query(q); + } + + /** + * Query a specific server for one entry. + * @param q The question section of the DNS query. + * @param host The dns server host. + * @throws IOException On IOErrors. + */ + public DNSMessage query(Question q, String host) throws IOException { + DNSMessage message = new DNSMessage(); + message.setQuestions(new Question[]{q}); + message.setRecursionDesired(true); + message.setId(random.nextInt()); + byte[] buf = message.toArray(); + DatagramSocket socket = new DatagramSocket(); + DatagramPacket packet = new DatagramPacket( + buf, buf.length, InetAddress.getByName(host), 53); + socket.setSoTimeout(5000); + socket.send(packet); + packet = new DatagramPacket(new byte[513], 513); + socket.receive(packet); + DNSMessage dnsMessage = DNSMessage.parse(packet.getData()); + if (dnsMessage.getId() != message.getId()) { + return null; + } + return dnsMessage; + } + + /** + * Query the system DNS server for one entry. + * @param q The question section of the DNS query. + */ + public DNSMessage query(Question q) { + String dnsServer[] = findDNS(); + for (String dns : dnsServer) { + try { + DNSMessage message = query(q, dns); + if (message == null) { + continue; + } + if (message.getResponseCode() != + DNSMessage.RESPONSE_CODE.NO_ERROR) { + continue; + } + for (Record record: message.getAnswers()) { + if (record.isAnswer(q)) { + return message; + } + } + } catch (IOException ioe) { + } + } + return null; + } + + /** + * Retrieve a list of currently configured DNS servers. + * @return The server array. + */ + public String[] findDNS() { + try { + Process process = Runtime.getRuntime().exec("getprop"); + InputStream inputStream = process.getInputStream(); + LineNumberReader lnr = new LineNumberReader( + new InputStreamReader(inputStream)); + String line = null; + HashSet server = new HashSet(6); + while ((line = lnr.readLine()) != null) { + int split = line.indexOf("]: ["); + if (split == -1) { + continue; + } + String property = line.substring(1, split); + String value = line.substring(split + 4, line.length() - 1); + if (property.endsWith(".dns") || property.endsWith(".dns1") || + property.endsWith(".dns2") || property.endsWith(".dns3") || + property.endsWith(".dns4")) { + server.add(value); + } + } + if (server.size() > 0) { + return server.toArray(new String[server.size()]); + } + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } +} diff --git a/src/de/measite/minidns/DNSMessage.java b/src/de/measite/minidns/DNSMessage.java new file mode 100644 index 00000000..14c8f04b --- /dev/null +++ b/src/de/measite/minidns/DNSMessage.java @@ -0,0 +1,507 @@ +package de.measite.minidns; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.Arrays; + +/** + * A DNS message as defined by rfc1035. The message consists of a header and + * 4 sections: question, answer, nameserver and addition resource record + * section. + * A message can either be parsed ({@see #parse(byte[])}) or serialized + * ({@see #toArray()}). + */ +public class DNSMessage { + + /** + * Possible DNS reply codes. + */ + public static enum RESPONSE_CODE { + NO_ERROR(0), FORMAT_ERR(1), SERVER_FAIL(2), NX_DOMAIN(3), + NO_IMP(4), REFUSED(5), YXDOMAIN(6), YXRRSET(7), + NXRRSET(8), NOT_AUTH(9),NOT_ZONE(10); + + /** + * Reverse lookup table for response codes. + */ + private final static RESPONSE_CODE INVERSE_LUT[] = new RESPONSE_CODE[]{ + NO_ERROR, FORMAT_ERR, SERVER_FAIL, NX_DOMAIN, NO_IMP, + REFUSED, YXDOMAIN, YXRRSET, NXRRSET, NOT_AUTH, NOT_ZONE, + null, null, null, null, null + }; + + /** + * The response code value. + */ + private final byte value; + + /** + * Create a new response code. + * @param value The response code value. + */ + private RESPONSE_CODE(int value) { + this.value = (byte)value; + } + + /** + * Retrieve the byte value of the response code. + * @return + */ + public byte getValue() { + return (byte) value; + } + + /** + * Retrieve the response code for a byte value. + * @param value The byte value. + * @return The symbolic response code or null. + * @throws IllegalArgumentException if the value is not in the range of + * 0..15. + */ + public static RESPONSE_CODE getResponseCode(int value) { + if (value < 0 || value > 15) { + throw new IllegalArgumentException(); + } + return INVERSE_LUT[value]; + } + + }; + + /** + * Symbolic DNS Opcode values. + */ + public static enum OPCODE { + QUERY(0), + INVERSE_QUERY(1), + STATUS(2), + NOTIFY(4), + UPDATE(5); + + /** + * Lookup table for for obcode reolution. + */ + private final static OPCODE INVERSE_LUT[] = new OPCODE[]{ + QUERY, INVERSE_QUERY, STATUS, null, NOTIFY, UPDATE, null, + null, null, null, null, null, null, null, null + }; + + /** + * The value of this opcode. + */ + private final byte value; + + /** + * Create a new opcode for a given byte value. + * @param value The byte value of the opcode. + */ + private OPCODE(int value) { + this.value = (byte)value; + } + + /** + * Retrieve the byte value of this opcode. + * @return The byte value of this opcode. + */ + public byte getValue() { + return value; + } + + /** + * Retrieve the symbolic name of an opcode byte. + * @param value The byte value of the opcode. + * @return The symbolic opcode or null. + * @throws IllegalArgumentException If the byte value is not in the + * range 0..15. + */ + public static OPCODE getOpcode(int value) { + if (value < 0 || value > 15) { + throw new IllegalArgumentException(); + } + return INVERSE_LUT[value]; + } + + }; + + /** + * The DNS message id. + */ + protected int id; + + /** + * The DNS message opcode. + */ + protected OPCODE opcode; + + /** + * The response code of this dns message. + */ + protected RESPONSE_CODE responseCode; + + /** + * True if this is a query. + */ + protected boolean query; + + /** + * True if this is a authorative response. + */ + protected boolean authoritativeAnswer; + + /** + * True on truncate, tcp should be used. + */ + protected boolean truncated; + + /** + * True if the server should recurse. + */ + protected boolean recursionDesired; + + /** + * True if recursion is possible. + */ + protected boolean recursionAvailable; + + /** + * True if the server regarded the response as authentic. + */ + protected boolean authenticData; + + /** + * True if the server should not check the replies. + */ + protected boolean checkDisabled; + + /** + * The question section content. + */ + protected Question questions[]; + + /** + * The answers section content. + */ + protected Record answers[]; + + /** + * The nameserver records. + */ + protected Record nameserverRecords[]; + + /** + * Additional resousrce records. + */ + protected Record additionalResourceRecords[]; + + /** + * Retrieve the current DNS message id. + * @return The current DNS message id. + */ + public int getId() { + return id; + } + + /** + * Set the current DNS message id. + * @param id The new DNS message id. + */ + public void setId(int id) { + this.id = id & 0xffff; + } + + /** + * Retrieve the query type (true or false; + * @return True if this DNS message is a query. + */ + public boolean isQuery() { + return query; + } + + /** + * Set the query status of this message. + * @param query The new query status. + */ + public void setQuery(boolean query) { + this.query = query; + } + + /** + * True if the DNS message is an authoritative answer. + * @return True if this an authoritative DNS message. + */ + public boolean isAuthoritativeAnswer() { + return authoritativeAnswer; + } + + /** + * Set the authoritative answer flag. + * @param authoritativeAnswer Tge new authoritative answer value. + */ + public void setAuthoritativeAnswer(boolean authoritativeAnswer) { + this.authoritativeAnswer = authoritativeAnswer; + } + + /** + * Retrieve the truncation status of this message. True means that the + * client should try a tcp lookup. + * @return True if this message was truncated. + */ + public boolean isTruncated() { + return truncated; + } + + /** + * Set the truncation bit on this DNS message. + * @param truncated The new truncated bit status. + */ + public void setTruncated(boolean truncated) { + this.truncated = truncated; + } + + /** + * Check if this message preferes recursion. + * @return True if recursion is desired. + */ + public boolean isRecursionDesired() { + return recursionDesired; + } + + /** + * Set the recursion desired flag on this message. + * @param recursionDesired The new recusrion setting. + */ + public void setRecursionDesired(boolean recursionDesired) { + this.recursionDesired = recursionDesired; + } + + /** + * Retrieve the recursion available flag of this DNS message. + * @return The recursion available flag of this message. + */ + public boolean isRecursionAvailable() { + return recursionAvailable; + } + + /** + * Set the recursion available flog from this DNS message. + * @param recursionAvailable The new recursion available status. + */ + public void setRecursionAvailable(boolean recursionAvailable) { + this.recursionAvailable = recursionAvailable; + } + + /** + * Retrieve the authentic data flag of this message. + * @return The authentic data flag. + */ + public boolean isAuthenticData() { + return authenticData; + } + + /** + * Set the authentic data flag on this DNS message. + * @param authenticData The new authentic data flag value. + */ + public void setAuthenticData(boolean authenticData) { + this.authenticData = authenticData; + } + + /** + * Check if checks are disabled. + * @return The status of the CheckDisabled flag. + */ + public boolean isCheckDisabled() { + return checkDisabled; + } + + /** + * Change the check status of this packet. + * @param checkDisabled + */ + public void setCheckDisabled(boolean checkDisabled) { + this.checkDisabled = checkDisabled; + } + + /** + * Generate a binary dns packet out of this message. + * @return byte[] the binary representation. + * @throws IOException Should never happen. + */ + public byte[] toArray() throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(512); + DataOutputStream dos = new DataOutputStream(baos); + int header = 0; + if (query) { + header += 1 << 15; + } + if (opcode != null) { + header += opcode.getValue() << 11; + } + if (authoritativeAnswer) { + header += 1 << 10; + } + if (truncated) { + header += 1 << 9; + } + if (recursionDesired) { + header += 1 << 8; + } + if (recursionAvailable) { + header += 1 << 7; + } + if (authenticData) { + header += 1 << 5; + } + if (checkDisabled) { + header += 1 << 4; + } + if (responseCode != null) { + header += responseCode.getValue(); + } + dos.writeShort((short)id); + dos.writeShort((short)header); + if (questions == null) { + dos.writeShort(0); + } else { + dos.writeShort((short)questions.length); + } + if (answers == null) { + dos.writeShort(0); + } else { + dos.writeShort((short)answers.length); + } + if (nameserverRecords == null) { + dos.writeShort(0); + } else { + dos.writeShort((short)nameserverRecords.length); + } + if (additionalResourceRecords == null) { + dos.writeShort(0); + } else { + dos.writeShort((short)additionalResourceRecords.length); + } + for (Question question: questions) { + dos.write(question.toByteArray()); + } + dos.flush(); + return baos.toByteArray(); + } + + /** + * Build a DNS Message based on a binary DNS message. + * @param data The DNS message data. + * @return Parsed DNSMessage message. + * @throws IOException On read errors. + */ + public static DNSMessage parse(byte data[]) throws IOException { + ByteArrayInputStream bis = new ByteArrayInputStream(data); + DataInputStream dis = new DataInputStream(bis); + DNSMessage message = new DNSMessage(); + message.id = dis.readUnsignedShort(); + int header = dis.readUnsignedShort(); + message.query = ((header >> 15) & 1) == 0; + message.opcode = OPCODE.getOpcode((header >> 11) & 0xf); + message.authoritativeAnswer = ((header >> 10) & 1) == 1; + message.truncated = ((header >> 9) & 1) == 1; + message.recursionDesired = ((header >> 8) & 1) == 1; + message.recursionAvailable = ((header >> 7) & 1) == 1; + message.authenticData = ((header >> 5) & 1) == 1; + message.checkDisabled = ((header >> 4) & 1) == 1; + message.responseCode = RESPONSE_CODE.getResponseCode(header & 0xf); + int questionCount = dis.readUnsignedShort(); + int answerCount = dis.readUnsignedShort(); + int nameserverCount = dis.readUnsignedShort(); + int additionalResourceRecordCount = dis.readUnsignedShort(); + message.questions = new Question[questionCount]; + while (questionCount-- > 0) { + Question q = new Question(); + q.parse(dis, data); + message.questions[questionCount] = q; + } + message.answers = new Record[answerCount]; + while (answerCount-- > 0) { + Record rr = new Record(); + rr.parse(dis, data); + message.answers[answerCount] = rr; + } + message.nameserverRecords = new Record[nameserverCount]; + while (nameserverCount-- > 0) { + Record rr = new Record(); + rr.parse(dis, data); + message.nameserverRecords[nameserverCount] = rr; + } + message.additionalResourceRecords = + new Record[additionalResourceRecordCount]; + while (additionalResourceRecordCount-- > 0) { + Record rr = new Record(); + rr.parse(dis, data); + message.additionalResourceRecords[additionalResourceRecordCount] = + rr; + } + return message; + } + + /** + * Set the question part of this message. + * @param questions The questions. + */ + public void setQuestions(Question ... questions) { + this.questions = questions; + } + + /** + * Retrieve the opcode of this message. + * @return The opcode of this message. + */ + public OPCODE getOpcode() { + return opcode; + } + + /** + * Retrieve the response code of this message. + * @return The response code. + */ + public RESPONSE_CODE getResponseCode() { + return responseCode; + } + + /** + * Retrieve the question section of this message. + * @return The DNS question section. + */ + public Question[] getQuestions() { + return questions; + } + + /** + * Retrieve the answer records of this DNS message. + * @return The answer section of this DNS message. + */ + public Record[] getAnswers() { + return answers; + } + + /** + * Retrieve the nameserver records of this DNS message. + * @return The nameserver section of this DNS message. + */ + public Record[] getNameserverRecords() { + return nameserverRecords; + } + + /** + * Retrieve the additional resource records attached to this DNS message. + * @return The additional resource record section of this DNS message. + */ + public Record[] getAdditionalResourceRecords() { + return additionalResourceRecords; + } + + public String toString() { + return "-- DNSMessage " + id + " --\n" + + Arrays.toString(answers); + } + +} diff --git a/src/de/measite/minidns/Question.java b/src/de/measite/minidns/Question.java new file mode 100644 index 00000000..9d1e3f56 --- /dev/null +++ b/src/de/measite/minidns/Question.java @@ -0,0 +1,62 @@ +package de.measite.minidns; + +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +import de.measite.minidns.Record.CLASS; +import de.measite.minidns.Record.TYPE; +import de.measite.minidns.util.NameUtil; + +public class Question { + + private String name; + + private TYPE type; + + private CLASS clazz = CLASS.IN; + + public TYPE getType() { + return type; + } + + public void setType(TYPE type) { + this.type = type; + } + + public CLASS getClazz() { + return clazz; + } + + public void setClazz(CLASS clazz) { + this.clazz = clazz; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public void parse(DataInputStream dis, byte[] data) throws IOException { + this.name = NameUtil.parse(dis, data); + this.type = TYPE.getType(dis.readUnsignedShort()); + this.clazz = CLASS.getClass(dis.readUnsignedShort()); + } + + public byte[] toByteArray() throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(512); + DataOutputStream dos = new DataOutputStream(baos); + + dos.write(NameUtil.toByteArray(this.name)); + dos.writeShort(type.getValue()); + dos.writeShort(clazz.getValue()); + + dos.flush(); + return baos.toByteArray(); + } + +} diff --git a/src/de/measite/minidns/Record.java b/src/de/measite/minidns/Record.java new file mode 100644 index 00000000..adfa9713 --- /dev/null +++ b/src/de/measite/minidns/Record.java @@ -0,0 +1,293 @@ +package de.measite.minidns; + +import java.io.DataInputStream; +import java.io.IOException; +import java.util.HashMap; + +import de.measite.minidns.record.A; +import de.measite.minidns.record.AAAA; +import de.measite.minidns.record.CNAME; +import de.measite.minidns.record.Data; +import de.measite.minidns.record.NS; +import de.measite.minidns.record.SRV; +import de.measite.minidns.util.NameUtil; + +/** + * A generic DNS record. + */ +public class Record { + + /** + * The record type. + * {@see http://www.iana.org/assignments/dns-parameters} + */ + public static enum TYPE { + A(1), + NS(2), + MD(3), + MF(4), + CNAME(5), + SOA(6), + MB(7), + MG(8), + MR(9), + NULL(10), + WKS(11), + PTR(12), + HINFO(13), + MINFO(14), + MX(15), + TXT(16), + RP(17), + AFSDB(18), + X25(19), + ISDN(20), + RT(21), + NSAP(22), + NSAP_PTR(23), + SIG(24), + KEY(25), + PX(26), + GPOS(27), + AAAA(28), + LOC(29), + NXT(30), + EID(31), + NIMLOC(32), + SRV(33), + ATMA(34), + NAPTR(35), + KX(36), + CERT(37), + A6(38), + DNAME(39), + SINK(40), + OPT(41), + APL(42), + DS(43), + SSHFP(44), + IPSECKEY(45), + RRSIG(46), + NSEC(47), + DNSKEY(48), + DHCID(49), + NSEC3(50), + NSEC3PARAM(51), + HIP(55), + NINFO(56), + RKEY(57), + TALINK(58), + SPF(99), + UINFO(100), + UID(101), + GID(102), + TKEY(249), + TSIG(250), + IXFR(251), + AXFR(252), + MAILB(253), + MAILA(254), + ANY(255), + TA(32768), + DLV(32769); + + /** + * The value of this DNS record type. + */ + private final int value; + + /** + * Internal lookup table to map values to types. + */ + private final static HashMap INVERSE_LUT = + new HashMap(); + + /** + * Initialize the reverse lookup table. + */ + static { + for(TYPE t: TYPE.values()) { + INVERSE_LUT.put(t.getValue(), t); + } + } + + /** + * Create a new record type. + * @param value The binary value of this type. + */ + private TYPE(int value) { + this.value = value; + } + + /** + * Retrieve the binary value of this type. + * @return The binary value. + */ + public int getValue() { + return value; + } + + /** + * Retrieve the symbolic type of the binary value. + * @param value The binary type value. + * @return The symbolic tpye. + */ + public static TYPE getType(int value) { + return INVERSE_LUT.get(value); + } + }; + + /** + * The symbolic class of a DNS record (usually IN for Internet). + */ + public static enum CLASS { + IN(1), + CH(3), + HS(4), + NONE(254), + ANY(255); + + /** + * Internal reverse lookup table to map binary class values to symbolic + * names. + */ + private final static HashMap INVERSE_LUT = + new HashMap(); + + /** + * Initialize the interal reverse lookup table. + */ + static { + for(CLASS c: CLASS.values()) { + INVERSE_LUT.put(c.getValue(), c); + } + } + + /** + * The binary value of this dns class. + */ + private final int value; + + /** + * Create a new DNS class based on a binary value. + * @param value The binary value of this DNS class. + */ + private CLASS(int value) { + this.value = value; + } + + /** + * Retrieve the binary value of this DNS class. + * @return The binary value of this DNS class. + */ + public int getValue() { + return value; + } + + /** + * Retrieve the symbolic DNS class for a binary class value. + * @param value The binary DNS class value. + * @return The symbolic class instance. + */ + public static CLASS getClass(int value) { + return INVERSE_LUT.get(value); + } + + } + + /** + * The generic name of this record. + */ + protected String name; + + /** + * The type (and payload type) of this record. + */ + protected TYPE type; + + /** + * The record class (usually CLASS.IN). + */ + protected CLASS clazz; + + /** + * The ttl of this record. + */ + protected long ttl; + + /** + * The payload object of this record. + */ + protected Data payloadData; + + /** + * Parse a given record based on the full message data and the current + * stream position. + * @param dis The DataInputStream positioned at the first record byte. + * @param data The full message data. + * @throws IOException In case of malformed replies. + */ + public void parse(DataInputStream dis, byte[] data) throws IOException { + this.name = NameUtil.parse(dis, data); + this.type = TYPE.getType(dis.readUnsignedShort()); + this.clazz = CLASS.getClass(dis.readUnsignedShort()); + this.ttl = (((long)dis.readUnsignedShort()) << 32) + + dis.readUnsignedShort(); + int payloadLength = dis.readUnsignedShort(); + switch (this.type) { + case SRV: + this.payloadData = new SRV(); + break; + case AAAA: + this.payloadData = new AAAA(); + break; + case A: + this.payloadData = new A(); + break; + case NS: + this.payloadData = new NS(); + break; + case CNAME: + this.payloadData = new CNAME(); + break; + default: + System.out.println("Unparsed type " + type); + this.payloadData = null; + for (int i = 0; i < payloadLength; i++) { + dis.readByte(); + } + break; + } + if (this.payloadData != null) { + this.payloadData.parse(dis, data, payloadLength); + } + } + + /** + * Retrieve a textual representation of this resource record. + * @return String + */ + @Override + public String toString() { + if (payloadData == null) { + return "RR " + type + "/" + clazz; + } + return "RR " + type + "/" + clazz + ": " + payloadData.toString(); + }; + + /** + * Check if this record answers a given query. + * @param q The query. + * @return True if this record is a valid answer. + */ + public boolean isAnswer(Question q) { + return ((q.getType() == type) || (q.getType() == TYPE.ANY)) && + ((q.getClazz() == clazz) || (q.getClazz() == CLASS.ANY)) && + (q.getName().equals(name)); + } + + public Data getPayload() { + return payloadData; + } + +} diff --git a/src/de/measite/minidns/record/A.java b/src/de/measite/minidns/record/A.java new file mode 100644 index 00000000..a85a7af0 --- /dev/null +++ b/src/de/measite/minidns/record/A.java @@ -0,0 +1,37 @@ +package de.measite.minidns.record; + +import java.io.DataInputStream; +import java.io.IOException; + +import de.measite.minidns.Record.TYPE; + +public class A implements Data { + + private byte[] ip; + + @Override + public TYPE getType() { + return TYPE.A; + } + + @Override + public byte[] toByteArray() { + return ip; + } + + @Override + public void parse(DataInputStream dis, byte[] data, int length) + throws IOException { + ip = new byte[4]; + dis.readFully(ip); + } + + @Override + public String toString() { + return Integer.toString(ip[0] & 0xff) + "." + + Integer.toString(ip[1] & 0xff) + "." + + Integer.toString(ip[2] & 0xff) + "." + + Integer.toString(ip[3] & 0xff); + } + +} diff --git a/src/de/measite/minidns/record/AAAA.java b/src/de/measite/minidns/record/AAAA.java new file mode 100644 index 00000000..d89147b2 --- /dev/null +++ b/src/de/measite/minidns/record/AAAA.java @@ -0,0 +1,43 @@ +package de.measite.minidns.record; + +import java.io.DataInputStream; +import java.io.IOException; + +import de.measite.minidns.Record.TYPE; + +public class AAAA implements Data { + + private byte[] ip; + + @Override + public TYPE getType() { + return TYPE.AAAA; + } + + @Override + public byte[] toByteArray() { + return ip; + } + + @Override + public void parse(DataInputStream dis, byte[] data, int length) + throws IOException { + ip = new byte[16]; + dis.readFully(ip); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < ip.length; i += 2) { + if (i != 0) { + sb.append(':'); + } + sb.append(Integer.toHexString( + ((ip[i] & 0xff) << 8) + (ip[i + 1] & 0xff) + )); + } + return sb.toString(); + } + +} diff --git a/src/de/measite/minidns/record/CNAME.java b/src/de/measite/minidns/record/CNAME.java new file mode 100644 index 00000000..4657b4a5 --- /dev/null +++ b/src/de/measite/minidns/record/CNAME.java @@ -0,0 +1,44 @@ +package de.measite.minidns.record; + +import java.io.DataInputStream; +import java.io.IOException; + +import de.measite.minidns.Record.TYPE; +import de.measite.minidns.util.NameUtil; + +public class CNAME implements Data { + + protected String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public byte[] toByteArray() { + // TODO Auto-generated method stub + return null; + } + + @Override + public void parse(DataInputStream dis, byte[] data, int length) + throws IOException + { + this.name = NameUtil.parse(dis, data); + } + + @Override + public TYPE getType() { + return TYPE.CNAME; + } + + @Override + public String toString() { + return "to \"" + name + "\""; + } + +} diff --git a/src/de/measite/minidns/record/Data.java b/src/de/measite/minidns/record/Data.java new file mode 100644 index 00000000..9cb80374 --- /dev/null +++ b/src/de/measite/minidns/record/Data.java @@ -0,0 +1,16 @@ +package de.measite.minidns.record; + +import java.io.DataInputStream; +import java.io.IOException; + +import de.measite.minidns.Record.TYPE; + +public interface Data { + + TYPE getType(); + + byte[] toByteArray(); + + void parse(DataInputStream dis, byte data[], int length) throws IOException; + +} diff --git a/src/de/measite/minidns/record/NS.java b/src/de/measite/minidns/record/NS.java new file mode 100644 index 00000000..bf07e8c4 --- /dev/null +++ b/src/de/measite/minidns/record/NS.java @@ -0,0 +1,12 @@ +package de.measite.minidns.record; + +import de.measite.minidns.Record.TYPE; + +public class NS extends CNAME { + + @Override + public TYPE getType() { + return TYPE.NS; + } + +} diff --git a/src/de/measite/minidns/record/SRV.java b/src/de/measite/minidns/record/SRV.java new file mode 100644 index 00000000..32b70c4d --- /dev/null +++ b/src/de/measite/minidns/record/SRV.java @@ -0,0 +1,74 @@ +package de.measite.minidns.record; + +import java.io.DataInputStream; +import java.io.IOException; + +import de.measite.minidns.Record.TYPE; +import de.measite.minidns.util.NameUtil; + +public class SRV implements Data { + + protected int priority; + protected int weight; + protected int port; + protected String name; + + public int getPriority() { + return priority; + } + + public void setPriority(int priority) { + this.priority = priority; + } + + public int getWeight() { + return weight; + } + + public void setWeight(int weight) { + this.weight = weight; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public byte[] toByteArray() { + // TODO Auto-generated method stub + return null; + } + + @Override + public void parse(DataInputStream dis, byte[] data, int length) + throws IOException + { + this.priority = dis.readUnsignedShort(); + this.weight = dis.readUnsignedShort(); + this.port = dis.readUnsignedShort(); + this.name = NameUtil.parse(dis, data); + } + + @Override + public String toString() { + return "SRV " + name + ":" + port + " p:" + priority + " w:" + weight; + } + + @Override + public TYPE getType() { + return TYPE.SRV; + } + +} diff --git a/src/de/measite/minidns/util/NameUtil.java b/src/de/measite/minidns/util/NameUtil.java new file mode 100644 index 00000000..c42ac3eb --- /dev/null +++ b/src/de/measite/minidns/util/NameUtil.java @@ -0,0 +1,77 @@ +package de.measite.minidns.util; + +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.net.IDN; +import java.util.HashSet; + +public class NameUtil { + + public static int size(String name) { + return name.length() + 2; + } + + public static byte[] toByteArray(String name) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(64); + DataOutputStream dos = new DataOutputStream(baos); + for (String s: name.split("[.\u3002\uFF0E\uFF61]")) { + byte[] buffer = IDN.toASCII(s).getBytes(); + dos.writeByte(buffer.length); + dos.write(buffer); + } + dos.writeByte(0); + dos.flush(); + return baos.toByteArray(); + } + + public static String parse(DataInputStream dis, byte data[]) + throws IOException + { + int c = dis.readUnsignedByte(); + if ((c & 0xc0) == 0xc0) { + c = ((c & 0x3f) << 8) + dis.readUnsignedByte(); + HashSet jumps = new HashSet(); + jumps.add(c); + return parse(data, c, jumps); + } + if (c == 0) { + return ""; + } + byte b[] = new byte[c]; + dis.readFully(b); + String s = IDN.toUnicode(new String(b)); + String t = parse(dis, data); + if (t.length() > 0) { + s = s + "." + t; + } + return s; + } + + public static String parse( + byte data[], + int offset, + HashSet jumps + ) { + int c = data[offset] & 0xff; + if ((c & 0xc0) == 0xc0) { + c = ((c & 0x3f) << 8) + data[offset + 1]; + if (jumps.contains(c)) { + throw new IllegalStateException("Cyclic offsets detected."); + } + jumps.add(c); + return parse(data, c, jumps); + } + if (c == 0) { + return ""; + } + String s = new String(data,offset + 1, c); + String t = parse(data, offset + 1 + c, jumps); + if (t.length() > 0) { + s = s + "." + t; + } + return s; + } + +} -- cgit v1.2.3 From d5cca3b3a48cfaed3008050fcab9574d97d771cc Mon Sep 17 00:00:00 2001 From: Rene Treffer Date: Wed, 2 Apr 2014 22:45:57 +0200 Subject: Try various ways to get the dns settings --- src/de/measite/minidns/Client.java | 91 +++++++++++++++++++++++++++++++++++++- 1 file changed, 90 insertions(+), 1 deletion(-) diff --git a/src/de/measite/minidns/Client.java b/src/de/measite/minidns/Client.java index c32ec3a1..beb461be 100644 --- a/src/de/measite/minidns/Client.java +++ b/src/de/measite/minidns/Client.java @@ -4,14 +4,18 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.LineNumberReader; +import java.lang.reflect.Method; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Arrays; import java.util.HashSet; import java.util.Random; +import android.util.Log; import de.measite.minidns.Record.CLASS; import de.measite.minidns.Record.TYPE; @@ -130,6 +134,33 @@ public class Client { * @return The server array. */ public String[] findDNS() { + String[] result = findDNSByReflection(); + if (result != null) { + Log.d("minidns/client", + "Got DNS servers via reflection: " + Arrays.toString(result)); + return result; + } + + result = findDNSByExec(); + if (result != null) { + Log.d("minidns/client", + "Got DNS servers via exec: " + Arrays.toString(result)); + return result; + } + + // fallback for ipv4 and ipv6 connectivity + // see https://developers.google.com/speed/public-dns/docs/using + Log.d("minidns/client", + "No DNS found? Using fallback [8.8.8.8, [2001:4860:4860::8888]]"); + + return new String[]{"8.8.8.8", "[2001:4860:4860::8888]"}; + } + + /** + * Try to retrieve the list of dns server by executing getprop. + * @return Array of servers, or null on failure. + */ + protected String[] findDNSByExec() { try { Process process = Runtime.getRuntime().exec("getprop"); InputStream inputStream = process.getInputStream(); @@ -146,7 +177,19 @@ public class Client { String value = line.substring(split + 4, line.length() - 1); if (property.endsWith(".dns") || property.endsWith(".dns1") || property.endsWith(".dns2") || property.endsWith(".dns3") || - property.endsWith(".dns4")) { + property.endsWith(".dns4")) { + + // normalize the address + + InetAddress ip = InetAddress.getByName(value); + + if (ip == null) continue; + + value = ip.getHostAddress(); + + if (value == null) continue; + if (value.length() == 0) continue; + server.add(value); } } @@ -158,4 +201,50 @@ public class Client { } return null; } + + /** + * Try to retrieve the list of dns server by calling SystemProperties. + * @return Array of servers, or null on failure. + */ + protected String[] findDNSByReflection() { + try { + Class SystemProperties = + Class.forName("android.os.SystemProperties"); + Method method = SystemProperties.getMethod("get", + new Class[] { String.class }); + + ArrayList servers = new ArrayList(5); + + for (String propKey : new String[] { + "net.dns1", "net.dns2", "net.dns3", "net.dns4"}) { + + String value = (String)method.invoke(null, propKey); + + if (value == null) continue; + if (value.length() == 0) continue; + if (servers.contains(value)) continue; + + InetAddress ip = InetAddress.getByName(value); + + if (ip == null) continue; + + value = ip.getHostAddress(); + + if (value == null) continue; + if (value.length() == 0) continue; + if (servers.contains(value)) continue; + + servers.add(value); + } + + if (servers.size() > 0) { + return servers.toArray(new String[servers.size()]); + } + } catch (Exception e) { + // we might trigger some problems this way + e.printStackTrace(); + } + return null; + } + } -- cgit v1.2.3 From 7d086eaf1a6a0b1d45e857eaf65ac53cd7a753e5 Mon Sep 17 00:00:00 2001 From: Rene Treffer Date: Thu, 3 Apr 2014 10:05:32 +0200 Subject: Export the record name --- src/de/measite/minidns/Record.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/de/measite/minidns/Record.java b/src/de/measite/minidns/Record.java index adfa9713..fb0b5d5f 100644 --- a/src/de/measite/minidns/Record.java +++ b/src/de/measite/minidns/Record.java @@ -286,6 +286,10 @@ public class Record { (q.getName().equals(name)); } + public String getName() { + return name; + } + public Data getPayload() { return payloadData; } -- cgit v1.2.3 From 8a8a7f3b0dd7928802a97e346b307bb06fdb3322 Mon Sep 17 00:00:00 2001 From: Rene Treffer Date: Thu, 3 Apr 2014 10:28:24 +0200 Subject: Add an IDN equals method to check names --- src/de/measite/minidns/util/NameUtil.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/de/measite/minidns/util/NameUtil.java b/src/de/measite/minidns/util/NameUtil.java index c42ac3eb..a7edc22a 100644 --- a/src/de/measite/minidns/util/NameUtil.java +++ b/src/de/measite/minidns/util/NameUtil.java @@ -6,6 +6,7 @@ import java.io.DataOutputStream; import java.io.IOException; import java.net.IDN; import java.util.HashSet; +import java.util.Arrays; public class NameUtil { @@ -13,6 +14,19 @@ public class NameUtil { return name.length() + 2; } + public static boolean idnEquals(String name1, String name2) { + if (name1 == name2) return true; // catches null, null + if (name1 == null) return false; + if (name2 == null) return false; + if (name1.equals(name2)) return true; + + try { + return Arrays.equals(toByteArray(name1),toByteArray(name2)); + } catch (IOException e) { + return false; // impossible + } + } + public static byte[] toByteArray(String name) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(64); DataOutputStream dos = new DataOutputStream(baos); -- cgit v1.2.3 From 921674b3f1585dde76d13f181abcfc6d49bd737d Mon Sep 17 00:00:00 2001 From: Rene Treffer Date: Thu, 3 Apr 2014 10:51:05 +0200 Subject: Fix negative jump targets --- src/de/measite/minidns/util/NameUtil.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/de/measite/minidns/util/NameUtil.java b/src/de/measite/minidns/util/NameUtil.java index a7edc22a..91a6649d 100644 --- a/src/de/measite/minidns/util/NameUtil.java +++ b/src/de/measite/minidns/util/NameUtil.java @@ -70,7 +70,7 @@ public class NameUtil { ) { int c = data[offset] & 0xff; if ((c & 0xc0) == 0xc0) { - c = ((c & 0x3f) << 8) + data[offset + 1]; + c = ((c & 0x3f) << 8) + (data[offset + 1] & 0xff); if (jumps.contains(c)) { throw new IllegalStateException("Cyclic offsets detected."); } -- cgit v1.2.3 From f1d50a5380f355a64913ba19f717debd8c40ab30 Mon Sep 17 00:00:00 2001 From: Rene Treffer Date: Thu, 3 Apr 2014 16:12:08 +0200 Subject: AndroidManifest.xml defines minSDK 9, set target sdk to the current one (19) --- build.gradle | 2 +- project.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 2a17c6ae..ae9c7693 100644 --- a/build.gradle +++ b/build.gradle @@ -11,7 +11,7 @@ buildscript { apply plugin: 'android-library' android { - compileSdkVersion 9 + compileSdkVersion 19 buildToolsVersion '19.0.3' // NOTE: We are using the old folder structure to also support Eclipse diff --git a/project.properties b/project.properties index 7096c5ce..9e86aa64 100644 --- a/project.properties +++ b/project.properties @@ -1,2 +1,2 @@ -target=android-9 +target=android-19 android.library=true -- cgit v1.2.3 From 114bda4d3fde90d5c824884ce6b3f8ada846b435 Mon Sep 17 00:00:00 2001 From: Rene Treffer Date: Thu, 3 Apr 2014 16:13:30 +0200 Subject: Fix name in build.xml --- build.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.xml b/build.xml index ed7657f2..e10480a0 100644 --- a/build.xml +++ b/build.xml @@ -1,5 +1,5 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/proguard-project.txt b/proguard-project.txt new file mode 100644 index 00000000..f2fe1559 --- /dev/null +++ b/proguard-project.txt @@ -0,0 +1,20 @@ +# To enable ProGuard in your project, edit project.properties +# to define the proguard.config property as described in that file. +# +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in ${sdk.dir}/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the ProGuard +# include property in project.properties. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/project.properties b/project.properties new file mode 100644 index 00000000..91d2b024 --- /dev/null +++ b/project.properties @@ -0,0 +1,15 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system edit +# "ant.properties", and override values to adapt the script to your +# project structure. +# +# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): +#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt + +# Project target. +target=android-19 +android.library=true diff --git a/res/drawable-hdpi/ic_action_cancel_launchersize.png b/res/drawable-hdpi/ic_action_cancel_launchersize.png new file mode 100644 index 00000000..71b9118d Binary files /dev/null and b/res/drawable-hdpi/ic_action_cancel_launchersize.png differ diff --git a/res/drawable-hdpi/ic_action_cancel_launchersize_light.png b/res/drawable-hdpi/ic_action_cancel_launchersize_light.png new file mode 100644 index 00000000..73b1d08f Binary files /dev/null and b/res/drawable-hdpi/ic_action_cancel_launchersize_light.png differ diff --git a/res/drawable-mdpi/ic_action_cancel_launchersize.png b/res/drawable-mdpi/ic_action_cancel_launchersize.png new file mode 100644 index 00000000..270abf45 Binary files /dev/null and b/res/drawable-mdpi/ic_action_cancel_launchersize.png differ diff --git a/res/drawable-mdpi/ic_action_cancel_launchersize_light.png b/res/drawable-mdpi/ic_action_cancel_launchersize_light.png new file mode 100644 index 00000000..d841821c Binary files /dev/null and b/res/drawable-mdpi/ic_action_cancel_launchersize_light.png differ diff --git a/res/drawable-xhdpi/ic_action_cancel_launchersize.png b/res/drawable-xhdpi/ic_action_cancel_launchersize.png new file mode 100644 index 00000000..1e3571fa Binary files /dev/null and b/res/drawable-xhdpi/ic_action_cancel_launchersize.png differ diff --git a/res/drawable-xhdpi/ic_action_cancel_launchersize_light.png b/res/drawable-xhdpi/ic_action_cancel_launchersize_light.png new file mode 100644 index 00000000..d505046b Binary files /dev/null and b/res/drawable-xhdpi/ic_action_cancel_launchersize_light.png differ diff --git a/res/drawable-xxhdpi/ic_action_cancel_launchersize.png b/res/drawable-xxhdpi/ic_action_cancel_launchersize.png new file mode 100644 index 00000000..52044601 Binary files /dev/null and b/res/drawable-xxhdpi/ic_action_cancel_launchersize.png differ diff --git a/res/drawable-xxhdpi/ic_action_cancel_launchersize_light.png b/res/drawable-xxhdpi/ic_action_cancel_launchersize_light.png new file mode 100644 index 00000000..d6fb86bd Binary files /dev/null and b/res/drawable-xxhdpi/ic_action_cancel_launchersize_light.png differ diff --git a/res/values/strings.xml b/res/values/strings.xml new file mode 100644 index 00000000..0119831c --- /dev/null +++ b/res/values/strings.xml @@ -0,0 +1,7 @@ + + + + None + Install OpenKeychain via %s + + \ No newline at end of file diff --git a/src/org/openintents/openpgp/IOpenPgpService.aidl b/src/org/openintents/openpgp/IOpenPgpService.aidl new file mode 100644 index 00000000..7ee79d6a --- /dev/null +++ b/src/org/openintents/openpgp/IOpenPgpService.aidl @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2014 Dominik Schürmann + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openintents.openpgp; + +interface IOpenPgpService { + + // see OpenPgpApi for documentation + Intent execute(in Intent data, in ParcelFileDescriptor input, in ParcelFileDescriptor output); + +} \ No newline at end of file diff --git a/src/org/openintents/openpgp/OpenPgpError.java b/src/org/openintents/openpgp/OpenPgpError.java new file mode 100644 index 00000000..b894a460 --- /dev/null +++ b/src/org/openintents/openpgp/OpenPgpError.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2014 Dominik Schürmann + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openintents.openpgp; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Parcelable versioning has been copied from Dashclock Widget + * https://code.google.com/p/dashclock/source/browse/api/src/main/java/com/google/android/apps/dashclock/api/ExtensionData.java + */ +public class OpenPgpError implements Parcelable { + /** + * Since there might be a case where new versions of the client using the library getting + * old versions of the protocol (and thus old versions of this class), we need a versioning + * system for the parcels sent between the clients and the providers. + */ + public static final int PARCELABLE_VERSION = 1; + + // possible values for errorId + public static final int CLIENT_SIDE_ERROR = -1; + public static final int GENERIC_ERROR = 0; + public static final int INCOMPATIBLE_API_VERSIONS = 1; + public static final int NO_OR_WRONG_PASSPHRASE = 2; + public static final int NO_USER_IDS = 3; + + int errorId; + String message; + + public OpenPgpError() { + } + + public OpenPgpError(int errorId, String message) { + this.errorId = errorId; + this.message = message; + } + + public OpenPgpError(OpenPgpError b) { + this.errorId = b.errorId; + this.message = b.message; + } + + public int getErrorId() { + return errorId; + } + + public void setErrorId(int errorId) { + this.errorId = errorId; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + /** + * NOTE: When adding fields in the process of updating this API, make sure to bump + * {@link #PARCELABLE_VERSION}. + */ + dest.writeInt(PARCELABLE_VERSION); + // Inject a placeholder that will store the parcel size from this point on + // (not including the size itself). + int sizePosition = dest.dataPosition(); + dest.writeInt(0); + int startPosition = dest.dataPosition(); + // version 1 + dest.writeInt(errorId); + dest.writeString(message); + // Go back and write the size + int parcelableSize = dest.dataPosition() - startPosition; + dest.setDataPosition(sizePosition); + dest.writeInt(parcelableSize); + dest.setDataPosition(startPosition + parcelableSize); + } + + public static final Creator CREATOR = new Creator() { + public OpenPgpError createFromParcel(final Parcel source) { + int parcelableVersion = source.readInt(); + int parcelableSize = source.readInt(); + int startPosition = source.dataPosition(); + + OpenPgpError error = new OpenPgpError(); + error.errorId = source.readInt(); + error.message = source.readString(); + + // skip over all fields added in future versions of this parcel + source.setDataPosition(startPosition + parcelableSize); + + return error; + } + + public OpenPgpError[] newArray(final int size) { + return new OpenPgpError[size]; + } + }; +} diff --git a/src/org/openintents/openpgp/OpenPgpSignatureResult.java b/src/org/openintents/openpgp/OpenPgpSignatureResult.java new file mode 100644 index 00000000..7a4d799d --- /dev/null +++ b/src/org/openintents/openpgp/OpenPgpSignatureResult.java @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2014 Dominik Schürmann + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openintents.openpgp; + +import android.os.Parcel; +import android.os.Parcelable; + +import org.openintents.openpgp.util.OpenPgpUtils; + +import java.util.Locale; + +/** + * Parcelable versioning has been copied from Dashclock Widget + * https://code.google.com/p/dashclock/source/browse/api/src/main/java/com/google/android/apps/dashclock/api/ExtensionData.java + */ +public class OpenPgpSignatureResult implements Parcelable { + /** + * Since there might be a case where new versions of the client using the library getting + * old versions of the protocol (and thus old versions of this class), we need a versioning + * system for the parcels sent between the clients and the providers. + */ + public static final int PARCELABLE_VERSION = 1; + + // generic error on signature verification + public static final int SIGNATURE_ERROR = 0; + // successfully verified signature, with certified public key + public static final int SIGNATURE_SUCCESS_CERTIFIED = 1; + // no public key was found for this signature verification + public static final int SIGNATURE_UNKNOWN_PUB_KEY = 2; + // successfully verified signature, but with uncertified public key + public static final int SIGNATURE_SUCCESS_UNCERTIFIED = 3; + + int status; + boolean signatureOnly; + String userId; + long keyId; + + public int getStatus() { + return status; + } + + public void setStatus(int status) { + this.status = status; + } + + public boolean isSignatureOnly() { + return signatureOnly; + } + + public void setSignatureOnly(boolean signatureOnly) { + this.signatureOnly = signatureOnly; + } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public long getKeyId() { + return keyId; + } + + public void setKeyId(long keyId) { + this.keyId = keyId; + } + + public OpenPgpSignatureResult() { + + } + + public OpenPgpSignatureResult(int signatureStatus, String signatureUserId, + boolean signatureOnly, long keyId) { + this.status = signatureStatus; + this.signatureOnly = signatureOnly; + this.userId = signatureUserId; + this.keyId = keyId; + } + + public OpenPgpSignatureResult(OpenPgpSignatureResult b) { + this.status = b.status; + this.userId = b.userId; + this.signatureOnly = b.signatureOnly; + this.keyId = b.keyId; + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + /** + * NOTE: When adding fields in the process of updating this API, make sure to bump + * {@link #PARCELABLE_VERSION}. + */ + dest.writeInt(PARCELABLE_VERSION); + // Inject a placeholder that will store the parcel size from this point on + // (not including the size itself). + int sizePosition = dest.dataPosition(); + dest.writeInt(0); + int startPosition = dest.dataPosition(); + // version 1 + dest.writeInt(status); + dest.writeByte((byte) (signatureOnly ? 1 : 0)); + dest.writeString(userId); + dest.writeLong(keyId); + // Go back and write the size + int parcelableSize = dest.dataPosition() - startPosition; + dest.setDataPosition(sizePosition); + dest.writeInt(parcelableSize); + dest.setDataPosition(startPosition + parcelableSize); + } + + public static final Creator CREATOR = new Creator() { + public OpenPgpSignatureResult createFromParcel(final Parcel source) { + int parcelableVersion = source.readInt(); + int parcelableSize = source.readInt(); + int startPosition = source.dataPosition(); + + OpenPgpSignatureResult vr = new OpenPgpSignatureResult(); + vr.status = source.readInt(); + vr.signatureOnly = source.readByte() == 1; + vr.userId = source.readString(); + vr.keyId = source.readLong(); + + // skip over all fields added in future versions of this parcel + source.setDataPosition(startPosition + parcelableSize); + + return vr; + } + + public OpenPgpSignatureResult[] newArray(final int size) { + return new OpenPgpSignatureResult[size]; + } + }; + + @Override + public String toString() { + String out = new String(); + out += "\nstatus: " + status; + out += "\nuserId: " + userId; + out += "\nsignatureOnly: " + signatureOnly; + out += "\nkeyId: " + OpenPgpUtils.convertKeyIdToHex(keyId); + return out; + } + +} diff --git a/src/org/openintents/openpgp/util/OpenPgpApi.java b/src/org/openintents/openpgp/util/OpenPgpApi.java new file mode 100644 index 00000000..f6a78d0e --- /dev/null +++ b/src/org/openintents/openpgp/util/OpenPgpApi.java @@ -0,0 +1,270 @@ +/* + * Copyright (C) 2014 Dominik Schürmann + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openintents.openpgp.util; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.Intent; +import android.os.AsyncTask; +import android.os.Build; +import android.os.ParcelFileDescriptor; +import android.util.Log; + +import org.openintents.openpgp.IOpenPgpService; +import org.openintents.openpgp.OpenPgpError; + +import java.io.InputStream; +import java.io.OutputStream; + +public class OpenPgpApi { + + public static final String TAG = "OpenPgp API"; + + public static final int API_VERSION = 3; + public static final String SERVICE_INTENT = "org.openintents.openpgp.IOpenPgpService"; + + /** + * General extras + * -------------- + * + * required extras: + * int EXTRA_API_VERSION (always required) + * + * returned extras: + * int RESULT_CODE (RESULT_CODE_ERROR, RESULT_CODE_SUCCESS or RESULT_CODE_USER_INTERACTION_REQUIRED) + * OpenPgpError RESULT_ERROR (if RESULT_CODE == RESULT_CODE_ERROR) + * PendingIntent RESULT_INTENT (if RESULT_CODE == RESULT_CODE_USER_INTERACTION_REQUIRED) + */ + + /** + * Sign only + *

+ * optional extras: + * boolean EXTRA_REQUEST_ASCII_ARMOR (request ascii armor for ouput) + * String EXTRA_PASSPHRASE (key passphrase) + */ + public static final String ACTION_SIGN = "org.openintents.openpgp.action.SIGN"; + + /** + * Encrypt + *

+ * required extras: + * String[] EXTRA_USER_IDS (=emails of recipients, if more than one key has a user_id, a PendingIntent is returned via RESULT_INTENT) + * or + * long[] EXTRA_KEY_IDS + *

+ * optional extras: + * boolean EXTRA_REQUEST_ASCII_ARMOR (request ascii armor for ouput) + * String EXTRA_PASSPHRASE (key passphrase) + */ + public static final String ACTION_ENCRYPT = "org.openintents.openpgp.action.ENCRYPT"; + + /** + * Sign and encrypt + *

+ * required extras: + * String[] EXTRA_USER_IDS (=emails of recipients, if more than one key has a user_id, a PendingIntent is returned via RESULT_INTENT) + * or + * long[] EXTRA_KEY_IDS + *

+ * optional extras: + * boolean EXTRA_REQUEST_ASCII_ARMOR (request ascii armor for ouput) + * String EXTRA_PASSPHRASE (key passphrase) + */ + public static final String ACTION_SIGN_AND_ENCRYPT = "org.openintents.openpgp.action.SIGN_AND_ENCRYPT"; + + /** + * Decrypts and verifies given input stream. This methods handles encrypted-only, signed-and-encrypted, + * and also signed-only input. + *

+ * If OpenPgpSignatureResult.getStatus() == OpenPgpSignatureResult.SIGNATURE_UNKNOWN_PUB_KEY + * in addition a PendingIntent is returned via RESULT_INTENT to download missing keys. + *

+ * optional extras: + * boolean EXTRA_REQUEST_ASCII_ARMOR (request ascii armor for ouput) + *

+ * returned extras: + * OpenPgpSignatureResult RESULT_SIGNATURE + */ + public static final String ACTION_DECRYPT_VERIFY = "org.openintents.openpgp.action.DECRYPT_VERIFY"; + + /** + * Get key ids based on given user ids (=emails) + *

+ * required extras: + * String[] EXTRA_USER_IDS + *

+ * returned extras: + * long[] RESULT_KEY_IDS + */ + public static final String ACTION_GET_KEY_IDS = "org.openintents.openpgp.action.GET_KEY_IDS"; + + /** + * This action returns RESULT_CODE_SUCCESS if the OpenPGP Provider already has the key + * corresponding to the given key id in its database. + *

+ * It returns RESULT_CODE_USER_INTERACTION_REQUIRED if the Provider does not have the key. + * The PendingIntent from RESULT_INTENT can be used to retrieve those from a keyserver. + *

+ * required extras: + * long EXTRA_KEY_ID + */ + public static final String ACTION_GET_KEY = "org.openintents.openpgp.action.GET_KEY"; + + /* Intent extras */ + public static final String EXTRA_API_VERSION = "api_version"; + + public static final String EXTRA_ACCOUNT_NAME = "account_name"; + + // SIGN, ENCRYPT, SIGN_AND_ENCRYPT, DECRYPT_VERIFY + // request ASCII Armor for output + // OpenPGP Radix-64, 33 percent overhead compared to binary, see http://tools.ietf.org/html/rfc4880#page-53) + public static final String EXTRA_REQUEST_ASCII_ARMOR = "ascii_armor"; + + // ENCRYPT, SIGN_AND_ENCRYPT + public static final String EXTRA_USER_IDS = "user_ids"; + public static final String EXTRA_KEY_IDS = "key_ids"; + // optional extras: + public static final String EXTRA_PASSPHRASE = "passphrase"; + + // GET_KEY + public static final String EXTRA_KEY_ID = "key_id"; + public static final String RESULT_KEY_IDS = "key_ids"; + + /* Service Intent returns */ + public static final String RESULT_CODE = "result_code"; + + // get actual error object from RESULT_ERROR + public static final int RESULT_CODE_ERROR = 0; + // success! + public static final int RESULT_CODE_SUCCESS = 1; + // get PendingIntent from RESULT_INTENT, start PendingIntent with startIntentSenderForResult, + // and execute service method again in onActivityResult + public static final int RESULT_CODE_USER_INTERACTION_REQUIRED = 2; + + public static final String RESULT_ERROR = "error"; + public static final String RESULT_INTENT = "intent"; + + // DECRYPT_VERIFY + public static final String RESULT_SIGNATURE = "signature"; + + IOpenPgpService mService; + Context mContext; + + public OpenPgpApi(Context context, IOpenPgpService service) { + this.mContext = context; + this.mService = service; + } + + public interface IOpenPgpCallback { + void onReturn(final Intent result); + } + + private class OpenPgpAsyncTask extends AsyncTask { + Intent data; + InputStream is; + OutputStream os; + IOpenPgpCallback callback; + + private OpenPgpAsyncTask(Intent data, InputStream is, OutputStream os, IOpenPgpCallback callback) { + this.data = data; + this.is = is; + this.os = os; + this.callback = callback; + } + + @Override + protected Intent doInBackground(Void... unused) { + return executeApi(data, is, os); + } + + protected void onPostExecute(Intent result) { + callback.onReturn(result); + } + + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public void executeApiAsync(Intent data, InputStream is, OutputStream os, IOpenPgpCallback callback) { + OpenPgpAsyncTask task = new OpenPgpAsyncTask(data, is, os, callback); + + // don't serialize async tasks! + // http://commonsware.com/blog/2012/04/20/asynctask-threading-regression-confirmed.html + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null); + } else { + task.execute((Void[]) null); + } + } + + public Intent executeApi(Intent data, InputStream is, OutputStream os) { + try { + data.putExtra(EXTRA_API_VERSION, OpenPgpApi.API_VERSION); + + Intent result; + + // pipe the input and output + ParcelFileDescriptor input = null; + if (is != null) { + input = ParcelFileDescriptorUtil.pipeFrom(is, + new ParcelFileDescriptorUtil.IThreadListener() { + + @Override + public void onThreadFinished(Thread thread) { + //Log.d(OpenPgpApi.TAG, "Copy to service finished"); + } + } + ); + } + ParcelFileDescriptor output = null; + if (os != null) { + output = ParcelFileDescriptorUtil.pipeTo(os, + new ParcelFileDescriptorUtil.IThreadListener() { + + @Override + public void onThreadFinished(Thread thread) { + //Log.d(OpenPgpApi.TAG, "Service finished writing!"); + } + } + ); + } + + // blocks until result is ready + result = mService.execute(data, input, output); + // close() is required to halt the TransferThread + if (output != null) { + output.close(); + } + // TODO: close input? + + // set class loader to current context to allow unparcelling + // of OpenPgpError and OpenPgpSignatureResult + // http://stackoverflow.com/a/3806769 + result.setExtrasClassLoader(mContext.getClassLoader()); + + return result; + } catch (Exception e) { + Log.e(OpenPgpApi.TAG, "Exception in executeApi call", e); + Intent result = new Intent(); + result.putExtra(RESULT_CODE, RESULT_CODE_ERROR); + result.putExtra(RESULT_ERROR, + new OpenPgpError(OpenPgpError.CLIENT_SIDE_ERROR, e.getMessage())); + return result; + } + } + +} diff --git a/src/org/openintents/openpgp/util/OpenPgpListPreference.java b/src/org/openintents/openpgp/util/OpenPgpListPreference.java new file mode 100644 index 00000000..cf586462 --- /dev/null +++ b/src/org/openintents/openpgp/util/OpenPgpListPreference.java @@ -0,0 +1,257 @@ +/* + * Copyright (C) 2014 Dominik Schürmann + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openintents.openpgp.util; + +import android.app.AlertDialog.Builder; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.ResolveInfo; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.preference.DialogPreference; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ListAdapter; +import android.widget.TextView; +import org.openintents.openpgp.R; + +import java.util.ArrayList; +import java.util.List; + +/** + * Does not extend ListPreference, but is very similar to it! + * http://grepcode.com/file_/repository.grepcode.com/java/ext/com.google.android/android/4.4_r1/android/preference/ListPreference.java/?v=source + */ +public class OpenPgpListPreference extends DialogPreference { + private static final String OPENKEYCHAIN_PACKAGE = "org.sufficientlysecure.keychain"; + private static final String MARKET_INTENT_URI_BASE = "market://details?id=%s"; + private static final Intent MARKET_INTENT = new Intent(Intent.ACTION_VIEW, Uri.parse( + String.format(MARKET_INTENT_URI_BASE, OPENKEYCHAIN_PACKAGE))); + + private ArrayList mLegacyList = new ArrayList(); + private ArrayList mList = new ArrayList(); + + private String mSelectedPackage; + + public OpenPgpListPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public OpenPgpListPreference(Context context) { + this(context, null); + } + + /** + * Public method to add new entries for legacy applications + * + * @param packageName + * @param simpleName + * @param icon + */ + public void addLegacyProvider(int position, String packageName, String simpleName, Drawable icon) { + mLegacyList.add(position, new OpenPgpProviderEntry(packageName, simpleName, icon)); + } + + @Override + protected void onPrepareDialogBuilder(Builder builder) { + mList.clear(); + + // add "none"-entry + mList.add(0, new OpenPgpProviderEntry("", + getContext().getString(R.string.openpgp_list_preference_none), + getContext().getResources().getDrawable(R.drawable.ic_action_cancel_launchersize))); + + // add all additional (legacy) providers + mList.addAll(mLegacyList); + + // search for OpenPGP providers... + ArrayList providerList = new ArrayList(); + Intent intent = new Intent(OpenPgpApi.SERVICE_INTENT); + List resInfo = getContext().getPackageManager().queryIntentServices(intent, 0); + if (!resInfo.isEmpty()) { + for (ResolveInfo resolveInfo : resInfo) { + if (resolveInfo.serviceInfo == null) + continue; + + String packageName = resolveInfo.serviceInfo.packageName; + String simpleName = String.valueOf(resolveInfo.serviceInfo.loadLabel(getContext() + .getPackageManager())); + Drawable icon = resolveInfo.serviceInfo.loadIcon(getContext().getPackageManager()); + + providerList.add(new OpenPgpProviderEntry(packageName, simpleName, icon)); + } + } + + if (providerList.isEmpty()) { + // add install links if provider list is empty + resInfo = getContext().getPackageManager().queryIntentActivities + (MARKET_INTENT, 0); + for (ResolveInfo resolveInfo : resInfo) { + Intent marketIntent = new Intent(MARKET_INTENT); + marketIntent.setPackage(resolveInfo.activityInfo.packageName); + Drawable icon = resolveInfo.activityInfo.loadIcon(getContext().getPackageManager()); + String marketName = String.valueOf(resolveInfo.activityInfo.applicationInfo + .loadLabel(getContext().getPackageManager())); + String simpleName = String.format(getContext().getString(R.string + .openpgp_install_openkeychain_via), marketName); + mList.add(new OpenPgpProviderEntry(OPENKEYCHAIN_PACKAGE, simpleName, + icon, marketIntent)); + } + } else { + // add provider + mList.addAll(providerList); + } + + // Init ArrayAdapter with OpenPGP Providers + ListAdapter adapter = new ArrayAdapter(getContext(), + android.R.layout.select_dialog_singlechoice, android.R.id.text1, mList) { + public View getView(int position, View convertView, ViewGroup parent) { + // User super class to create the View + View v = super.getView(position, convertView, parent); + TextView tv = (TextView) v.findViewById(android.R.id.text1); + + // Put the image on the TextView + tv.setCompoundDrawablesWithIntrinsicBounds(mList.get(position).icon, null, + null, null); + + // Add margin between image and text (support various screen densities) + int dp10 = (int) (10 * getContext().getResources().getDisplayMetrics().density + 0.5f); + tv.setCompoundDrawablePadding(dp10); + + return v; + } + }; + + builder.setSingleChoiceItems(adapter, getIndexOfProviderList(getValue()), + new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + OpenPgpProviderEntry entry = mList.get(which); + + if (entry.intent != null) { + /* + * Intents are called as activity + * + * Current approach is to assume the user installed the app. + * If he does not, the selected package is not valid. + * + * However applications should always consider this could happen, + * as the user might remove the currently used OpenPGP app. + */ + getContext().startActivity(entry.intent); + } + + mSelectedPackage = entry.packageName; + + /* + * Clicking on an item simulates the positive button click, and dismisses + * the dialog. + */ + OpenPgpListPreference.this.onClick(dialog, DialogInterface.BUTTON_POSITIVE); + dialog.dismiss(); + } + }); + + /* + * The typical interaction for list-based dialogs is to have click-on-an-item dismiss the + * dialog instead of the user having to press 'Ok'. + */ + builder.setPositiveButton(null, null); + } + + @Override + protected void onDialogClosed(boolean positiveResult) { + super.onDialogClosed(positiveResult); + + if (positiveResult && (mSelectedPackage != null)) { + if (callChangeListener(mSelectedPackage)) { + setValue(mSelectedPackage); + } + } + } + + private int getIndexOfProviderList(String packageName) { + for (OpenPgpProviderEntry app : mList) { + if (app.packageName.equals(packageName)) { + return mList.indexOf(app); + } + } + + return -1; + } + + public void setValue(String packageName) { + mSelectedPackage = packageName; + persistString(packageName); + } + + public String getValue() { + return mSelectedPackage; + } + + public String getEntry() { + return getEntryByValue(mSelectedPackage); + } + + @Override + protected Object onGetDefaultValue(TypedArray a, int index) { + return a.getString(index); + } + + @Override + protected void onSetInitialValue(boolean restoreValue, Object defaultValue) { + setValue(restoreValue ? getPersistedString(mSelectedPackage) : (String) defaultValue); + } + + public String getEntryByValue(String packageName) { + for (OpenPgpProviderEntry app : mList) { + if (app.packageName.equals(packageName)) { + return app.simpleName; + } + } + + return null; + } + + private static class OpenPgpProviderEntry { + private String packageName; + private String simpleName; + private Drawable icon; + private Intent intent; + + public OpenPgpProviderEntry(String packageName, String simpleName, Drawable icon) { + this.packageName = packageName; + this.simpleName = simpleName; + this.icon = icon; + } + + public OpenPgpProviderEntry(String packageName, String simpleName, Drawable icon, Intent intent) { + this(packageName, simpleName, icon); + this.intent = intent; + } + + @Override + public String toString() { + return simpleName; + } + } +} diff --git a/src/org/openintents/openpgp/util/OpenPgpServiceConnection.java b/src/org/openintents/openpgp/util/OpenPgpServiceConnection.java new file mode 100644 index 00000000..0395a7bc --- /dev/null +++ b/src/org/openintents/openpgp/util/OpenPgpServiceConnection.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2014 Dominik Schürmann + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openintents.openpgp.util; + +import org.openintents.openpgp.IOpenPgpService; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.IBinder; + +public class OpenPgpServiceConnection { + + // interface to create callbacks for onServiceConnected + public interface OnBound { + public void onBound(IOpenPgpService service); + } + + private Context mApplicationContext; + + private IOpenPgpService mService; + private String mProviderPackageName; + + private OnBound mOnBoundListener; + + /** + * Create new OpenPgpServiceConnection + * + * @param context + * @param providerPackageName specify package name of OpenPGP provider, + * e.g., "org.sufficientlysecure.keychain" + */ + public OpenPgpServiceConnection(Context context, String providerPackageName) { + this.mApplicationContext = context.getApplicationContext(); + this.mProviderPackageName = providerPackageName; + } + + /** + * Create new OpenPgpServiceConnection + * + * @param context + * @param providerPackageName specify package name of OpenPGP provider, + * e.g., "org.sufficientlysecure.keychain" + * @param onBoundListener callback, executed when connection to service has been established + */ + public OpenPgpServiceConnection(Context context, String providerPackageName, + OnBound onBoundListener) { + this.mApplicationContext = context.getApplicationContext(); + this.mProviderPackageName = providerPackageName; + this.mOnBoundListener = onBoundListener; + } + + public IOpenPgpService getService() { + return mService; + } + + public boolean isBound() { + return (mService != null); + } + + private ServiceConnection mServiceConnection = new ServiceConnection() { + public void onServiceConnected(ComponentName name, IBinder service) { + mService = IOpenPgpService.Stub.asInterface(service); + if (mOnBoundListener != null) { + mOnBoundListener.onBound(mService); + } + } + + public void onServiceDisconnected(ComponentName name) { + mService = null; + } + }; + + /** + * If not already bound, bind to service! + * + * @return + */ + public boolean bindToService() { + // if not already bound... + if (mService == null) { + try { + Intent serviceIntent = new Intent(); + serviceIntent.setAction(IOpenPgpService.class.getName()); + // NOTE: setPackage is very important to restrict the intent to this provider only! + serviceIntent.setPackage(mProviderPackageName); + mApplicationContext.bindService(serviceIntent, mServiceConnection, + Context.BIND_AUTO_CREATE); + + return true; + } catch (Exception e) { + return false; + } + } else { + return true; + } + } + + public void unbindFromService() { + mApplicationContext.unbindService(mServiceConnection); + } + +} diff --git a/src/org/openintents/openpgp/util/OpenPgpUtils.java b/src/org/openintents/openpgp/util/OpenPgpUtils.java new file mode 100644 index 00000000..e24c937a --- /dev/null +++ b/src/org/openintents/openpgp/util/OpenPgpUtils.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2014 Dominik Schürmann + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openintents.openpgp.util; + +import java.util.List; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.ResolveInfo; + +public class OpenPgpUtils { + + public static final Pattern PGP_MESSAGE = Pattern.compile( + ".*?(-----BEGIN PGP MESSAGE-----.*?-----END PGP MESSAGE-----).*", + Pattern.DOTALL); + + public static final Pattern PGP_SIGNED_MESSAGE = Pattern.compile( + ".*?(-----BEGIN PGP SIGNED MESSAGE-----.*?-----BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----).*", + Pattern.DOTALL); + + public static final int PARSE_RESULT_NO_PGP = -1; + public static final int PARSE_RESULT_MESSAGE = 0; + public static final int PARSE_RESULT_SIGNED_MESSAGE = 1; + + public static int parseMessage(String message) { + Matcher matcherSigned = PGP_SIGNED_MESSAGE.matcher(message); + Matcher matcherMessage = PGP_MESSAGE.matcher(message); + + if (matcherMessage.matches()) { + return PARSE_RESULT_MESSAGE; + } else if (matcherSigned.matches()) { + return PARSE_RESULT_SIGNED_MESSAGE; + } else { + return PARSE_RESULT_NO_PGP; + } + } + + public static boolean isAvailable(Context context) { + Intent intent = new Intent(OpenPgpApi.SERVICE_INTENT); + List resInfo = context.getPackageManager().queryIntentServices(intent, 0); + if (!resInfo.isEmpty()) { + return true; + } else { + return false; + } + } + + public static String convertKeyIdToHex(long keyId) { + return "0x" + convertKeyIdToHex32bit(keyId >> 32) + convertKeyIdToHex32bit(keyId); + } + + private static String convertKeyIdToHex32bit(long keyId) { + String hexString = Long.toHexString(keyId & 0xffffffffL).toLowerCase(Locale.US); + while (hexString.length() < 8) { + hexString = "0" + hexString; + } + return hexString; + } +} diff --git a/src/org/openintents/openpgp/util/ParcelFileDescriptorUtil.java b/src/org/openintents/openpgp/util/ParcelFileDescriptorUtil.java new file mode 100644 index 00000000..58c62110 --- /dev/null +++ b/src/org/openintents/openpgp/util/ParcelFileDescriptorUtil.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2014 Dominik Schürmann + * 2013 Flow (http://stackoverflow.com/questions/18212152/transfer-inputstream-to-another-service-across-process-boundaries-with-parcelf) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openintents.openpgp.util; + +import android.os.ParcelFileDescriptor; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class ParcelFileDescriptorUtil { + + public interface IThreadListener { + void onThreadFinished(final Thread thread); + } + + public static ParcelFileDescriptor pipeFrom(InputStream inputStream, IThreadListener listener) + throws IOException { + ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe(); + ParcelFileDescriptor readSide = pipe[0]; + ParcelFileDescriptor writeSide = pipe[1]; + + // start the transfer thread + new TransferThread(inputStream, new ParcelFileDescriptor.AutoCloseOutputStream(writeSide), + listener) + .start(); + + return readSide; + } + + public static ParcelFileDescriptor pipeTo(OutputStream outputStream, IThreadListener listener) + throws IOException { + ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe(); + ParcelFileDescriptor readSide = pipe[0]; + ParcelFileDescriptor writeSide = pipe[1]; + + // start the transfer thread + new TransferThread(new ParcelFileDescriptor.AutoCloseInputStream(readSide), outputStream, + listener) + .start(); + + return writeSide; + } + + static class TransferThread extends Thread { + final InputStream mIn; + final OutputStream mOut; + final IThreadListener mListener; + + TransferThread(InputStream in, OutputStream out, IThreadListener listener) { + super("ParcelFileDescriptor Transfer Thread"); + mIn = in; + mOut = out; + mListener = listener; + setDaemon(true); + } + + @Override + public void run() { + byte[] buf = new byte[1024]; + int len; + + try { + while ((len = mIn.read(buf)) > 0) { + mOut.write(buf, 0, len); + } + mOut.flush(); // just to be safe + } catch (IOException e) { + //Log.e(OpenPgpApi.TAG, "TransferThread" + getId() + ": writing failed", e); + } finally { + try { + mIn.close(); + } catch (IOException e) { + //Log.e(OpenPgpApi.TAG, "TransferThread" + getId(), e); + } + try { + mOut.close(); + } catch (IOException e) { + //Log.e(OpenPgpApi.TAG, "TransferThread" + getId(), e); + } + } + if (mListener != null) { + //Log.d(OpenPgpApi.TAG, "TransferThread " + getId() + " finished!"); + mListener.onThreadFinished(this); + } + } + } +} \ No newline at end of file -- cgit v1.2.3 From 650e1ebda82596cd4fbfaae406e6eccf189f4f63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Wed, 7 May 2014 17:01:17 +0200 Subject: Add readme --- README.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 00000000..da8361e8 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# OpenPGP Remote API library + +For usage instructions, please consult our Wiki page about the [OpenPGP Remote API](https://github.com/open-keychain/open-keychain/wiki/OpenPGP-API). \ No newline at end of file -- cgit v1.2.3 From 3236432c39f1d5a1bbbe362c5cfdb088756fd04f Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Sun, 8 Jun 2014 13:18:46 +0200 Subject: Make minidns Android agnostic there is really no need for minidns to be Android exclusive. Replacing the Android log API with JUL make minidns available for Android and Java SE. --- AndroidManifest.xml | 13 - README.md | 2 +- build.gradle | 154 +++++-- build.xml | 92 ---- proguard-project.txt | 0 project.properties | 2 - src/de/measite/minidns/Client.java | 291 ------------ src/de/measite/minidns/DNSMessage.java | 507 --------------------- src/de/measite/minidns/Question.java | 62 --- src/de/measite/minidns/Record.java | 297 ------------ src/de/measite/minidns/record/A.java | 37 -- src/de/measite/minidns/record/AAAA.java | 43 -- src/de/measite/minidns/record/CNAME.java | 44 -- src/de/measite/minidns/record/Data.java | 16 - src/de/measite/minidns/record/NS.java | 12 - src/de/measite/minidns/record/SRV.java | 74 --- src/de/measite/minidns/util/NameUtil.java | 91 ---- src/main/java/de/measite/minidns/Client.java | 290 ++++++++++++ src/main/java/de/measite/minidns/DNSMessage.java | 507 +++++++++++++++++++++ src/main/java/de/measite/minidns/Question.java | 62 +++ src/main/java/de/measite/minidns/Record.java | 297 ++++++++++++ src/main/java/de/measite/minidns/record/A.java | 37 ++ src/main/java/de/measite/minidns/record/AAAA.java | 43 ++ src/main/java/de/measite/minidns/record/CNAME.java | 44 ++ src/main/java/de/measite/minidns/record/Data.java | 16 + src/main/java/de/measite/minidns/record/NS.java | 12 + src/main/java/de/measite/minidns/record/SRV.java | 74 +++ .../java/de/measite/minidns/util/NameUtil.java | 91 ++++ 28 files changed, 1597 insertions(+), 1613 deletions(-) delete mode 100644 AndroidManifest.xml delete mode 100644 build.xml delete mode 100644 proguard-project.txt delete mode 100644 project.properties delete mode 100644 src/de/measite/minidns/Client.java delete mode 100644 src/de/measite/minidns/DNSMessage.java delete mode 100644 src/de/measite/minidns/Question.java delete mode 100644 src/de/measite/minidns/Record.java delete mode 100644 src/de/measite/minidns/record/A.java delete mode 100644 src/de/measite/minidns/record/AAAA.java delete mode 100644 src/de/measite/minidns/record/CNAME.java delete mode 100644 src/de/measite/minidns/record/Data.java delete mode 100644 src/de/measite/minidns/record/NS.java delete mode 100644 src/de/measite/minidns/record/SRV.java delete mode 100644 src/de/measite/minidns/util/NameUtil.java create mode 100644 src/main/java/de/measite/minidns/Client.java create mode 100644 src/main/java/de/measite/minidns/DNSMessage.java create mode 100644 src/main/java/de/measite/minidns/Question.java create mode 100644 src/main/java/de/measite/minidns/Record.java create mode 100644 src/main/java/de/measite/minidns/record/A.java create mode 100644 src/main/java/de/measite/minidns/record/AAAA.java create mode 100644 src/main/java/de/measite/minidns/record/CNAME.java create mode 100644 src/main/java/de/measite/minidns/record/Data.java create mode 100644 src/main/java/de/measite/minidns/record/NS.java create mode 100644 src/main/java/de/measite/minidns/record/SRV.java create mode 100644 src/main/java/de/measite/minidns/util/NameUtil.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml deleted file mode 100644 index 6c9477d9..00000000 --- a/AndroidManifest.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - diff --git a/README.md b/README.md index 99ba78b3..3c1417a1 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ MiniDNS ------- -MiniDNS is a minial dns client library for android. It can parse a basic set +MiniDNS is a minimal dns client library for android. It can parse a basic set of resource records (A, AAAA, NS, SRV) and is easy to use and extend. This library is not intended to be used as a DNS server. You might want to diff --git a/build.gradle b/build.gradle index ae9c7693..7cf46718 100644 --- a/build.gradle +++ b/build.gradle @@ -1,34 +1,126 @@ -buildscript { - repositories { - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:0.9.0' - } +apply plugin: 'java' +apply plugin: 'eclipse' +apply plugin: 'maven' +apply plugin: 'osgi' +apply plugin: 'signing' + +ext { + shortVersion = '0.1' + isSnapshot = true + gitCommit = getGitCommit() + isReleaseVersion = !shortVersion + sonatypeCredentialsAvailable = project.hasProperty('sonatypeUsername') && project.hasProperty('sonatypePassword') + signingRequired = isReleaseVersion + sonatypeSnapshotUrl = 'https://oss.sonatype.org/content/repositories/snapshots' + sonatypeStagingUrl = 'https://oss.sonatype.org/service/local/staging/deploy/maven2' + buildDate = (new java.text.SimpleDateFormat("yyyy-MM-dd")).format(new Date()) +} + +group = 'de.measite.minidns' +description = "A minimal DNS client library with support for A, AAAA, NS and SRV records" +sourceCompatibility = 1.7 +version = shortVersion +if (isSnapshot) { + version += '-SNAPSHOT' +} + +jar { + manifest { + instruction 'Implementation-GitRevision:', project.ext.gitCommit + } +} + +gradle.taskGraph.whenReady { taskGraph -> + if (signingRequired + && taskGraph.allTasks.any { it instanceof Sign }) { + // Use Java 6's console to read from the console (no good for a CI environment) + Console console = System.console() + console.printf '\n\nWe have to sign some things in this build.\n\nPlease enter your signing details.\n\n' + def password = console.readPassword('GnuPG Private Key Password: ') + + allprojects { ext.'signing.password' = password } + + console.printf '\nThanks.\n\n' + } } -apply plugin: 'android-library' - -android { - compileSdkVersion 19 - buildToolsVersion '19.0.3' - - // NOTE: We are using the old folder structure to also support Eclipse - sourceSets { - main { - manifest.srcFile 'AndroidManifest.xml' - java.srcDirs = ['src'] - resources.srcDirs = ['src'] - aidl.srcDirs = ['src'] - renderscript.srcDirs = ['src'] - res.srcDirs = ['res'] - assets.srcDirs = ['assets'] - } - } - - // Do not abort build if lint finds errors - lintOptions { - abortOnError false - } +uploadArchives { + repositories { + mavenDeployer { + if (signingRequired) { + beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } + } + repository(url: project.sonatypeStagingUrl) { + if (sonatypeCredentialsAvailable) { + authentication(userName: sonatypeUsername, password: sonatypePassword) + } + } + snapshotRepository(url: project.sonatypeSnapshotUrl) { + if (sonatypeCredentialsAvailable) { + authentication(userName: sonatypeUsername, password: sonatypePassword) + } + } + + pom.project { + name 'minidns' + packaging 'jar' + inceptionYear '2014' + url 'https://github.com/rtreffer/minidns' + description project.description + + issueManagement { + system 'GitHub' + url 'https://github.com/rtreffer/minidns/issues' + } + + distributionManagement { + snapshotRepository { + id 'minidns.snapshot' + url project.sonatypeSnapshotUrl + } + } + + scm { + url 'https://github.com/rtreffer/minidns' + connection 'scm:git:https://github.com/rtreffer/minidns.git' + developerConnection 'scm:git:https://github.com/rtreffer/minidns.git' + } + + licenses { + license { + name 'The Apache Software License, Version 2.0' + url 'http://www.apache.org/licenses/LICENSE-2.0.txt' + distribution 'repo' + } + } + + developers { + developer { + id 'rtreffer' + name 'Rene Treffer' + } + developer { + id 'flow' + name 'Florian Schmaus' + email 'flow@geekplace.eu' + } + } + } + } + } + signing { + required { signingRequired } + sign configurations.archives + } +} + +def getGitCommit() { + def dotGit = new File("$projectDir/.git") + if (!dotGit.isDirectory()) return 'non-git build' + + def cmd = 'git describe --all --dirty=+' + def proc = cmd.execute() + def gitCommit = proc.text.trim() + assert !gitCommit.isEmpty() + gitCommit } diff --git a/build.xml b/build.xml deleted file mode 100644 index e10480a0..00000000 --- a/build.xml +++ /dev/null @@ -1,92 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/proguard-project.txt b/proguard-project.txt deleted file mode 100644 index e69de29b..00000000 diff --git a/project.properties b/project.properties deleted file mode 100644 index 9e86aa64..00000000 --- a/project.properties +++ /dev/null @@ -1,2 +0,0 @@ -target=android-19 -android.library=true diff --git a/src/de/measite/minidns/Client.java b/src/de/measite/minidns/Client.java deleted file mode 100644 index 8bc75d01..00000000 --- a/src/de/measite/minidns/Client.java +++ /dev/null @@ -1,291 +0,0 @@ -package de.measite.minidns; - -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.LineNumberReader; -import java.lang.reflect.Method; -import java.net.DatagramPacket; -import java.net.DatagramSocket; -import java.net.InetAddress; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Random; - -import android.util.Log; -import de.measite.minidns.Record.CLASS; -import de.measite.minidns.Record.TYPE; - -/** - * A minimal DNS client for SRV/A/AAAA/NS and CNAME lookups, with IDN support. - * This circumvents the missing javax.naming package on android. - */ -public class Client { - - /** - * The internal random class for sequence generation. - */ - protected Random random; - - /** - * The buffer size for dns replies. - */ - protected int bufferSize = 1500; - - /** - * DNS timeout. - */ - protected int timeout = 5000; - - /** - * Create a new DNS client. - */ - public Client() { - try { - random = SecureRandom.getInstance("SHA1PRNG"); - } catch (NoSuchAlgorithmException e1) { - random = new SecureRandom(); - } - } - - /** - * Query a nameserver for a single entry. - * @param name The DNS name to request. - * @param type The DNS type to request (SRV, A, AAAA, ...). - * @param clazz The class of the request (usually IN for Internet). - * @param host The DNS server host. - * @param port The DNS server port. - * @return - * @throws IOException On IO Errors. - */ - public DNSMessage query(String name, TYPE type, CLASS clazz, String host, int port) - throws IOException - { - Question q = new Question(); - q.setClazz(clazz); - q.setType(type); - q.setName(name); - return query(q, host, port); - } - - /** - * Query a nameserver for a single entry. - * @param name The DNS name to request. - * @param type The DNS type to request (SRV, A, AAAA, ...). - * @param clazz The class of the request (usually IN for Internet). - * @param host The DNS server host. - * @return - * @throws IOException On IO Errors. - */ - public DNSMessage query(String name, TYPE type, CLASS clazz, String host) - throws IOException - { - Question q = new Question(); - q.setClazz(clazz); - q.setType(type); - q.setName(name); - return query(q, host); - } - - /** - * Query the system nameserver for a single entry. - * @param name The DNS name to request. - * @param type The DNS type to request (SRV, A, AAAA, ...). - * @param clazz The class of the request (usually IN for Internet). - * @return The DNSMessage reply or null. - */ - public DNSMessage query(String name, TYPE type, CLASS clazz) - { - Question q = new Question(); - q.setClazz(clazz); - q.setType(type); - q.setName(name); - return query(q); - } - - /** - * Query a specific server for one entry. - * @param q The question section of the DNS query. - * @param host The dns server host. - * @throws IOException On IOErrors. - */ - public DNSMessage query(Question q, String host) throws IOException { - return query(q, host, 53); - } - - /** - * Query a specific server for one entry. - * @param q The question section of the DNS query. - * @param host The dns server host. - * @param port the dns port. - * @throws IOException On IOErrors. - */ - public DNSMessage query(Question q, String host, int port) throws IOException { - DNSMessage message = new DNSMessage(); - message.setQuestions(new Question[]{q}); - message.setRecursionDesired(true); - message.setId(random.nextInt()); - byte[] buf = message.toArray(); - DatagramSocket socket = new DatagramSocket(); - DatagramPacket packet = new DatagramPacket( - buf, buf.length, InetAddress.getByName(host), port); - socket.setSoTimeout(timeout); - socket.send(packet); - packet = new DatagramPacket(new byte[bufferSize], bufferSize); - socket.receive(packet); - DNSMessage dnsMessage = DNSMessage.parse(packet.getData()); - if (dnsMessage.getId() != message.getId()) { - return null; - } - return dnsMessage; - } - - /** - * Query the system DNS server for one entry. - * @param q The question section of the DNS query. - */ - public DNSMessage query(Question q) { - String dnsServer[] = findDNS(); - for (String dns : dnsServer) { - try { - DNSMessage message = query(q, dns); - if (message == null) { - continue; - } - if (message.getResponseCode() != - DNSMessage.RESPONSE_CODE.NO_ERROR) { - continue; - } - for (Record record: message.getAnswers()) { - if (record.isAnswer(q)) { - return message; - } - } - } catch (IOException ioe) { - } - } - return null; - } - - /** - * Retrieve a list of currently configured DNS servers. - * @return The server array. - */ - public String[] findDNS() { - String[] result = findDNSByReflection(); - if (result != null) { - Log.d("minidns/client", - "Got DNS servers via reflection: " + Arrays.toString(result)); - return result; - } - - result = findDNSByExec(); - if (result != null) { - Log.d("minidns/client", - "Got DNS servers via exec: " + Arrays.toString(result)); - return result; - } - - // fallback for ipv4 and ipv6 connectivity - // see https://developers.google.com/speed/public-dns/docs/using - Log.d("minidns/client", - "No DNS found? Using fallback [8.8.8.8, [2001:4860:4860::8888]]"); - - return new String[]{"8.8.8.8", "[2001:4860:4860::8888]"}; - } - - /** - * Try to retrieve the list of dns server by executing getprop. - * @return Array of servers, or null on failure. - */ - protected String[] findDNSByExec() { - try { - Process process = Runtime.getRuntime().exec("getprop"); - InputStream inputStream = process.getInputStream(); - LineNumberReader lnr = new LineNumberReader( - new InputStreamReader(inputStream)); - String line = null; - HashSet server = new HashSet(6); - while ((line = lnr.readLine()) != null) { - int split = line.indexOf("]: ["); - if (split == -1) { - continue; - } - String property = line.substring(1, split); - String value = line.substring(split + 4, line.length() - 1); - if (property.endsWith(".dns") || property.endsWith(".dns1") || - property.endsWith(".dns2") || property.endsWith(".dns3") || - property.endsWith(".dns4")) { - - // normalize the address - - InetAddress ip = InetAddress.getByName(value); - - if (ip == null) continue; - - value = ip.getHostAddress(); - - if (value == null) continue; - if (value.length() == 0) continue; - - server.add(value); - } - } - if (server.size() > 0) { - return server.toArray(new String[server.size()]); - } - } catch (IOException e) { - e.printStackTrace(); - } - return null; - } - - /** - * Try to retrieve the list of dns server by calling SystemProperties. - * @return Array of servers, or null on failure. - */ - protected String[] findDNSByReflection() { - try { - Class SystemProperties = - Class.forName("android.os.SystemProperties"); - Method method = SystemProperties.getMethod("get", - new Class[] { String.class }); - - ArrayList servers = new ArrayList(5); - - for (String propKey : new String[] { - "net.dns1", "net.dns2", "net.dns3", "net.dns4"}) { - - String value = (String)method.invoke(null, propKey); - - if (value == null) continue; - if (value.length() == 0) continue; - if (servers.contains(value)) continue; - - InetAddress ip = InetAddress.getByName(value); - - if (ip == null) continue; - - value = ip.getHostAddress(); - - if (value == null) continue; - if (value.length() == 0) continue; - if (servers.contains(value)) continue; - - servers.add(value); - } - - if (servers.size() > 0) { - return servers.toArray(new String[servers.size()]); - } - } catch (Exception e) { - // we might trigger some problems this way - e.printStackTrace(); - } - return null; - } - -} diff --git a/src/de/measite/minidns/DNSMessage.java b/src/de/measite/minidns/DNSMessage.java deleted file mode 100644 index 14c8f04b..00000000 --- a/src/de/measite/minidns/DNSMessage.java +++ /dev/null @@ -1,507 +0,0 @@ -package de.measite.minidns; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; -import java.util.Arrays; - -/** - * A DNS message as defined by rfc1035. The message consists of a header and - * 4 sections: question, answer, nameserver and addition resource record - * section. - * A message can either be parsed ({@see #parse(byte[])}) or serialized - * ({@see #toArray()}). - */ -public class DNSMessage { - - /** - * Possible DNS reply codes. - */ - public static enum RESPONSE_CODE { - NO_ERROR(0), FORMAT_ERR(1), SERVER_FAIL(2), NX_DOMAIN(3), - NO_IMP(4), REFUSED(5), YXDOMAIN(6), YXRRSET(7), - NXRRSET(8), NOT_AUTH(9),NOT_ZONE(10); - - /** - * Reverse lookup table for response codes. - */ - private final static RESPONSE_CODE INVERSE_LUT[] = new RESPONSE_CODE[]{ - NO_ERROR, FORMAT_ERR, SERVER_FAIL, NX_DOMAIN, NO_IMP, - REFUSED, YXDOMAIN, YXRRSET, NXRRSET, NOT_AUTH, NOT_ZONE, - null, null, null, null, null - }; - - /** - * The response code value. - */ - private final byte value; - - /** - * Create a new response code. - * @param value The response code value. - */ - private RESPONSE_CODE(int value) { - this.value = (byte)value; - } - - /** - * Retrieve the byte value of the response code. - * @return - */ - public byte getValue() { - return (byte) value; - } - - /** - * Retrieve the response code for a byte value. - * @param value The byte value. - * @return The symbolic response code or null. - * @throws IllegalArgumentException if the value is not in the range of - * 0..15. - */ - public static RESPONSE_CODE getResponseCode(int value) { - if (value < 0 || value > 15) { - throw new IllegalArgumentException(); - } - return INVERSE_LUT[value]; - } - - }; - - /** - * Symbolic DNS Opcode values. - */ - public static enum OPCODE { - QUERY(0), - INVERSE_QUERY(1), - STATUS(2), - NOTIFY(4), - UPDATE(5); - - /** - * Lookup table for for obcode reolution. - */ - private final static OPCODE INVERSE_LUT[] = new OPCODE[]{ - QUERY, INVERSE_QUERY, STATUS, null, NOTIFY, UPDATE, null, - null, null, null, null, null, null, null, null - }; - - /** - * The value of this opcode. - */ - private final byte value; - - /** - * Create a new opcode for a given byte value. - * @param value The byte value of the opcode. - */ - private OPCODE(int value) { - this.value = (byte)value; - } - - /** - * Retrieve the byte value of this opcode. - * @return The byte value of this opcode. - */ - public byte getValue() { - return value; - } - - /** - * Retrieve the symbolic name of an opcode byte. - * @param value The byte value of the opcode. - * @return The symbolic opcode or null. - * @throws IllegalArgumentException If the byte value is not in the - * range 0..15. - */ - public static OPCODE getOpcode(int value) { - if (value < 0 || value > 15) { - throw new IllegalArgumentException(); - } - return INVERSE_LUT[value]; - } - - }; - - /** - * The DNS message id. - */ - protected int id; - - /** - * The DNS message opcode. - */ - protected OPCODE opcode; - - /** - * The response code of this dns message. - */ - protected RESPONSE_CODE responseCode; - - /** - * True if this is a query. - */ - protected boolean query; - - /** - * True if this is a authorative response. - */ - protected boolean authoritativeAnswer; - - /** - * True on truncate, tcp should be used. - */ - protected boolean truncated; - - /** - * True if the server should recurse. - */ - protected boolean recursionDesired; - - /** - * True if recursion is possible. - */ - protected boolean recursionAvailable; - - /** - * True if the server regarded the response as authentic. - */ - protected boolean authenticData; - - /** - * True if the server should not check the replies. - */ - protected boolean checkDisabled; - - /** - * The question section content. - */ - protected Question questions[]; - - /** - * The answers section content. - */ - protected Record answers[]; - - /** - * The nameserver records. - */ - protected Record nameserverRecords[]; - - /** - * Additional resousrce records. - */ - protected Record additionalResourceRecords[]; - - /** - * Retrieve the current DNS message id. - * @return The current DNS message id. - */ - public int getId() { - return id; - } - - /** - * Set the current DNS message id. - * @param id The new DNS message id. - */ - public void setId(int id) { - this.id = id & 0xffff; - } - - /** - * Retrieve the query type (true or false; - * @return True if this DNS message is a query. - */ - public boolean isQuery() { - return query; - } - - /** - * Set the query status of this message. - * @param query The new query status. - */ - public void setQuery(boolean query) { - this.query = query; - } - - /** - * True if the DNS message is an authoritative answer. - * @return True if this an authoritative DNS message. - */ - public boolean isAuthoritativeAnswer() { - return authoritativeAnswer; - } - - /** - * Set the authoritative answer flag. - * @param authoritativeAnswer Tge new authoritative answer value. - */ - public void setAuthoritativeAnswer(boolean authoritativeAnswer) { - this.authoritativeAnswer = authoritativeAnswer; - } - - /** - * Retrieve the truncation status of this message. True means that the - * client should try a tcp lookup. - * @return True if this message was truncated. - */ - public boolean isTruncated() { - return truncated; - } - - /** - * Set the truncation bit on this DNS message. - * @param truncated The new truncated bit status. - */ - public void setTruncated(boolean truncated) { - this.truncated = truncated; - } - - /** - * Check if this message preferes recursion. - * @return True if recursion is desired. - */ - public boolean isRecursionDesired() { - return recursionDesired; - } - - /** - * Set the recursion desired flag on this message. - * @param recursionDesired The new recusrion setting. - */ - public void setRecursionDesired(boolean recursionDesired) { - this.recursionDesired = recursionDesired; - } - - /** - * Retrieve the recursion available flag of this DNS message. - * @return The recursion available flag of this message. - */ - public boolean isRecursionAvailable() { - return recursionAvailable; - } - - /** - * Set the recursion available flog from this DNS message. - * @param recursionAvailable The new recursion available status. - */ - public void setRecursionAvailable(boolean recursionAvailable) { - this.recursionAvailable = recursionAvailable; - } - - /** - * Retrieve the authentic data flag of this message. - * @return The authentic data flag. - */ - public boolean isAuthenticData() { - return authenticData; - } - - /** - * Set the authentic data flag on this DNS message. - * @param authenticData The new authentic data flag value. - */ - public void setAuthenticData(boolean authenticData) { - this.authenticData = authenticData; - } - - /** - * Check if checks are disabled. - * @return The status of the CheckDisabled flag. - */ - public boolean isCheckDisabled() { - return checkDisabled; - } - - /** - * Change the check status of this packet. - * @param checkDisabled - */ - public void setCheckDisabled(boolean checkDisabled) { - this.checkDisabled = checkDisabled; - } - - /** - * Generate a binary dns packet out of this message. - * @return byte[] the binary representation. - * @throws IOException Should never happen. - */ - public byte[] toArray() throws IOException { - ByteArrayOutputStream baos = new ByteArrayOutputStream(512); - DataOutputStream dos = new DataOutputStream(baos); - int header = 0; - if (query) { - header += 1 << 15; - } - if (opcode != null) { - header += opcode.getValue() << 11; - } - if (authoritativeAnswer) { - header += 1 << 10; - } - if (truncated) { - header += 1 << 9; - } - if (recursionDesired) { - header += 1 << 8; - } - if (recursionAvailable) { - header += 1 << 7; - } - if (authenticData) { - header += 1 << 5; - } - if (checkDisabled) { - header += 1 << 4; - } - if (responseCode != null) { - header += responseCode.getValue(); - } - dos.writeShort((short)id); - dos.writeShort((short)header); - if (questions == null) { - dos.writeShort(0); - } else { - dos.writeShort((short)questions.length); - } - if (answers == null) { - dos.writeShort(0); - } else { - dos.writeShort((short)answers.length); - } - if (nameserverRecords == null) { - dos.writeShort(0); - } else { - dos.writeShort((short)nameserverRecords.length); - } - if (additionalResourceRecords == null) { - dos.writeShort(0); - } else { - dos.writeShort((short)additionalResourceRecords.length); - } - for (Question question: questions) { - dos.write(question.toByteArray()); - } - dos.flush(); - return baos.toByteArray(); - } - - /** - * Build a DNS Message based on a binary DNS message. - * @param data The DNS message data. - * @return Parsed DNSMessage message. - * @throws IOException On read errors. - */ - public static DNSMessage parse(byte data[]) throws IOException { - ByteArrayInputStream bis = new ByteArrayInputStream(data); - DataInputStream dis = new DataInputStream(bis); - DNSMessage message = new DNSMessage(); - message.id = dis.readUnsignedShort(); - int header = dis.readUnsignedShort(); - message.query = ((header >> 15) & 1) == 0; - message.opcode = OPCODE.getOpcode((header >> 11) & 0xf); - message.authoritativeAnswer = ((header >> 10) & 1) == 1; - message.truncated = ((header >> 9) & 1) == 1; - message.recursionDesired = ((header >> 8) & 1) == 1; - message.recursionAvailable = ((header >> 7) & 1) == 1; - message.authenticData = ((header >> 5) & 1) == 1; - message.checkDisabled = ((header >> 4) & 1) == 1; - message.responseCode = RESPONSE_CODE.getResponseCode(header & 0xf); - int questionCount = dis.readUnsignedShort(); - int answerCount = dis.readUnsignedShort(); - int nameserverCount = dis.readUnsignedShort(); - int additionalResourceRecordCount = dis.readUnsignedShort(); - message.questions = new Question[questionCount]; - while (questionCount-- > 0) { - Question q = new Question(); - q.parse(dis, data); - message.questions[questionCount] = q; - } - message.answers = new Record[answerCount]; - while (answerCount-- > 0) { - Record rr = new Record(); - rr.parse(dis, data); - message.answers[answerCount] = rr; - } - message.nameserverRecords = new Record[nameserverCount]; - while (nameserverCount-- > 0) { - Record rr = new Record(); - rr.parse(dis, data); - message.nameserverRecords[nameserverCount] = rr; - } - message.additionalResourceRecords = - new Record[additionalResourceRecordCount]; - while (additionalResourceRecordCount-- > 0) { - Record rr = new Record(); - rr.parse(dis, data); - message.additionalResourceRecords[additionalResourceRecordCount] = - rr; - } - return message; - } - - /** - * Set the question part of this message. - * @param questions The questions. - */ - public void setQuestions(Question ... questions) { - this.questions = questions; - } - - /** - * Retrieve the opcode of this message. - * @return The opcode of this message. - */ - public OPCODE getOpcode() { - return opcode; - } - - /** - * Retrieve the response code of this message. - * @return The response code. - */ - public RESPONSE_CODE getResponseCode() { - return responseCode; - } - - /** - * Retrieve the question section of this message. - * @return The DNS question section. - */ - public Question[] getQuestions() { - return questions; - } - - /** - * Retrieve the answer records of this DNS message. - * @return The answer section of this DNS message. - */ - public Record[] getAnswers() { - return answers; - } - - /** - * Retrieve the nameserver records of this DNS message. - * @return The nameserver section of this DNS message. - */ - public Record[] getNameserverRecords() { - return nameserverRecords; - } - - /** - * Retrieve the additional resource records attached to this DNS message. - * @return The additional resource record section of this DNS message. - */ - public Record[] getAdditionalResourceRecords() { - return additionalResourceRecords; - } - - public String toString() { - return "-- DNSMessage " + id + " --\n" + - Arrays.toString(answers); - } - -} diff --git a/src/de/measite/minidns/Question.java b/src/de/measite/minidns/Question.java deleted file mode 100644 index 9d1e3f56..00000000 --- a/src/de/measite/minidns/Question.java +++ /dev/null @@ -1,62 +0,0 @@ -package de.measite.minidns; - -import java.io.ByteArrayOutputStream; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; - -import de.measite.minidns.Record.CLASS; -import de.measite.minidns.Record.TYPE; -import de.measite.minidns.util.NameUtil; - -public class Question { - - private String name; - - private TYPE type; - - private CLASS clazz = CLASS.IN; - - public TYPE getType() { - return type; - } - - public void setType(TYPE type) { - this.type = type; - } - - public CLASS getClazz() { - return clazz; - } - - public void setClazz(CLASS clazz) { - this.clazz = clazz; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public void parse(DataInputStream dis, byte[] data) throws IOException { - this.name = NameUtil.parse(dis, data); - this.type = TYPE.getType(dis.readUnsignedShort()); - this.clazz = CLASS.getClass(dis.readUnsignedShort()); - } - - public byte[] toByteArray() throws IOException { - ByteArrayOutputStream baos = new ByteArrayOutputStream(512); - DataOutputStream dos = new DataOutputStream(baos); - - dos.write(NameUtil.toByteArray(this.name)); - dos.writeShort(type.getValue()); - dos.writeShort(clazz.getValue()); - - dos.flush(); - return baos.toByteArray(); - } - -} diff --git a/src/de/measite/minidns/Record.java b/src/de/measite/minidns/Record.java deleted file mode 100644 index fb0b5d5f..00000000 --- a/src/de/measite/minidns/Record.java +++ /dev/null @@ -1,297 +0,0 @@ -package de.measite.minidns; - -import java.io.DataInputStream; -import java.io.IOException; -import java.util.HashMap; - -import de.measite.minidns.record.A; -import de.measite.minidns.record.AAAA; -import de.measite.minidns.record.CNAME; -import de.measite.minidns.record.Data; -import de.measite.minidns.record.NS; -import de.measite.minidns.record.SRV; -import de.measite.minidns.util.NameUtil; - -/** - * A generic DNS record. - */ -public class Record { - - /** - * The record type. - * {@see http://www.iana.org/assignments/dns-parameters} - */ - public static enum TYPE { - A(1), - NS(2), - MD(3), - MF(4), - CNAME(5), - SOA(6), - MB(7), - MG(8), - MR(9), - NULL(10), - WKS(11), - PTR(12), - HINFO(13), - MINFO(14), - MX(15), - TXT(16), - RP(17), - AFSDB(18), - X25(19), - ISDN(20), - RT(21), - NSAP(22), - NSAP_PTR(23), - SIG(24), - KEY(25), - PX(26), - GPOS(27), - AAAA(28), - LOC(29), - NXT(30), - EID(31), - NIMLOC(32), - SRV(33), - ATMA(34), - NAPTR(35), - KX(36), - CERT(37), - A6(38), - DNAME(39), - SINK(40), - OPT(41), - APL(42), - DS(43), - SSHFP(44), - IPSECKEY(45), - RRSIG(46), - NSEC(47), - DNSKEY(48), - DHCID(49), - NSEC3(50), - NSEC3PARAM(51), - HIP(55), - NINFO(56), - RKEY(57), - TALINK(58), - SPF(99), - UINFO(100), - UID(101), - GID(102), - TKEY(249), - TSIG(250), - IXFR(251), - AXFR(252), - MAILB(253), - MAILA(254), - ANY(255), - TA(32768), - DLV(32769); - - /** - * The value of this DNS record type. - */ - private final int value; - - /** - * Internal lookup table to map values to types. - */ - private final static HashMap INVERSE_LUT = - new HashMap(); - - /** - * Initialize the reverse lookup table. - */ - static { - for(TYPE t: TYPE.values()) { - INVERSE_LUT.put(t.getValue(), t); - } - } - - /** - * Create a new record type. - * @param value The binary value of this type. - */ - private TYPE(int value) { - this.value = value; - } - - /** - * Retrieve the binary value of this type. - * @return The binary value. - */ - public int getValue() { - return value; - } - - /** - * Retrieve the symbolic type of the binary value. - * @param value The binary type value. - * @return The symbolic tpye. - */ - public static TYPE getType(int value) { - return INVERSE_LUT.get(value); - } - }; - - /** - * The symbolic class of a DNS record (usually IN for Internet). - */ - public static enum CLASS { - IN(1), - CH(3), - HS(4), - NONE(254), - ANY(255); - - /** - * Internal reverse lookup table to map binary class values to symbolic - * names. - */ - private final static HashMap INVERSE_LUT = - new HashMap(); - - /** - * Initialize the interal reverse lookup table. - */ - static { - for(CLASS c: CLASS.values()) { - INVERSE_LUT.put(c.getValue(), c); - } - } - - /** - * The binary value of this dns class. - */ - private final int value; - - /** - * Create a new DNS class based on a binary value. - * @param value The binary value of this DNS class. - */ - private CLASS(int value) { - this.value = value; - } - - /** - * Retrieve the binary value of this DNS class. - * @return The binary value of this DNS class. - */ - public int getValue() { - return value; - } - - /** - * Retrieve the symbolic DNS class for a binary class value. - * @param value The binary DNS class value. - * @return The symbolic class instance. - */ - public static CLASS getClass(int value) { - return INVERSE_LUT.get(value); - } - - } - - /** - * The generic name of this record. - */ - protected String name; - - /** - * The type (and payload type) of this record. - */ - protected TYPE type; - - /** - * The record class (usually CLASS.IN). - */ - protected CLASS clazz; - - /** - * The ttl of this record. - */ - protected long ttl; - - /** - * The payload object of this record. - */ - protected Data payloadData; - - /** - * Parse a given record based on the full message data and the current - * stream position. - * @param dis The DataInputStream positioned at the first record byte. - * @param data The full message data. - * @throws IOException In case of malformed replies. - */ - public void parse(DataInputStream dis, byte[] data) throws IOException { - this.name = NameUtil.parse(dis, data); - this.type = TYPE.getType(dis.readUnsignedShort()); - this.clazz = CLASS.getClass(dis.readUnsignedShort()); - this.ttl = (((long)dis.readUnsignedShort()) << 32) + - dis.readUnsignedShort(); - int payloadLength = dis.readUnsignedShort(); - switch (this.type) { - case SRV: - this.payloadData = new SRV(); - break; - case AAAA: - this.payloadData = new AAAA(); - break; - case A: - this.payloadData = new A(); - break; - case NS: - this.payloadData = new NS(); - break; - case CNAME: - this.payloadData = new CNAME(); - break; - default: - System.out.println("Unparsed type " + type); - this.payloadData = null; - for (int i = 0; i < payloadLength; i++) { - dis.readByte(); - } - break; - } - if (this.payloadData != null) { - this.payloadData.parse(dis, data, payloadLength); - } - } - - /** - * Retrieve a textual representation of this resource record. - * @return String - */ - @Override - public String toString() { - if (payloadData == null) { - return "RR " + type + "/" + clazz; - } - return "RR " + type + "/" + clazz + ": " + payloadData.toString(); - }; - - /** - * Check if this record answers a given query. - * @param q The query. - * @return True if this record is a valid answer. - */ - public boolean isAnswer(Question q) { - return ((q.getType() == type) || (q.getType() == TYPE.ANY)) && - ((q.getClazz() == clazz) || (q.getClazz() == CLASS.ANY)) && - (q.getName().equals(name)); - } - - public String getName() { - return name; - } - - public Data getPayload() { - return payloadData; - } - -} diff --git a/src/de/measite/minidns/record/A.java b/src/de/measite/minidns/record/A.java deleted file mode 100644 index a85a7af0..00000000 --- a/src/de/measite/minidns/record/A.java +++ /dev/null @@ -1,37 +0,0 @@ -package de.measite.minidns.record; - -import java.io.DataInputStream; -import java.io.IOException; - -import de.measite.minidns.Record.TYPE; - -public class A implements Data { - - private byte[] ip; - - @Override - public TYPE getType() { - return TYPE.A; - } - - @Override - public byte[] toByteArray() { - return ip; - } - - @Override - public void parse(DataInputStream dis, byte[] data, int length) - throws IOException { - ip = new byte[4]; - dis.readFully(ip); - } - - @Override - public String toString() { - return Integer.toString(ip[0] & 0xff) + "." + - Integer.toString(ip[1] & 0xff) + "." + - Integer.toString(ip[2] & 0xff) + "." + - Integer.toString(ip[3] & 0xff); - } - -} diff --git a/src/de/measite/minidns/record/AAAA.java b/src/de/measite/minidns/record/AAAA.java deleted file mode 100644 index d89147b2..00000000 --- a/src/de/measite/minidns/record/AAAA.java +++ /dev/null @@ -1,43 +0,0 @@ -package de.measite.minidns.record; - -import java.io.DataInputStream; -import java.io.IOException; - -import de.measite.minidns.Record.TYPE; - -public class AAAA implements Data { - - private byte[] ip; - - @Override - public TYPE getType() { - return TYPE.AAAA; - } - - @Override - public byte[] toByteArray() { - return ip; - } - - @Override - public void parse(DataInputStream dis, byte[] data, int length) - throws IOException { - ip = new byte[16]; - dis.readFully(ip); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < ip.length; i += 2) { - if (i != 0) { - sb.append(':'); - } - sb.append(Integer.toHexString( - ((ip[i] & 0xff) << 8) + (ip[i + 1] & 0xff) - )); - } - return sb.toString(); - } - -} diff --git a/src/de/measite/minidns/record/CNAME.java b/src/de/measite/minidns/record/CNAME.java deleted file mode 100644 index 4657b4a5..00000000 --- a/src/de/measite/minidns/record/CNAME.java +++ /dev/null @@ -1,44 +0,0 @@ -package de.measite.minidns.record; - -import java.io.DataInputStream; -import java.io.IOException; - -import de.measite.minidns.Record.TYPE; -import de.measite.minidns.util.NameUtil; - -public class CNAME implements Data { - - protected String name; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - @Override - public byte[] toByteArray() { - // TODO Auto-generated method stub - return null; - } - - @Override - public void parse(DataInputStream dis, byte[] data, int length) - throws IOException - { - this.name = NameUtil.parse(dis, data); - } - - @Override - public TYPE getType() { - return TYPE.CNAME; - } - - @Override - public String toString() { - return "to \"" + name + "\""; - } - -} diff --git a/src/de/measite/minidns/record/Data.java b/src/de/measite/minidns/record/Data.java deleted file mode 100644 index 9cb80374..00000000 --- a/src/de/measite/minidns/record/Data.java +++ /dev/null @@ -1,16 +0,0 @@ -package de.measite.minidns.record; - -import java.io.DataInputStream; -import java.io.IOException; - -import de.measite.minidns.Record.TYPE; - -public interface Data { - - TYPE getType(); - - byte[] toByteArray(); - - void parse(DataInputStream dis, byte data[], int length) throws IOException; - -} diff --git a/src/de/measite/minidns/record/NS.java b/src/de/measite/minidns/record/NS.java deleted file mode 100644 index bf07e8c4..00000000 --- a/src/de/measite/minidns/record/NS.java +++ /dev/null @@ -1,12 +0,0 @@ -package de.measite.minidns.record; - -import de.measite.minidns.Record.TYPE; - -public class NS extends CNAME { - - @Override - public TYPE getType() { - return TYPE.NS; - } - -} diff --git a/src/de/measite/minidns/record/SRV.java b/src/de/measite/minidns/record/SRV.java deleted file mode 100644 index 32b70c4d..00000000 --- a/src/de/measite/minidns/record/SRV.java +++ /dev/null @@ -1,74 +0,0 @@ -package de.measite.minidns.record; - -import java.io.DataInputStream; -import java.io.IOException; - -import de.measite.minidns.Record.TYPE; -import de.measite.minidns.util.NameUtil; - -public class SRV implements Data { - - protected int priority; - protected int weight; - protected int port; - protected String name; - - public int getPriority() { - return priority; - } - - public void setPriority(int priority) { - this.priority = priority; - } - - public int getWeight() { - return weight; - } - - public void setWeight(int weight) { - this.weight = weight; - } - - public int getPort() { - return port; - } - - public void setPort(int port) { - this.port = port; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - @Override - public byte[] toByteArray() { - // TODO Auto-generated method stub - return null; - } - - @Override - public void parse(DataInputStream dis, byte[] data, int length) - throws IOException - { - this.priority = dis.readUnsignedShort(); - this.weight = dis.readUnsignedShort(); - this.port = dis.readUnsignedShort(); - this.name = NameUtil.parse(dis, data); - } - - @Override - public String toString() { - return "SRV " + name + ":" + port + " p:" + priority + " w:" + weight; - } - - @Override - public TYPE getType() { - return TYPE.SRV; - } - -} diff --git a/src/de/measite/minidns/util/NameUtil.java b/src/de/measite/minidns/util/NameUtil.java deleted file mode 100644 index 91a6649d..00000000 --- a/src/de/measite/minidns/util/NameUtil.java +++ /dev/null @@ -1,91 +0,0 @@ -package de.measite.minidns.util; - -import java.io.ByteArrayOutputStream; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; -import java.net.IDN; -import java.util.HashSet; -import java.util.Arrays; - -public class NameUtil { - - public static int size(String name) { - return name.length() + 2; - } - - public static boolean idnEquals(String name1, String name2) { - if (name1 == name2) return true; // catches null, null - if (name1 == null) return false; - if (name2 == null) return false; - if (name1.equals(name2)) return true; - - try { - return Arrays.equals(toByteArray(name1),toByteArray(name2)); - } catch (IOException e) { - return false; // impossible - } - } - - public static byte[] toByteArray(String name) throws IOException { - ByteArrayOutputStream baos = new ByteArrayOutputStream(64); - DataOutputStream dos = new DataOutputStream(baos); - for (String s: name.split("[.\u3002\uFF0E\uFF61]")) { - byte[] buffer = IDN.toASCII(s).getBytes(); - dos.writeByte(buffer.length); - dos.write(buffer); - } - dos.writeByte(0); - dos.flush(); - return baos.toByteArray(); - } - - public static String parse(DataInputStream dis, byte data[]) - throws IOException - { - int c = dis.readUnsignedByte(); - if ((c & 0xc0) == 0xc0) { - c = ((c & 0x3f) << 8) + dis.readUnsignedByte(); - HashSet jumps = new HashSet(); - jumps.add(c); - return parse(data, c, jumps); - } - if (c == 0) { - return ""; - } - byte b[] = new byte[c]; - dis.readFully(b); - String s = IDN.toUnicode(new String(b)); - String t = parse(dis, data); - if (t.length() > 0) { - s = s + "." + t; - } - return s; - } - - public static String parse( - byte data[], - int offset, - HashSet jumps - ) { - int c = data[offset] & 0xff; - if ((c & 0xc0) == 0xc0) { - c = ((c & 0x3f) << 8) + (data[offset + 1] & 0xff); - if (jumps.contains(c)) { - throw new IllegalStateException("Cyclic offsets detected."); - } - jumps.add(c); - return parse(data, c, jumps); - } - if (c == 0) { - return ""; - } - String s = new String(data,offset + 1, c); - String t = parse(data, offset + 1 + c, jumps); - if (t.length() > 0) { - s = s + "." + t; - } - return s; - } - -} diff --git a/src/main/java/de/measite/minidns/Client.java b/src/main/java/de/measite/minidns/Client.java new file mode 100644 index 00000000..fb42cd25 --- /dev/null +++ b/src/main/java/de/measite/minidns/Client.java @@ -0,0 +1,290 @@ +package de.measite.minidns; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.LineNumberReader; +import java.lang.reflect.Method; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Random; +import java.util.logging.Logger; + +import de.measite.minidns.Record.CLASS; +import de.measite.minidns.Record.TYPE; + +/** + * A minimal DNS client for SRV/A/AAAA/NS and CNAME lookups, with IDN support. + * This circumvents the missing javax.naming package on android. + */ +public class Client { + + private static final Logger LOGGER = Logger.getLogger(Client.class.getName()); + + /** + * The internal random class for sequence generation. + */ + protected Random random; + + /** + * The buffer size for dns replies. + */ + protected int bufferSize = 1500; + + /** + * DNS timeout. + */ + protected int timeout = 5000; + + /** + * Create a new DNS client. + */ + public Client() { + try { + random = SecureRandom.getInstance("SHA1PRNG"); + } catch (NoSuchAlgorithmException e1) { + random = new SecureRandom(); + } + } + + /** + * Query a nameserver for a single entry. + * @param name The DNS name to request. + * @param type The DNS type to request (SRV, A, AAAA, ...). + * @param clazz The class of the request (usually IN for Internet). + * @param host The DNS server host. + * @param port The DNS server port. + * @return + * @throws IOException On IO Errors. + */ + public DNSMessage query(String name, TYPE type, CLASS clazz, String host, int port) + throws IOException + { + Question q = new Question(); + q.setClazz(clazz); + q.setType(type); + q.setName(name); + return query(q, host, port); + } + + /** + * Query a nameserver for a single entry. + * @param name The DNS name to request. + * @param type The DNS type to request (SRV, A, AAAA, ...). + * @param clazz The class of the request (usually IN for Internet). + * @param host The DNS server host. + * @return + * @throws IOException On IO Errors. + */ + public DNSMessage query(String name, TYPE type, CLASS clazz, String host) + throws IOException + { + Question q = new Question(); + q.setClazz(clazz); + q.setType(type); + q.setName(name); + return query(q, host); + } + + /** + * Query the system nameserver for a single entry. + * @param name The DNS name to request. + * @param type The DNS type to request (SRV, A, AAAA, ...). + * @param clazz The class of the request (usually IN for Internet). + * @return The DNSMessage reply or null. + */ + public DNSMessage query(String name, TYPE type, CLASS clazz) + { + Question q = new Question(); + q.setClazz(clazz); + q.setType(type); + q.setName(name); + return query(q); + } + + /** + * Query a specific server for one entry. + * @param q The question section of the DNS query. + * @param host The dns server host. + * @throws IOException On IOErrors. + */ + public DNSMessage query(Question q, String host) throws IOException { + return query(q, host, 53); + } + + /** + * Query a specific server for one entry. + * @param q The question section of the DNS query. + * @param host The dns server host. + * @param port the dns port. + * @throws IOException On IOErrors. + */ + public DNSMessage query(Question q, String host, int port) throws IOException { + DNSMessage message = new DNSMessage(); + message.setQuestions(new Question[]{q}); + message.setRecursionDesired(true); + message.setId(random.nextInt()); + byte[] buf = message.toArray(); + DatagramSocket socket = new DatagramSocket(); + DatagramPacket packet = new DatagramPacket( + buf, buf.length, InetAddress.getByName(host), port); + socket.setSoTimeout(timeout); + socket.send(packet); + packet = new DatagramPacket(new byte[bufferSize], bufferSize); + socket.receive(packet); + DNSMessage dnsMessage = DNSMessage.parse(packet.getData()); + if (dnsMessage.getId() != message.getId()) { + return null; + } + return dnsMessage; + } + + /** + * Query the system DNS server for one entry. + * @param q The question section of the DNS query. + */ + public DNSMessage query(Question q) { + String dnsServer[] = findDNS(); + for (String dns : dnsServer) { + try { + DNSMessage message = query(q, dns); + if (message == null) { + continue; + } + if (message.getResponseCode() != + DNSMessage.RESPONSE_CODE.NO_ERROR) { + continue; + } + for (Record record: message.getAnswers()) { + if (record.isAnswer(q)) { + return message; + } + } + } catch (IOException ioe) { + } + } + return null; + } + + /** + * Retrieve a list of currently configured DNS servers. + * @return The server array. + */ + public String[] findDNS() { + String[] result = findDNSByReflection(); + if (result != null) { + LOGGER.fine("Got DNS servers via reflection: " + Arrays.toString(result)); + return result; + } + + result = findDNSByExec(); + if (result != null) { + LOGGER.fine("Got DNS servers via exec: " + Arrays.toString(result)); + return result; + } + + // fallback for ipv4 and ipv6 connectivity + // see https://developers.google.com/speed/public-dns/docs/using + LOGGER.fine("No DNS found? Using fallback [8.8.8.8, [2001:4860:4860::8888]]"); + + return new String[]{"8.8.8.8", "[2001:4860:4860::8888]"}; + } + + /** + * Try to retrieve the list of dns server by executing getprop. + * @return Array of servers, or null on failure. + */ + protected String[] findDNSByExec() { + try { + Process process = Runtime.getRuntime().exec("getprop"); + InputStream inputStream = process.getInputStream(); + LineNumberReader lnr = new LineNumberReader( + new InputStreamReader(inputStream)); + String line = null; + HashSet server = new HashSet(6); + while ((line = lnr.readLine()) != null) { + int split = line.indexOf("]: ["); + if (split == -1) { + continue; + } + String property = line.substring(1, split); + String value = line.substring(split + 4, line.length() - 1); + if (property.endsWith(".dns") || property.endsWith(".dns1") || + property.endsWith(".dns2") || property.endsWith(".dns3") || + property.endsWith(".dns4")) { + + // normalize the address + + InetAddress ip = InetAddress.getByName(value); + + if (ip == null) continue; + + value = ip.getHostAddress(); + + if (value == null) continue; + if (value.length() == 0) continue; + + server.add(value); + } + } + if (server.size() > 0) { + return server.toArray(new String[server.size()]); + } + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + + /** + * Try to retrieve the list of dns server by calling SystemProperties. + * @return Array of servers, or null on failure. + */ + protected String[] findDNSByReflection() { + try { + Class SystemProperties = + Class.forName("android.os.SystemProperties"); + Method method = SystemProperties.getMethod("get", + new Class[] { String.class }); + + ArrayList servers = new ArrayList(5); + + for (String propKey : new String[] { + "net.dns1", "net.dns2", "net.dns3", "net.dns4"}) { + + String value = (String)method.invoke(null, propKey); + + if (value == null) continue; + if (value.length() == 0) continue; + if (servers.contains(value)) continue; + + InetAddress ip = InetAddress.getByName(value); + + if (ip == null) continue; + + value = ip.getHostAddress(); + + if (value == null) continue; + if (value.length() == 0) continue; + if (servers.contains(value)) continue; + + servers.add(value); + } + + if (servers.size() > 0) { + return servers.toArray(new String[servers.size()]); + } + } catch (Exception e) { + // we might trigger some problems this way + e.printStackTrace(); + } + return null; + } + +} diff --git a/src/main/java/de/measite/minidns/DNSMessage.java b/src/main/java/de/measite/minidns/DNSMessage.java new file mode 100644 index 00000000..14c8f04b --- /dev/null +++ b/src/main/java/de/measite/minidns/DNSMessage.java @@ -0,0 +1,507 @@ +package de.measite.minidns; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.Arrays; + +/** + * A DNS message as defined by rfc1035. The message consists of a header and + * 4 sections: question, answer, nameserver and addition resource record + * section. + * A message can either be parsed ({@see #parse(byte[])}) or serialized + * ({@see #toArray()}). + */ +public class DNSMessage { + + /** + * Possible DNS reply codes. + */ + public static enum RESPONSE_CODE { + NO_ERROR(0), FORMAT_ERR(1), SERVER_FAIL(2), NX_DOMAIN(3), + NO_IMP(4), REFUSED(5), YXDOMAIN(6), YXRRSET(7), + NXRRSET(8), NOT_AUTH(9),NOT_ZONE(10); + + /** + * Reverse lookup table for response codes. + */ + private final static RESPONSE_CODE INVERSE_LUT[] = new RESPONSE_CODE[]{ + NO_ERROR, FORMAT_ERR, SERVER_FAIL, NX_DOMAIN, NO_IMP, + REFUSED, YXDOMAIN, YXRRSET, NXRRSET, NOT_AUTH, NOT_ZONE, + null, null, null, null, null + }; + + /** + * The response code value. + */ + private final byte value; + + /** + * Create a new response code. + * @param value The response code value. + */ + private RESPONSE_CODE(int value) { + this.value = (byte)value; + } + + /** + * Retrieve the byte value of the response code. + * @return + */ + public byte getValue() { + return (byte) value; + } + + /** + * Retrieve the response code for a byte value. + * @param value The byte value. + * @return The symbolic response code or null. + * @throws IllegalArgumentException if the value is not in the range of + * 0..15. + */ + public static RESPONSE_CODE getResponseCode(int value) { + if (value < 0 || value > 15) { + throw new IllegalArgumentException(); + } + return INVERSE_LUT[value]; + } + + }; + + /** + * Symbolic DNS Opcode values. + */ + public static enum OPCODE { + QUERY(0), + INVERSE_QUERY(1), + STATUS(2), + NOTIFY(4), + UPDATE(5); + + /** + * Lookup table for for obcode reolution. + */ + private final static OPCODE INVERSE_LUT[] = new OPCODE[]{ + QUERY, INVERSE_QUERY, STATUS, null, NOTIFY, UPDATE, null, + null, null, null, null, null, null, null, null + }; + + /** + * The value of this opcode. + */ + private final byte value; + + /** + * Create a new opcode for a given byte value. + * @param value The byte value of the opcode. + */ + private OPCODE(int value) { + this.value = (byte)value; + } + + /** + * Retrieve the byte value of this opcode. + * @return The byte value of this opcode. + */ + public byte getValue() { + return value; + } + + /** + * Retrieve the symbolic name of an opcode byte. + * @param value The byte value of the opcode. + * @return The symbolic opcode or null. + * @throws IllegalArgumentException If the byte value is not in the + * range 0..15. + */ + public static OPCODE getOpcode(int value) { + if (value < 0 || value > 15) { + throw new IllegalArgumentException(); + } + return INVERSE_LUT[value]; + } + + }; + + /** + * The DNS message id. + */ + protected int id; + + /** + * The DNS message opcode. + */ + protected OPCODE opcode; + + /** + * The response code of this dns message. + */ + protected RESPONSE_CODE responseCode; + + /** + * True if this is a query. + */ + protected boolean query; + + /** + * True if this is a authorative response. + */ + protected boolean authoritativeAnswer; + + /** + * True on truncate, tcp should be used. + */ + protected boolean truncated; + + /** + * True if the server should recurse. + */ + protected boolean recursionDesired; + + /** + * True if recursion is possible. + */ + protected boolean recursionAvailable; + + /** + * True if the server regarded the response as authentic. + */ + protected boolean authenticData; + + /** + * True if the server should not check the replies. + */ + protected boolean checkDisabled; + + /** + * The question section content. + */ + protected Question questions[]; + + /** + * The answers section content. + */ + protected Record answers[]; + + /** + * The nameserver records. + */ + protected Record nameserverRecords[]; + + /** + * Additional resousrce records. + */ + protected Record additionalResourceRecords[]; + + /** + * Retrieve the current DNS message id. + * @return The current DNS message id. + */ + public int getId() { + return id; + } + + /** + * Set the current DNS message id. + * @param id The new DNS message id. + */ + public void setId(int id) { + this.id = id & 0xffff; + } + + /** + * Retrieve the query type (true or false; + * @return True if this DNS message is a query. + */ + public boolean isQuery() { + return query; + } + + /** + * Set the query status of this message. + * @param query The new query status. + */ + public void setQuery(boolean query) { + this.query = query; + } + + /** + * True if the DNS message is an authoritative answer. + * @return True if this an authoritative DNS message. + */ + public boolean isAuthoritativeAnswer() { + return authoritativeAnswer; + } + + /** + * Set the authoritative answer flag. + * @param authoritativeAnswer Tge new authoritative answer value. + */ + public void setAuthoritativeAnswer(boolean authoritativeAnswer) { + this.authoritativeAnswer = authoritativeAnswer; + } + + /** + * Retrieve the truncation status of this message. True means that the + * client should try a tcp lookup. + * @return True if this message was truncated. + */ + public boolean isTruncated() { + return truncated; + } + + /** + * Set the truncation bit on this DNS message. + * @param truncated The new truncated bit status. + */ + public void setTruncated(boolean truncated) { + this.truncated = truncated; + } + + /** + * Check if this message preferes recursion. + * @return True if recursion is desired. + */ + public boolean isRecursionDesired() { + return recursionDesired; + } + + /** + * Set the recursion desired flag on this message. + * @param recursionDesired The new recusrion setting. + */ + public void setRecursionDesired(boolean recursionDesired) { + this.recursionDesired = recursionDesired; + } + + /** + * Retrieve the recursion available flag of this DNS message. + * @return The recursion available flag of this message. + */ + public boolean isRecursionAvailable() { + return recursionAvailable; + } + + /** + * Set the recursion available flog from this DNS message. + * @param recursionAvailable The new recursion available status. + */ + public void setRecursionAvailable(boolean recursionAvailable) { + this.recursionAvailable = recursionAvailable; + } + + /** + * Retrieve the authentic data flag of this message. + * @return The authentic data flag. + */ + public boolean isAuthenticData() { + return authenticData; + } + + /** + * Set the authentic data flag on this DNS message. + * @param authenticData The new authentic data flag value. + */ + public void setAuthenticData(boolean authenticData) { + this.authenticData = authenticData; + } + + /** + * Check if checks are disabled. + * @return The status of the CheckDisabled flag. + */ + public boolean isCheckDisabled() { + return checkDisabled; + } + + /** + * Change the check status of this packet. + * @param checkDisabled + */ + public void setCheckDisabled(boolean checkDisabled) { + this.checkDisabled = checkDisabled; + } + + /** + * Generate a binary dns packet out of this message. + * @return byte[] the binary representation. + * @throws IOException Should never happen. + */ + public byte[] toArray() throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(512); + DataOutputStream dos = new DataOutputStream(baos); + int header = 0; + if (query) { + header += 1 << 15; + } + if (opcode != null) { + header += opcode.getValue() << 11; + } + if (authoritativeAnswer) { + header += 1 << 10; + } + if (truncated) { + header += 1 << 9; + } + if (recursionDesired) { + header += 1 << 8; + } + if (recursionAvailable) { + header += 1 << 7; + } + if (authenticData) { + header += 1 << 5; + } + if (checkDisabled) { + header += 1 << 4; + } + if (responseCode != null) { + header += responseCode.getValue(); + } + dos.writeShort((short)id); + dos.writeShort((short)header); + if (questions == null) { + dos.writeShort(0); + } else { + dos.writeShort((short)questions.length); + } + if (answers == null) { + dos.writeShort(0); + } else { + dos.writeShort((short)answers.length); + } + if (nameserverRecords == null) { + dos.writeShort(0); + } else { + dos.writeShort((short)nameserverRecords.length); + } + if (additionalResourceRecords == null) { + dos.writeShort(0); + } else { + dos.writeShort((short)additionalResourceRecords.length); + } + for (Question question: questions) { + dos.write(question.toByteArray()); + } + dos.flush(); + return baos.toByteArray(); + } + + /** + * Build a DNS Message based on a binary DNS message. + * @param data The DNS message data. + * @return Parsed DNSMessage message. + * @throws IOException On read errors. + */ + public static DNSMessage parse(byte data[]) throws IOException { + ByteArrayInputStream bis = new ByteArrayInputStream(data); + DataInputStream dis = new DataInputStream(bis); + DNSMessage message = new DNSMessage(); + message.id = dis.readUnsignedShort(); + int header = dis.readUnsignedShort(); + message.query = ((header >> 15) & 1) == 0; + message.opcode = OPCODE.getOpcode((header >> 11) & 0xf); + message.authoritativeAnswer = ((header >> 10) & 1) == 1; + message.truncated = ((header >> 9) & 1) == 1; + message.recursionDesired = ((header >> 8) & 1) == 1; + message.recursionAvailable = ((header >> 7) & 1) == 1; + message.authenticData = ((header >> 5) & 1) == 1; + message.checkDisabled = ((header >> 4) & 1) == 1; + message.responseCode = RESPONSE_CODE.getResponseCode(header & 0xf); + int questionCount = dis.readUnsignedShort(); + int answerCount = dis.readUnsignedShort(); + int nameserverCount = dis.readUnsignedShort(); + int additionalResourceRecordCount = dis.readUnsignedShort(); + message.questions = new Question[questionCount]; + while (questionCount-- > 0) { + Question q = new Question(); + q.parse(dis, data); + message.questions[questionCount] = q; + } + message.answers = new Record[answerCount]; + while (answerCount-- > 0) { + Record rr = new Record(); + rr.parse(dis, data); + message.answers[answerCount] = rr; + } + message.nameserverRecords = new Record[nameserverCount]; + while (nameserverCount-- > 0) { + Record rr = new Record(); + rr.parse(dis, data); + message.nameserverRecords[nameserverCount] = rr; + } + message.additionalResourceRecords = + new Record[additionalResourceRecordCount]; + while (additionalResourceRecordCount-- > 0) { + Record rr = new Record(); + rr.parse(dis, data); + message.additionalResourceRecords[additionalResourceRecordCount] = + rr; + } + return message; + } + + /** + * Set the question part of this message. + * @param questions The questions. + */ + public void setQuestions(Question ... questions) { + this.questions = questions; + } + + /** + * Retrieve the opcode of this message. + * @return The opcode of this message. + */ + public OPCODE getOpcode() { + return opcode; + } + + /** + * Retrieve the response code of this message. + * @return The response code. + */ + public RESPONSE_CODE getResponseCode() { + return responseCode; + } + + /** + * Retrieve the question section of this message. + * @return The DNS question section. + */ + public Question[] getQuestions() { + return questions; + } + + /** + * Retrieve the answer records of this DNS message. + * @return The answer section of this DNS message. + */ + public Record[] getAnswers() { + return answers; + } + + /** + * Retrieve the nameserver records of this DNS message. + * @return The nameserver section of this DNS message. + */ + public Record[] getNameserverRecords() { + return nameserverRecords; + } + + /** + * Retrieve the additional resource records attached to this DNS message. + * @return The additional resource record section of this DNS message. + */ + public Record[] getAdditionalResourceRecords() { + return additionalResourceRecords; + } + + public String toString() { + return "-- DNSMessage " + id + " --\n" + + Arrays.toString(answers); + } + +} diff --git a/src/main/java/de/measite/minidns/Question.java b/src/main/java/de/measite/minidns/Question.java new file mode 100644 index 00000000..9d1e3f56 --- /dev/null +++ b/src/main/java/de/measite/minidns/Question.java @@ -0,0 +1,62 @@ +package de.measite.minidns; + +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +import de.measite.minidns.Record.CLASS; +import de.measite.minidns.Record.TYPE; +import de.measite.minidns.util.NameUtil; + +public class Question { + + private String name; + + private TYPE type; + + private CLASS clazz = CLASS.IN; + + public TYPE getType() { + return type; + } + + public void setType(TYPE type) { + this.type = type; + } + + public CLASS getClazz() { + return clazz; + } + + public void setClazz(CLASS clazz) { + this.clazz = clazz; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public void parse(DataInputStream dis, byte[] data) throws IOException { + this.name = NameUtil.parse(dis, data); + this.type = TYPE.getType(dis.readUnsignedShort()); + this.clazz = CLASS.getClass(dis.readUnsignedShort()); + } + + public byte[] toByteArray() throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(512); + DataOutputStream dos = new DataOutputStream(baos); + + dos.write(NameUtil.toByteArray(this.name)); + dos.writeShort(type.getValue()); + dos.writeShort(clazz.getValue()); + + dos.flush(); + return baos.toByteArray(); + } + +} diff --git a/src/main/java/de/measite/minidns/Record.java b/src/main/java/de/measite/minidns/Record.java new file mode 100644 index 00000000..fb0b5d5f --- /dev/null +++ b/src/main/java/de/measite/minidns/Record.java @@ -0,0 +1,297 @@ +package de.measite.minidns; + +import java.io.DataInputStream; +import java.io.IOException; +import java.util.HashMap; + +import de.measite.minidns.record.A; +import de.measite.minidns.record.AAAA; +import de.measite.minidns.record.CNAME; +import de.measite.minidns.record.Data; +import de.measite.minidns.record.NS; +import de.measite.minidns.record.SRV; +import de.measite.minidns.util.NameUtil; + +/** + * A generic DNS record. + */ +public class Record { + + /** + * The record type. + * {@see http://www.iana.org/assignments/dns-parameters} + */ + public static enum TYPE { + A(1), + NS(2), + MD(3), + MF(4), + CNAME(5), + SOA(6), + MB(7), + MG(8), + MR(9), + NULL(10), + WKS(11), + PTR(12), + HINFO(13), + MINFO(14), + MX(15), + TXT(16), + RP(17), + AFSDB(18), + X25(19), + ISDN(20), + RT(21), + NSAP(22), + NSAP_PTR(23), + SIG(24), + KEY(25), + PX(26), + GPOS(27), + AAAA(28), + LOC(29), + NXT(30), + EID(31), + NIMLOC(32), + SRV(33), + ATMA(34), + NAPTR(35), + KX(36), + CERT(37), + A6(38), + DNAME(39), + SINK(40), + OPT(41), + APL(42), + DS(43), + SSHFP(44), + IPSECKEY(45), + RRSIG(46), + NSEC(47), + DNSKEY(48), + DHCID(49), + NSEC3(50), + NSEC3PARAM(51), + HIP(55), + NINFO(56), + RKEY(57), + TALINK(58), + SPF(99), + UINFO(100), + UID(101), + GID(102), + TKEY(249), + TSIG(250), + IXFR(251), + AXFR(252), + MAILB(253), + MAILA(254), + ANY(255), + TA(32768), + DLV(32769); + + /** + * The value of this DNS record type. + */ + private final int value; + + /** + * Internal lookup table to map values to types. + */ + private final static HashMap INVERSE_LUT = + new HashMap(); + + /** + * Initialize the reverse lookup table. + */ + static { + for(TYPE t: TYPE.values()) { + INVERSE_LUT.put(t.getValue(), t); + } + } + + /** + * Create a new record type. + * @param value The binary value of this type. + */ + private TYPE(int value) { + this.value = value; + } + + /** + * Retrieve the binary value of this type. + * @return The binary value. + */ + public int getValue() { + return value; + } + + /** + * Retrieve the symbolic type of the binary value. + * @param value The binary type value. + * @return The symbolic tpye. + */ + public static TYPE getType(int value) { + return INVERSE_LUT.get(value); + } + }; + + /** + * The symbolic class of a DNS record (usually IN for Internet). + */ + public static enum CLASS { + IN(1), + CH(3), + HS(4), + NONE(254), + ANY(255); + + /** + * Internal reverse lookup table to map binary class values to symbolic + * names. + */ + private final static HashMap INVERSE_LUT = + new HashMap(); + + /** + * Initialize the interal reverse lookup table. + */ + static { + for(CLASS c: CLASS.values()) { + INVERSE_LUT.put(c.getValue(), c); + } + } + + /** + * The binary value of this dns class. + */ + private final int value; + + /** + * Create a new DNS class based on a binary value. + * @param value The binary value of this DNS class. + */ + private CLASS(int value) { + this.value = value; + } + + /** + * Retrieve the binary value of this DNS class. + * @return The binary value of this DNS class. + */ + public int getValue() { + return value; + } + + /** + * Retrieve the symbolic DNS class for a binary class value. + * @param value The binary DNS class value. + * @return The symbolic class instance. + */ + public static CLASS getClass(int value) { + return INVERSE_LUT.get(value); + } + + } + + /** + * The generic name of this record. + */ + protected String name; + + /** + * The type (and payload type) of this record. + */ + protected TYPE type; + + /** + * The record class (usually CLASS.IN). + */ + protected CLASS clazz; + + /** + * The ttl of this record. + */ + protected long ttl; + + /** + * The payload object of this record. + */ + protected Data payloadData; + + /** + * Parse a given record based on the full message data and the current + * stream position. + * @param dis The DataInputStream positioned at the first record byte. + * @param data The full message data. + * @throws IOException In case of malformed replies. + */ + public void parse(DataInputStream dis, byte[] data) throws IOException { + this.name = NameUtil.parse(dis, data); + this.type = TYPE.getType(dis.readUnsignedShort()); + this.clazz = CLASS.getClass(dis.readUnsignedShort()); + this.ttl = (((long)dis.readUnsignedShort()) << 32) + + dis.readUnsignedShort(); + int payloadLength = dis.readUnsignedShort(); + switch (this.type) { + case SRV: + this.payloadData = new SRV(); + break; + case AAAA: + this.payloadData = new AAAA(); + break; + case A: + this.payloadData = new A(); + break; + case NS: + this.payloadData = new NS(); + break; + case CNAME: + this.payloadData = new CNAME(); + break; + default: + System.out.println("Unparsed type " + type); + this.payloadData = null; + for (int i = 0; i < payloadLength; i++) { + dis.readByte(); + } + break; + } + if (this.payloadData != null) { + this.payloadData.parse(dis, data, payloadLength); + } + } + + /** + * Retrieve a textual representation of this resource record. + * @return String + */ + @Override + public String toString() { + if (payloadData == null) { + return "RR " + type + "/" + clazz; + } + return "RR " + type + "/" + clazz + ": " + payloadData.toString(); + }; + + /** + * Check if this record answers a given query. + * @param q The query. + * @return True if this record is a valid answer. + */ + public boolean isAnswer(Question q) { + return ((q.getType() == type) || (q.getType() == TYPE.ANY)) && + ((q.getClazz() == clazz) || (q.getClazz() == CLASS.ANY)) && + (q.getName().equals(name)); + } + + public String getName() { + return name; + } + + public Data getPayload() { + return payloadData; + } + +} diff --git a/src/main/java/de/measite/minidns/record/A.java b/src/main/java/de/measite/minidns/record/A.java new file mode 100644 index 00000000..a85a7af0 --- /dev/null +++ b/src/main/java/de/measite/minidns/record/A.java @@ -0,0 +1,37 @@ +package de.measite.minidns.record; + +import java.io.DataInputStream; +import java.io.IOException; + +import de.measite.minidns.Record.TYPE; + +public class A implements Data { + + private byte[] ip; + + @Override + public TYPE getType() { + return TYPE.A; + } + + @Override + public byte[] toByteArray() { + return ip; + } + + @Override + public void parse(DataInputStream dis, byte[] data, int length) + throws IOException { + ip = new byte[4]; + dis.readFully(ip); + } + + @Override + public String toString() { + return Integer.toString(ip[0] & 0xff) + "." + + Integer.toString(ip[1] & 0xff) + "." + + Integer.toString(ip[2] & 0xff) + "." + + Integer.toString(ip[3] & 0xff); + } + +} diff --git a/src/main/java/de/measite/minidns/record/AAAA.java b/src/main/java/de/measite/minidns/record/AAAA.java new file mode 100644 index 00000000..d89147b2 --- /dev/null +++ b/src/main/java/de/measite/minidns/record/AAAA.java @@ -0,0 +1,43 @@ +package de.measite.minidns.record; + +import java.io.DataInputStream; +import java.io.IOException; + +import de.measite.minidns.Record.TYPE; + +public class AAAA implements Data { + + private byte[] ip; + + @Override + public TYPE getType() { + return TYPE.AAAA; + } + + @Override + public byte[] toByteArray() { + return ip; + } + + @Override + public void parse(DataInputStream dis, byte[] data, int length) + throws IOException { + ip = new byte[16]; + dis.readFully(ip); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < ip.length; i += 2) { + if (i != 0) { + sb.append(':'); + } + sb.append(Integer.toHexString( + ((ip[i] & 0xff) << 8) + (ip[i + 1] & 0xff) + )); + } + return sb.toString(); + } + +} diff --git a/src/main/java/de/measite/minidns/record/CNAME.java b/src/main/java/de/measite/minidns/record/CNAME.java new file mode 100644 index 00000000..4657b4a5 --- /dev/null +++ b/src/main/java/de/measite/minidns/record/CNAME.java @@ -0,0 +1,44 @@ +package de.measite.minidns.record; + +import java.io.DataInputStream; +import java.io.IOException; + +import de.measite.minidns.Record.TYPE; +import de.measite.minidns.util.NameUtil; + +public class CNAME implements Data { + + protected String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public byte[] toByteArray() { + // TODO Auto-generated method stub + return null; + } + + @Override + public void parse(DataInputStream dis, byte[] data, int length) + throws IOException + { + this.name = NameUtil.parse(dis, data); + } + + @Override + public TYPE getType() { + return TYPE.CNAME; + } + + @Override + public String toString() { + return "to \"" + name + "\""; + } + +} diff --git a/src/main/java/de/measite/minidns/record/Data.java b/src/main/java/de/measite/minidns/record/Data.java new file mode 100644 index 00000000..9cb80374 --- /dev/null +++ b/src/main/java/de/measite/minidns/record/Data.java @@ -0,0 +1,16 @@ +package de.measite.minidns.record; + +import java.io.DataInputStream; +import java.io.IOException; + +import de.measite.minidns.Record.TYPE; + +public interface Data { + + TYPE getType(); + + byte[] toByteArray(); + + void parse(DataInputStream dis, byte data[], int length) throws IOException; + +} diff --git a/src/main/java/de/measite/minidns/record/NS.java b/src/main/java/de/measite/minidns/record/NS.java new file mode 100644 index 00000000..bf07e8c4 --- /dev/null +++ b/src/main/java/de/measite/minidns/record/NS.java @@ -0,0 +1,12 @@ +package de.measite.minidns.record; + +import de.measite.minidns.Record.TYPE; + +public class NS extends CNAME { + + @Override + public TYPE getType() { + return TYPE.NS; + } + +} diff --git a/src/main/java/de/measite/minidns/record/SRV.java b/src/main/java/de/measite/minidns/record/SRV.java new file mode 100644 index 00000000..32b70c4d --- /dev/null +++ b/src/main/java/de/measite/minidns/record/SRV.java @@ -0,0 +1,74 @@ +package de.measite.minidns.record; + +import java.io.DataInputStream; +import java.io.IOException; + +import de.measite.minidns.Record.TYPE; +import de.measite.minidns.util.NameUtil; + +public class SRV implements Data { + + protected int priority; + protected int weight; + protected int port; + protected String name; + + public int getPriority() { + return priority; + } + + public void setPriority(int priority) { + this.priority = priority; + } + + public int getWeight() { + return weight; + } + + public void setWeight(int weight) { + this.weight = weight; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public byte[] toByteArray() { + // TODO Auto-generated method stub + return null; + } + + @Override + public void parse(DataInputStream dis, byte[] data, int length) + throws IOException + { + this.priority = dis.readUnsignedShort(); + this.weight = dis.readUnsignedShort(); + this.port = dis.readUnsignedShort(); + this.name = NameUtil.parse(dis, data); + } + + @Override + public String toString() { + return "SRV " + name + ":" + port + " p:" + priority + " w:" + weight; + } + + @Override + public TYPE getType() { + return TYPE.SRV; + } + +} diff --git a/src/main/java/de/measite/minidns/util/NameUtil.java b/src/main/java/de/measite/minidns/util/NameUtil.java new file mode 100644 index 00000000..91a6649d --- /dev/null +++ b/src/main/java/de/measite/minidns/util/NameUtil.java @@ -0,0 +1,91 @@ +package de.measite.minidns.util; + +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.net.IDN; +import java.util.HashSet; +import java.util.Arrays; + +public class NameUtil { + + public static int size(String name) { + return name.length() + 2; + } + + public static boolean idnEquals(String name1, String name2) { + if (name1 == name2) return true; // catches null, null + if (name1 == null) return false; + if (name2 == null) return false; + if (name1.equals(name2)) return true; + + try { + return Arrays.equals(toByteArray(name1),toByteArray(name2)); + } catch (IOException e) { + return false; // impossible + } + } + + public static byte[] toByteArray(String name) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(64); + DataOutputStream dos = new DataOutputStream(baos); + for (String s: name.split("[.\u3002\uFF0E\uFF61]")) { + byte[] buffer = IDN.toASCII(s).getBytes(); + dos.writeByte(buffer.length); + dos.write(buffer); + } + dos.writeByte(0); + dos.flush(); + return baos.toByteArray(); + } + + public static String parse(DataInputStream dis, byte data[]) + throws IOException + { + int c = dis.readUnsignedByte(); + if ((c & 0xc0) == 0xc0) { + c = ((c & 0x3f) << 8) + dis.readUnsignedByte(); + HashSet jumps = new HashSet(); + jumps.add(c); + return parse(data, c, jumps); + } + if (c == 0) { + return ""; + } + byte b[] = new byte[c]; + dis.readFully(b); + String s = IDN.toUnicode(new String(b)); + String t = parse(dis, data); + if (t.length() > 0) { + s = s + "." + t; + } + return s; + } + + public static String parse( + byte data[], + int offset, + HashSet jumps + ) { + int c = data[offset] & 0xff; + if ((c & 0xc0) == 0xc0) { + c = ((c & 0x3f) << 8) + (data[offset + 1] & 0xff); + if (jumps.contains(c)) { + throw new IllegalStateException("Cyclic offsets detected."); + } + jumps.add(c); + return parse(data, c, jumps); + } + if (c == 0) { + return ""; + } + String s = new String(data,offset + 1, c); + String t = parse(data, offset + 1 + c, jumps); + if (t.length() > 0) { + s = s + "." + t; + } + return s; + } + +} -- cgit v1.2.3 From defe51910153ed6a9aaf6becae9e6484bed609bd Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Sun, 8 Jun 2014 13:33:14 +0200 Subject: Close DatagramSocket using try-with-resources --- src/main/java/de/measite/minidns/Client.java | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/main/java/de/measite/minidns/Client.java b/src/main/java/de/measite/minidns/Client.java index fb42cd25..e20c007e 100644 --- a/src/main/java/de/measite/minidns/Client.java +++ b/src/main/java/de/measite/minidns/Client.java @@ -131,18 +131,19 @@ public class Client { message.setRecursionDesired(true); message.setId(random.nextInt()); byte[] buf = message.toArray(); - DatagramSocket socket = new DatagramSocket(); - DatagramPacket packet = new DatagramPacket( - buf, buf.length, InetAddress.getByName(host), port); - socket.setSoTimeout(timeout); - socket.send(packet); - packet = new DatagramPacket(new byte[bufferSize], bufferSize); - socket.receive(packet); - DNSMessage dnsMessage = DNSMessage.parse(packet.getData()); - if (dnsMessage.getId() != message.getId()) { - return null; + try (DatagramSocket socket = new DatagramSocket()) { + DatagramPacket packet = new DatagramPacket(buf, buf.length, + InetAddress.getByName(host), port); + socket.setSoTimeout(timeout); + socket.send(packet); + packet = new DatagramPacket(new byte[bufferSize], bufferSize); + socket.receive(packet); + DNSMessage dnsMessage = DNSMessage.parse(packet.getData()); + if (dnsMessage.getId() != message.getId()) { + return null; + } + return dnsMessage; } - return dnsMessage; } /** -- cgit v1.2.3 From 315648382ec2b1fd59c36e388e0fae5d23b3b9a6 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Sun, 8 Jun 2014 13:54:51 +0200 Subject: Remove printStackTrace() calls it's hard to assign those an app in the Android log. Better to use JUL. --- src/main/java/de/measite/minidns/Client.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/de/measite/minidns/Client.java b/src/main/java/de/measite/minidns/Client.java index e20c007e..b5712d24 100644 --- a/src/main/java/de/measite/minidns/Client.java +++ b/src/main/java/de/measite/minidns/Client.java @@ -14,6 +14,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.Random; +import java.util.logging.Level; import java.util.logging.Logger; import de.measite.minidns.Record.CLASS; @@ -238,7 +239,7 @@ public class Client { return server.toArray(new String[server.size()]); } } catch (IOException e) { - e.printStackTrace(); + LOGGER.log(Level.WARNING, "Exception in findDNSByExec", e); } return null; } @@ -283,7 +284,7 @@ public class Client { } } catch (Exception e) { // we might trigger some problems this way - e.printStackTrace(); + LOGGER.log(Level.WARNING, "Exception in findDNSByReflection", e); } return null; } -- cgit v1.2.3 From 52807fe7c415e9977cd2d7191b7e1251c9216256 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Sun, 8 Jun 2014 13:57:50 +0200 Subject: Log IOException --- src/main/java/de/measite/minidns/Client.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/de/measite/minidns/Client.java b/src/main/java/de/measite/minidns/Client.java index b5712d24..cad3902a 100644 --- a/src/main/java/de/measite/minidns/Client.java +++ b/src/main/java/de/measite/minidns/Client.java @@ -169,6 +169,7 @@ public class Client { } } } catch (IOException ioe) { + LOGGER.log(Level.FINE, "IOException in query", ioe); } } return null; -- cgit v1.2.3 From a77887d32fae68171fcd0d2989bf537c0c11f0b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Mon, 9 Jun 2014 21:54:19 +0200 Subject: Update buildTools to 19.1 --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 69b937c1..096f358e 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:0.10.0' + classpath 'com.android.tools.build:gradle:0.11.1' } } @@ -13,7 +13,7 @@ apply plugin: 'android-library' android { compileSdkVersion 19 - buildToolsVersion '19.0.3' + buildToolsVersion '19.1' // NOTE: We are using the old folder structure to also support Eclipse sourceSets { -- cgit v1.2.3 From 7dd8cfc6e6cd9bafb2cb3a6fdc9866de7d7495a6 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Thu, 12 Jun 2014 09:29:35 +0200 Subject: Add Cache to minidns --- build.gradle | 12 ++++ src/main/java/de/measite/minidns/Client.java | 43 +++++++++----- src/main/java/de/measite/minidns/DNSMessage.java | 3 +- src/main/java/de/measite/minidns/Question.java | 73 +++++++++++++++--------- 4 files changed, 88 insertions(+), 43 deletions(-) diff --git a/build.gradle b/build.gradle index 7cf46718..01eafc65 100644 --- a/build.gradle +++ b/build.gradle @@ -24,6 +24,18 @@ if (isSnapshot) { version += '-SNAPSHOT' } +repositories { + mavenLocal() + mavenCentral() + maven { + url 'https://oss.sonatype.org/content/repositories/snapshots/' + } +} + +dependencies { + compile 'org.igniterealtime.jxmpp:jxmpp-util-cache:0.1.0-alpha1-SNAPSHOT' +} + jar { manifest { instruction 'Implementation-GitRevision:', project.ext.gitCommit diff --git a/src/main/java/de/measite/minidns/Client.java b/src/main/java/de/measite/minidns/Client.java index cad3902a..26e3ba01 100644 --- a/src/main/java/de/measite/minidns/Client.java +++ b/src/main/java/de/measite/minidns/Client.java @@ -17,6 +17,8 @@ import java.util.Random; import java.util.logging.Level; import java.util.logging.Logger; +import org.jxmpp.util.cache.ExpirationCache; + import de.measite.minidns.Record.CLASS; import de.measite.minidns.Record.TYPE; @@ -28,6 +30,9 @@ public class Client { private static final Logger LOGGER = Logger.getLogger(Client.class.getName()); + protected static final ExpirationCache cache = new ExpirationCache( + 10, 1000 * 60 * 60 * 24); + /** * The internal random class for sequence generation. */ @@ -67,10 +72,7 @@ public class Client { public DNSMessage query(String name, TYPE type, CLASS clazz, String host, int port) throws IOException { - Question q = new Question(); - q.setClazz(clazz); - q.setType(type); - q.setName(name); + Question q = new Question(name, type, clazz); return query(q, host, port); } @@ -86,10 +88,7 @@ public class Client { public DNSMessage query(String name, TYPE type, CLASS clazz, String host) throws IOException { - Question q = new Question(); - q.setClazz(clazz); - q.setType(type); - q.setName(name); + Question q = new Question(name, type, clazz); return query(q, host); } @@ -102,10 +101,7 @@ public class Client { */ public DNSMessage query(String name, TYPE type, CLASS clazz) { - Question q = new Question(); - q.setClazz(clazz); - q.setType(type); - q.setName(name); + Question q = new Question(name, type, clazz); return query(q); } @@ -127,6 +123,10 @@ public class Client { * @throws IOException On IOErrors. */ public DNSMessage query(Question q, String host, int port) throws IOException { + DNSMessage dnsMessage = cache.get(q); + if (dnsMessage != null) { + return dnsMessage; + } DNSMessage message = new DNSMessage(); message.setQuestions(new Question[]{q}); message.setRecursionDesired(true); @@ -139,10 +139,16 @@ public class Client { socket.send(packet); packet = new DatagramPacket(new byte[bufferSize], bufferSize); socket.receive(packet); - DNSMessage dnsMessage = DNSMessage.parse(packet.getData()); + dnsMessage = DNSMessage.parse(packet.getData()); if (dnsMessage.getId() != message.getId()) { return null; } + for (Record record : dnsMessage.getAnswers()) { + if (record.isAnswer(q)) { + cache.put(q, dnsMessage, record.ttl); + break; + } + } return dnsMessage; } } @@ -152,10 +158,19 @@ public class Client { * @param q The question section of the DNS query. */ public DNSMessage query(Question q) { + // While this query method does in fact re-use query(Question, String) + // we still do a cache lookup here in order to avoid unnecessary + // findDNS()calls, which are expensive on Android. Note that we do not + // put the results back into the Cache, as this is already done by + // query(Question, String). + DNSMessage message = cache.get(q); + if (message != null) { + return message; + } String dnsServer[] = findDNS(); for (String dns : dnsServer) { try { - DNSMessage message = query(q, dns); + message = query(q, dns); if (message == null) { continue; } diff --git a/src/main/java/de/measite/minidns/DNSMessage.java b/src/main/java/de/measite/minidns/DNSMessage.java index 14c8f04b..ea1b25c8 100644 --- a/src/main/java/de/measite/minidns/DNSMessage.java +++ b/src/main/java/de/measite/minidns/DNSMessage.java @@ -416,8 +416,7 @@ public class DNSMessage { int additionalResourceRecordCount = dis.readUnsignedShort(); message.questions = new Question[questionCount]; while (questionCount-- > 0) { - Question q = new Question(); - q.parse(dis, data); + Question q = Question.parse(dis, data); message.questions[questionCount] = q; } message.answers = new Record[answerCount]; diff --git a/src/main/java/de/measite/minidns/Question.java b/src/main/java/de/measite/minidns/Question.java index 9d1e3f56..8efe19f1 100644 --- a/src/main/java/de/measite/minidns/Question.java +++ b/src/main/java/de/measite/minidns/Question.java @@ -11,52 +11,71 @@ import de.measite.minidns.util.NameUtil; public class Question { - private String name; + private final String name; - private TYPE type; + private final TYPE type; - private CLASS clazz = CLASS.IN; + private final CLASS clazz; - public TYPE getType() { - return type; - } + private byte[] byteArray; - public void setType(TYPE type) { + public Question(String name, TYPE type, CLASS clazz) { + this.name = name; this.type = type; + this.clazz = clazz; } - public CLASS getClazz() { - return clazz; + public TYPE getType() { + return type; } - public void setClazz(CLASS clazz) { - this.clazz = clazz; + public CLASS getClazz() { + return clazz; } public String getName() { return name; } - public void setName(String name) { - this.name = name; + public static Question parse(DataInputStream dis, byte[] data) throws IOException { + String name = NameUtil.parse(dis, data); + TYPE type = TYPE.getType(dis.readUnsignedShort()); + CLASS clazz = CLASS.getClass(dis.readUnsignedShort()); + return new Question (name, type, clazz); } - public void parse(DataInputStream dis, byte[] data) throws IOException { - this.name = NameUtil.parse(dis, data); - this.type = TYPE.getType(dis.readUnsignedShort()); - this.clazz = CLASS.getClass(dis.readUnsignedShort()); + public byte[] toByteArray() { + if (byteArray == null) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(512); + DataOutputStream dos = new DataOutputStream(baos); + + try { + dos.write(NameUtil.toByteArray(this.name)); + dos.writeShort(type.getValue()); + dos.writeShort(clazz.getValue()); + dos.flush(); + } catch (IOException e) { + // Should never happen + throw new IllegalStateException(e); + } + byteArray = baos.toByteArray(); + } + return byteArray; } - public byte[] toByteArray() throws IOException { - ByteArrayOutputStream baos = new ByteArrayOutputStream(512); - DataOutputStream dos = new DataOutputStream(baos); - - dos.write(NameUtil.toByteArray(this.name)); - dos.writeShort(type.getValue()); - dos.writeShort(clazz.getValue()); - - dos.flush(); - return baos.toByteArray(); + @Override + public int hashCode() { + return toByteArray().hashCode(); } + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof Question)) { + return false; + } + return this.hashCode() == other.hashCode(); + } } -- cgit v1.2.3 From 4da60e7e2071f41cb35f9eacf048c30f5dbf6ca6 Mon Sep 17 00:00:00 2001 From: Rene Treffer Date: Thu, 12 Jun 2014 22:39:51 +0200 Subject: Simplify cache and extend cache operations. Remove the external cache dependency and use a simple LRU based on LinkedHashMap. Make it possible to get the parse time of DNSMessage, which means we can evaluate the TTL later on :-) --- build.gradle | 7 -- src/main/java/de/measite/minidns/Client.java | 87 +++++++++++++++++++++--- src/main/java/de/measite/minidns/DNSMessage.java | 14 ++++ 3 files changed, 92 insertions(+), 16 deletions(-) diff --git a/build.gradle b/build.gradle index 01eafc65..9de89b7c 100644 --- a/build.gradle +++ b/build.gradle @@ -27,13 +27,6 @@ if (isSnapshot) { repositories { mavenLocal() mavenCentral() - maven { - url 'https://oss.sonatype.org/content/repositories/snapshots/' - } -} - -dependencies { - compile 'org.igniterealtime.jxmpp:jxmpp-util-cache:0.1.0-alpha1-SNAPSHOT' } jar { diff --git a/src/main/java/de/measite/minidns/Client.java b/src/main/java/de/measite/minidns/Client.java index 26e3ba01..3f152346 100644 --- a/src/main/java/de/measite/minidns/Client.java +++ b/src/main/java/de/measite/minidns/Client.java @@ -13,12 +13,12 @@ import java.security.SecureRandom; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map.Entry; import java.util.Random; import java.util.logging.Level; import java.util.logging.Logger; -import org.jxmpp.util.cache.ExpirationCache; - import de.measite.minidns.Record.CLASS; import de.measite.minidns.Record.TYPE; @@ -30,9 +30,6 @@ public class Client { private static final Logger LOGGER = Logger.getLogger(Client.class.getName()); - protected static final ExpirationCache cache = new ExpirationCache( - 10, 1000 * 60 * 60 * 24); - /** * The internal random class for sequence generation. */ @@ -48,6 +45,16 @@ public class Client { */ protected int timeout = 5000; + /** + * The internal DNS cache. + */ + protected LinkedHashMap cache; + + /** + * Maximum acceptable ttl. + */ + protected long maxTTL = 60 * 60 * 1000; + /** * Create a new DNS client. */ @@ -57,6 +64,7 @@ public class Client { } catch (NoSuchAlgorithmException e1) { random = new SecureRandom(); } + setCacheSize(10); } /** @@ -123,9 +131,20 @@ public class Client { * @throws IOException On IOErrors. */ public DNSMessage query(Question q, String host, int port) throws IOException { - DNSMessage dnsMessage = cache.get(q); - if (dnsMessage != null) { - return dnsMessage; + DNSMessage dnsMessage = (cache == null) ? null : cache.get(q); + if (dnsMessage != null && dnsMessage.getReceiveTimestamp() > 0l) { + // check the ttl + long ttl = maxTTL; + for (Record r : dnsMessage.getAnswers()) { + ttl = Math.min(ttl, r.ttl); + } + for (Record r : dnsMessage.getAdditionalResourceRecords()) { + ttl = Math.min(ttl, r.ttl); + } + if (dnsMessage.getReceiveTimestamp() + ttl < + System.currentTimeMillis()) { + return dnsMessage; + } } DNSMessage message = new DNSMessage(); message.setQuestions(new Question[]{q}); @@ -145,7 +164,9 @@ public class Client { } for (Record record : dnsMessage.getAnswers()) { if (record.isAnswer(q)) { - cache.put(q, dnsMessage, record.ttl); + if (cache != null) { + cache.put(q, dnsMessage); + } break; } } @@ -305,4 +326,52 @@ public class Client { return null; } + /** + * Configure the cache size (default 10). + * @param maximumSize The new cache size or 0 to disable. + */ + @SuppressWarnings("serial") + public void setCacheSize(final int maximumSize) { + if (maximumSize == 0) { + this.cache = null; + } else { + LinkedHashMap old = cache; + cache = new LinkedHashMap() { + @Override + protected boolean removeEldestEntry( + Entry eldest) { + return size() > maximumSize; + } + }; + if (old != null) { + cache.putAll(old); + } + } + } + + /** + * Flush the DNS cache. + */ + public void flushCache() { + if (cache != null) { + cache.clear(); + } + } + + /** + * Get the current maximum record ttl. + * @return The maximum record ttl. + */ + public long getMaxTTL() { + return maxTTL; + } + + /** + * Set the maximum record ttl. + * @param maxTTL The new maximum ttl. + */ + public void setMaxTTL(long maxTTL) { + this.maxTTL = maxTTL; + } + } diff --git a/src/main/java/de/measite/minidns/DNSMessage.java b/src/main/java/de/measite/minidns/DNSMessage.java index ea1b25c8..977c08d0 100644 --- a/src/main/java/de/measite/minidns/DNSMessage.java +++ b/src/main/java/de/measite/minidns/DNSMessage.java @@ -195,6 +195,11 @@ public class DNSMessage { */ protected Record additionalResourceRecords[]; + /** + * The receive timestamp of this message. + */ + protected long receiveTimestamp; + /** * Retrieve the current DNS message id. * @return The current DNS message id. @@ -211,6 +216,14 @@ public class DNSMessage { this.id = id & 0xffff; } + /** + * Get the receive timestamp if this message was created via parse. + * This should be used to evaluate TTLs. + */ + public long getReceiveTimestamp() { + return receiveTimestamp; + } + /** * Retrieve the query type (true or false; * @return True if this DNS message is a query. @@ -410,6 +423,7 @@ public class DNSMessage { message.authenticData = ((header >> 5) & 1) == 1; message.checkDisabled = ((header >> 4) & 1) == 1; message.responseCode = RESPONSE_CODE.getResponseCode(header & 0xf); + message.receiveTimestamp = System.currentTimeMillis(); int questionCount = dis.readUnsignedShort(); int answerCount = dis.readUnsignedShort(); int nameserverCount = dis.readUnsignedShort(); -- cgit v1.2.3 From 8e848f7afdb1f4ac8ac5a56d20e7ea833c5c840c Mon Sep 17 00:00:00 2001 From: Rene Treffer Date: Fri, 13 Jun 2014 17:51:45 +0200 Subject: Switch to deticated DNSCache interface + reference LRU implementation. --- src/main/java/de/measite/minidns/Client.java | 86 +++------------ src/main/java/de/measite/minidns/DNSCache.java | 12 +++ src/main/java/de/measite/minidns/LRUCache.java | 139 +++++++++++++++++++++++++ 3 files changed, 165 insertions(+), 72 deletions(-) create mode 100644 src/main/java/de/measite/minidns/DNSCache.java create mode 100644 src/main/java/de/measite/minidns/LRUCache.java diff --git a/src/main/java/de/measite/minidns/Client.java b/src/main/java/de/measite/minidns/Client.java index 3f152346..462d52ff 100644 --- a/src/main/java/de/measite/minidns/Client.java +++ b/src/main/java/de/measite/minidns/Client.java @@ -13,8 +13,6 @@ import java.security.SecureRandom; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.Map.Entry; import java.util.Random; import java.util.logging.Level; import java.util.logging.Logger; @@ -48,23 +46,26 @@ public class Client { /** * The internal DNS cache. */ - protected LinkedHashMap cache; + protected DNSCache cache; /** - * Maximum acceptable ttl. + * Create a new DNS client with the given DNS cache. + * @param cache The backend DNS cache. */ - protected long maxTTL = 60 * 60 * 1000; - - /** - * Create a new DNS client. - */ - public Client() { + public Client(DNSCache cache) { try { random = SecureRandom.getInstance("SHA1PRNG"); } catch (NoSuchAlgorithmException e1) { random = new SecureRandom(); } - setCacheSize(10); + this.cache = cache; + } + + /** + * Create a new DNS client. + */ + public Client() { + this(null); } /** @@ -132,19 +133,8 @@ public class Client { */ public DNSMessage query(Question q, String host, int port) throws IOException { DNSMessage dnsMessage = (cache == null) ? null : cache.get(q); - if (dnsMessage != null && dnsMessage.getReceiveTimestamp() > 0l) { - // check the ttl - long ttl = maxTTL; - for (Record r : dnsMessage.getAnswers()) { - ttl = Math.min(ttl, r.ttl); - } - for (Record r : dnsMessage.getAdditionalResourceRecords()) { - ttl = Math.min(ttl, r.ttl); - } - if (dnsMessage.getReceiveTimestamp() + ttl < - System.currentTimeMillis()) { - return dnsMessage; - } + if (dnsMessage != null) { + return dnsMessage; } DNSMessage message = new DNSMessage(); message.setQuestions(new Question[]{q}); @@ -326,52 +316,4 @@ public class Client { return null; } - /** - * Configure the cache size (default 10). - * @param maximumSize The new cache size or 0 to disable. - */ - @SuppressWarnings("serial") - public void setCacheSize(final int maximumSize) { - if (maximumSize == 0) { - this.cache = null; - } else { - LinkedHashMap old = cache; - cache = new LinkedHashMap() { - @Override - protected boolean removeEldestEntry( - Entry eldest) { - return size() > maximumSize; - } - }; - if (old != null) { - cache.putAll(old); - } - } - } - - /** - * Flush the DNS cache. - */ - public void flushCache() { - if (cache != null) { - cache.clear(); - } - } - - /** - * Get the current maximum record ttl. - * @return The maximum record ttl. - */ - public long getMaxTTL() { - return maxTTL; - } - - /** - * Set the maximum record ttl. - * @param maxTTL The new maximum ttl. - */ - public void setMaxTTL(long maxTTL) { - this.maxTTL = maxTTL; - } - } diff --git a/src/main/java/de/measite/minidns/DNSCache.java b/src/main/java/de/measite/minidns/DNSCache.java new file mode 100644 index 00000000..78e6c041 --- /dev/null +++ b/src/main/java/de/measite/minidns/DNSCache.java @@ -0,0 +1,12 @@ +package de.measite.minidns; + +/** + * Cache for DNS Entries. Implementations must be thread safe. + */ +public interface DNSCache { + + void put(Question q, DNSMessage message); + + DNSMessage get(Question q); + +} diff --git a/src/main/java/de/measite/minidns/LRUCache.java b/src/main/java/de/measite/minidns/LRUCache.java new file mode 100644 index 00000000..132bf794 --- /dev/null +++ b/src/main/java/de/measite/minidns/LRUCache.java @@ -0,0 +1,139 @@ +package de.measite.minidns; + +import java.util.LinkedHashMap; +import java.util.Map.Entry; + +/** + * LRU based DNSCache backed by a LinkedHashMap. + */ +public class LRUCache implements DNSCache { + + /** + * Internal miss count. + */ + protected long missCount = 0l; + + /** + * Internal expire count (subset of misses that was caused by expire). + */ + protected long expireCount = 0l; + + /** + * Internal hit count. + */ + protected long hitCount = 0l; + + /** + * The internal capacity of the backend cache. + */ + protected int capacity; + + /** + * The upper bound of the ttl. All longer TTLs will be capped by this ttl. + */ + protected long maxTTL; + + /** + * The backend cache. + */ + protected LinkedHashMap backend; + + /** + * Create a new LRUCache with given capacity and upper bound ttl. + * @param capacity The internal capacity. + * @param maxTTL The upper bound for any ttl. + */ + @SuppressWarnings("serial") + public LRUCache(final int capacity, final long maxTTL) { + this.capacity = capacity; + this.maxTTL = maxTTL; + backend = new LinkedHashMap( + Math.min(capacity, 11), 0.75f, true) + { + @Override + protected boolean removeEldestEntry( + Entry eldest) { + return size() > capacity; + } + }; + } + + /** + * Create a new LRUCache with given capacity. + * @param capacity + */ + public LRUCache(final int capacity) { + this(capacity, Long.MAX_VALUE); + } + + @Override + public synchronized void put(Question q, DNSMessage message) { + if (message.getReceiveTimestamp() <= 0l) { + return; + } + backend.put(q, message); + } + + @Override + public synchronized DNSMessage get(Question q) { + DNSMessage message = backend.get(q); + if (message == null) { + missCount++; + return null; + } + + long ttl = maxTTL; + for (Record r : message.getAnswers()) { + ttl = Math.min(ttl, r.ttl); + } + for (Record r : message.getAdditionalResourceRecords()) { + ttl = Math.min(ttl, r.ttl); + } + if (message.getReceiveTimestamp() + ttl > System.currentTimeMillis()) { + missCount++; + expireCount++; + backend.remove(q); + return null; + } else { + hitCount++; + return message; + } + } + + /** + * Clear all entries in this cache. + */ + public synchronized void clear() { + backend.clear(); + missCount = 0l; + hitCount = 0l; + expireCount = 0l; + } + + /** + * Get the miss count of this cache which is the number of fruitless + * get calls since this cache was last resetted. + * @return The number of cache misses. + */ + public long getMissCount() { + return missCount; + } + + /** + * The number of expires (cache hits that have had a ttl to low to be + * retrieved). + * @return The expire count. + */ + public long getExpireCount() { + return expireCount; + } + + /** + * The cache hit count (all sucessful calls to get). + * @return The hit count. + */ + public long getHitCount() { + return hitCount; + } + +} -- cgit v1.2.3 From 028700efe0345e764f598378cb64c5e57f980e33 Mon Sep 17 00:00:00 2001 From: Rene Treffer Date: Fri, 13 Jun 2014 17:54:54 +0200 Subject: Make sure we initialize big enough if s.o. chooses small values --- src/main/java/de/measite/minidns/LRUCache.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/de/measite/minidns/LRUCache.java b/src/main/java/de/measite/minidns/LRUCache.java index 132bf794..0262e4c5 100644 --- a/src/main/java/de/measite/minidns/LRUCache.java +++ b/src/main/java/de/measite/minidns/LRUCache.java @@ -48,7 +48,7 @@ public class LRUCache implements DNSCache { this.capacity = capacity; this.maxTTL = maxTTL; backend = new LinkedHashMap( - Math.min(capacity, 11), 0.75f, true) + Math.min(capacity + (capacity + 3) / 4 + 2, 11), 0.75f, true) { @Override protected boolean removeEldestEntry( -- cgit v1.2.3 From 289d48b633b7493f2a760583c88670b9fc4ef96f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Sun, 22 Jun 2014 12:01:13 +0200 Subject: Add transifex config --- .tx/config | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .tx/config diff --git a/.tx/config b/.tx/config new file mode 100644 index 00000000..9e6de616 --- /dev/null +++ b/.tx/config @@ -0,0 +1,8 @@ +[main] +host = https://www.transifex.com +lang_map = af_ZA: af-rZA, am_ET: am-rET, ar_AE: ar-rAE, ar_BH: ar-rBH, ar_DZ: ar-rDZ, ar_EG: ar-rEG, ar_IQ: ar-rIQ, ar_JO: ar-rJO, ar_KW: ar-rKW, ar_LB: ar-rLB, ar_LY: ar-rLY, ar_MA: ar-rMA, ar_OM: ar-rOM, ar_QA: ar-rQA, ar_SA: ar-rSA, ar_SY: ar-rSY, ar_TN: ar-rTN, ar_YE: ar-rYE, arn_CL: arn-rCL, as_IN: as-rIN, az_AZ: az-rAZ, ba_RU: ba-rRU, be_BY: be-rBY, bg_BG: bg-rBG, bn_BD: bn-rBD, bn_IN: bn-rIN, bo_CN: bo-rCN, br_FR: br-rFR, bs_BA: bs-rBA, ca_ES: ca-rES, co_FR: co-rFR, cs_CZ: cs-rCZ, cy_GB: cy-rGB, da_DK: da-rDK, de_AT: de-rAT, de_CH: de-rCH, de_DE: de-rDE, de_LI: de-rLI, de_LU: de-rLU, dsb_DE: dsb-rDE, dv_MV: dv-rMV, el_GR: el-rGR, en_AU: en-rAU, en_BZ: en-rBZ, en_CA: en-rCA, en_GB: en-rGB, en_IE: en-rIE, en_IN: en-rIN, en_JM: en-rJM, en_MY: en-rMY, en_NZ: en-rNZ, en_PH: en-rPH, en_SG: en-rSG, en_TT: en-rTT, en_US: en-rUS, en_ZA: en-rZA, en_ZW: en-rZW, es_AR: es-rAR, es_BO: es-rBO, es_CL: es-rCL, es_CO: es-rCO, es_CR: es-rCR, es_DO: es-rDO, es_EC: es-rEC, es_ES: es-rES, es_GT: es-rGT, es_HN: es-rHN, es_MX: es-rMX, es_NI: es-rNI, es_PA: es-rPA, es_PE: es-rPE, es_PR: es-rPR, es_PY: es-rPY, es_SV: es-rSV, es_US: es-rUS, es_UY: es-rUY, es_VE: es-rVE, et_EE: et-rEE, eu_ES: eu-rES, fa_IR: fa-rIR, fi_FI: fi-rFI, fil_PH: fil-rPH, fo_FO: fo-rFO, fr_BE: fr-rBE, fr_CA: fr-rCA, fr_CH: fr-rCH, fr_FR: fr-rFR, fr_LU: fr-rLU, fr_MC: fr-rMC, fy_NL: fy-rNL, ga_IE: ga-rIE, gd_GB: gd-rGB, gl_ES: gl-rES, gsw_FR: gsw-rFR, gu_IN: gu-rIN, ha_NG: ha-rNG, hi_IN: hi-rIN, hr_BA: hr-rBA, hr_HR: hr-rHR, hsb_DE: hsb-rDE, hu_HU: hu-rHU, hy_AM: hy-rAM, id_ID: id-rID, ig_NG: ig-rNG, ii_CN: ii-rCN, is_IS: is-rIS, it_CH: it-rCH, it_IT: it-rIT, iu_CA: iu-rCA, ja_JP: ja-rJP, ka_GE: ka-rGE, kk_KZ: kk-rKZ, kl_GL: kl-rGL, km_KH: km-rKH, kn_IN: kn-rIN, ko_KR: ko-rKR, kok_IN: kok-rIN, ky_KG: ky-rKG, lb_LU: lb-rLU, lo_LA: lo-rLA, lt_LT: lt-rLT, lv_LV: lv-rLV, mi_NZ: mi-rNZ, mk_MK: mk-rMK, ml_IN: ml-rIN, mn_CN: mn-rCN, mn_MN: mn-rMN, moh_CA: moh-rCA, mr_IN: mr-rIN, ms_BN: ms-rBN, ms_MY: ms-rMY, mt_MT: mt-rMT, nb_NO: nb-rNO, ne_NP: ne-rNP, nl_BE: nl-rBE, nl_NL: nl-rNL, nn_NO: nn-rNO, nso_ZA: nso-rZA, oc_FR: oc-rFR, or_IN: or-rIN, pa_IN: pa-rIN, pl_PL: pl-rPL, prs_AF: prs-rAF, ps_AF: ps-rAF, pt_BR: pt-rBR, pt_PT: pt-rPT, qut_GT: qut-rGT, quz_BO: quz-rBO, quz_EC: quz-rEC, quz_PE: quz-rPE, rm_CH: rm-rCH, ro_RO: ro-rRO, ru_RU: ru-rRU, rw_RW: rw-rRW, sa_IN: sa-rIN, sah_RU: sah-rRU, se_FI: se-rFI, se_NO: se-rNO, se_SE: se-rSE, si_LK: si-rLK, sk_SK: sk-rSK, sl_SI: sl-rSI, sma_NO: sma-rNO, sma_SE: sma-rSE, smj_NO: smj-rNO, smj_SE: smj-rSE, smn_FI: smn-rFI, sms_FI: sms-rFI, sq_AL: sq-rAL, sr_BA: sr-rBA, sr_CS: sr-rCS, sr_ME: sr-rME, sr_RS: sr-rRS, sv_FI: sv-rFI, sv_SE: sv-rSE, sw_KE: sw-rKE, syr_SY: syr-rSY, ta_IN: ta-rIN, te_IN: te-rIN, tg_TJ: tg-rTJ, th_TH: th-rTH, tk_TM: tk-rTM, tn_ZA: tn-rZA, tr_TR: tr-rTR, tt_RU: tt-rRU, tzm_DZ: tzm-rDZ, ug_CN: ug-rCN, uk_UA: uk-rUA, ur_PK: ur-rPK, uz_UZ: uz-rUZ, vi_VN: vi-rVN, wo_SN: wo-rSN, xh_ZA: xh-rZA, yo_NG: yo-rNG, zh_CN: zh-rCN, zh_HK: zh-rHK, zh_MO: zh-rMO, zh_SG: zh-rSG, zh_TW: zh-rTW, zu_ZA: zu-rZA, no_NO: no-rNO, he_IL: iw-rIL, he: iw + +[open-keychain.api-strings] +file_filter = res/values-/strings.xml +source_file = res/values/strings.xml +source_lang = en -- cgit v1.2.3 From 0e484dd17f85d923891460ff606b13294e8985ba Mon Sep 17 00:00:00 2001 From: Rene Treffer Date: Sun, 22 Jun 2014 15:29:51 +0200 Subject: Add missing javadoc and review new code --- src/main/java/de/measite/minidns/DNSCache.java | 11 ++++ src/main/java/de/measite/minidns/Question.java | 60 +++++++++++++++++++++- src/main/java/de/measite/minidns/Record.java | 8 +++ src/main/java/de/measite/minidns/record/A.java | 6 +++ src/main/java/de/measite/minidns/record/AAAA.java | 6 +++ src/main/java/de/measite/minidns/record/CNAME.java | 6 ++- src/main/java/de/measite/minidns/record/Data.java | 18 +++++++ src/main/java/de/measite/minidns/record/NS.java | 3 ++ src/main/java/de/measite/minidns/record/SRV.java | 54 ++++++++++++++++++- .../java/de/measite/minidns/util/NameUtil.java | 40 ++++++++++++++- 10 files changed, 205 insertions(+), 7 deletions(-) diff --git a/src/main/java/de/measite/minidns/DNSCache.java b/src/main/java/de/measite/minidns/DNSCache.java index 78e6c041..14a3a776 100644 --- a/src/main/java/de/measite/minidns/DNSCache.java +++ b/src/main/java/de/measite/minidns/DNSCache.java @@ -5,8 +5,19 @@ package de.measite.minidns; */ public interface DNSCache { + /** + * Add an an dns answer/response for a given dns question. Implementations + * should honor the ttl / receive timestamp. + * @param q The question. + * @param message The dns message. + */ void put(Question q, DNSMessage message); + /** + * Request a cached dns response. + * @param q The dns question. + * @return The dns message. + */ DNSMessage get(Question q); } diff --git a/src/main/java/de/measite/minidns/Question.java b/src/main/java/de/measite/minidns/Question.java index 8efe19f1..62862251 100644 --- a/src/main/java/de/measite/minidns/Question.java +++ b/src/main/java/de/measite/minidns/Question.java @@ -4,39 +4,89 @@ import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; +import java.util.Arrays; import de.measite.minidns.Record.CLASS; import de.measite.minidns.Record.TYPE; import de.measite.minidns.util.NameUtil; +/** + * A DNS question (request). + */ public class Question { + /** + * The question string (e.g. "measite.de"). + */ private final String name; + /** + * The question type (e.g. A). + */ private final TYPE type; + /** + * The question class (usually IN / internet). + */ private final CLASS clazz; + /** + * Cache for the serialized object. + */ private byte[] byteArray; + /** + * Create a dns question for the given name/type/class. + * @param name The name e.g. "measite.de". + * @param type The type, e.g. A. + * @param clazz The class, usually IN (internet). + */ public Question(String name, TYPE type, CLASS clazz) { this.name = name; this.type = type; this.clazz = clazz; } + /** + * Create a dns question for the given name/type/IN (internet class). + * @param name The name e.g. "measite.de". + * @param type The type, e.g. A. + */ + public Question(String name, TYPE type) { + this(name, type, CLASS.IN); + } + + /** + * Retrieve the type of this question. + * @return The type. + */ public TYPE getType() { return type; } + /** + * Retrieve the class of this dns question (usually internet). + * @return The class of this dns question. + */ public CLASS getClazz() { return clazz; } + /** + * Retrieve the name of this dns question (e.g. "measite.de"). + * @return The name of this dns question. + */ public String getName() { return name; } + /** + * Parse a byte array and rebuild the dns question from it. + * @param dis The input stream. + * @param data The plain data (for dns name references). + * @return The parsed dns question. + * @throws IOException On errors (read outside of packet). + */ public static Question parse(DataInputStream dis, byte[] data) throws IOException { String name = NameUtil.parse(dis, data); TYPE type = TYPE.getType(dis.readUnsignedShort()); @@ -44,6 +94,10 @@ public class Question { return new Question (name, type, clazz); } + /** + * Generate a binary paket for this dns question. + * @return The dns question. + */ public byte[] toByteArray() { if (byteArray == null) { ByteArrayOutputStream baos = new ByteArrayOutputStream(512); @@ -65,7 +119,7 @@ public class Question { @Override public int hashCode() { - return toByteArray().hashCode(); + return Arrays.hashCode(toByteArray()); } @Override @@ -76,6 +130,8 @@ public class Question { if (!(other instanceof Question)) { return false; } - return this.hashCode() == other.hashCode(); + byte t[] = toByteArray(); + byte o[] = ((Question)other).toByteArray(); + return Arrays.equals(t, o); } } diff --git a/src/main/java/de/measite/minidns/Record.java b/src/main/java/de/measite/minidns/Record.java index fb0b5d5f..36d3db1a 100644 --- a/src/main/java/de/measite/minidns/Record.java +++ b/src/main/java/de/measite/minidns/Record.java @@ -286,10 +286,18 @@ public class Record { (q.getName().equals(name)); } + /** + * The generic record name, e.g. "measite.de". + * @return The record name. + */ public String getName() { return name; } + /** + * The payload data, usually a subclass of data (A, AAAA, CNAME, ...). + * @return The payload data. + */ public Data getPayload() { return payloadData; } diff --git a/src/main/java/de/measite/minidns/record/A.java b/src/main/java/de/measite/minidns/record/A.java index a85a7af0..4311c651 100644 --- a/src/main/java/de/measite/minidns/record/A.java +++ b/src/main/java/de/measite/minidns/record/A.java @@ -5,8 +5,14 @@ import java.io.IOException; import de.measite.minidns.Record.TYPE; +/** + * A record payload (ip pointer). + */ public class A implements Data { + /** + * Target IP. + */ private byte[] ip; @Override diff --git a/src/main/java/de/measite/minidns/record/AAAA.java b/src/main/java/de/measite/minidns/record/AAAA.java index d89147b2..e4fd5ecf 100644 --- a/src/main/java/de/measite/minidns/record/AAAA.java +++ b/src/main/java/de/measite/minidns/record/AAAA.java @@ -5,8 +5,14 @@ import java.io.IOException; import de.measite.minidns.Record.TYPE; +/** + * AAAA payload (an ipv6 pointer). + */ public class AAAA implements Data { + /** + * The ipv6 address. + */ private byte[] ip; @Override diff --git a/src/main/java/de/measite/minidns/record/CNAME.java b/src/main/java/de/measite/minidns/record/CNAME.java index 4657b4a5..1ac27814 100644 --- a/src/main/java/de/measite/minidns/record/CNAME.java +++ b/src/main/java/de/measite/minidns/record/CNAME.java @@ -6,6 +6,9 @@ import java.io.IOException; import de.measite.minidns.Record.TYPE; import de.measite.minidns.util.NameUtil; +/** + * CNAME payload (pointer to another domain / address). + */ public class CNAME implements Data { protected String name; @@ -20,8 +23,7 @@ public class CNAME implements Data { @Override public byte[] toByteArray() { - // TODO Auto-generated method stub - return null; + throw new UnsupportedOperationException("Not implemented yet"); } @Override diff --git a/src/main/java/de/measite/minidns/record/Data.java b/src/main/java/de/measite/minidns/record/Data.java index 9cb80374..7f2db03a 100644 --- a/src/main/java/de/measite/minidns/record/Data.java +++ b/src/main/java/de/measite/minidns/record/Data.java @@ -5,12 +5,30 @@ import java.io.IOException; import de.measite.minidns.Record.TYPE; +/** + * Generic payload class. + */ public interface Data { + /** + * The payload type. + * @return The payload type. + */ TYPE getType(); + /** + * Binary representation of this payload. + * @return The binary representation of this payload. + */ byte[] toByteArray(); + /** + * Parse this payload. + * @param dis The input stream. + * @param data The plain data (needed for name cross references). + * @param length The payload length. + * @throws IOException on io error (read past paket boundary). + */ void parse(DataInputStream dis, byte data[], int length) throws IOException; } diff --git a/src/main/java/de/measite/minidns/record/NS.java b/src/main/java/de/measite/minidns/record/NS.java index bf07e8c4..8ac2d4c3 100644 --- a/src/main/java/de/measite/minidns/record/NS.java +++ b/src/main/java/de/measite/minidns/record/NS.java @@ -2,6 +2,9 @@ package de.measite.minidns.record; import de.measite.minidns.Record.TYPE; +/** + * Nameserver record. + */ public class NS extends CNAME { @Override diff --git a/src/main/java/de/measite/minidns/record/SRV.java b/src/main/java/de/measite/minidns/record/SRV.java index 32b70c4d..707bf3f5 100644 --- a/src/main/java/de/measite/minidns/record/SRV.java +++ b/src/main/java/de/measite/minidns/record/SRV.java @@ -6,49 +6,99 @@ import java.io.IOException; import de.measite.minidns.Record.TYPE; import de.measite.minidns.util.NameUtil; +/** + * SRV record payload (service pointer). + */ public class SRV implements Data { + /** + * The priority of this service. + */ protected int priority; + + /** + * The weight of this service. + */ protected int weight; + + /** + * The target port. + */ protected int port; + + /** + * The target server. + */ protected String name; + /** + * The priority of this service. Lower values mean higher priority. + * @return The priority. + */ public int getPriority() { return priority; } + /** + * Set the priority of this service entry. Lower values have higher priority. + * @param priority The new priority. + */ public void setPriority(int priority) { this.priority = priority; } + /** + * The weight of this service. Services with the same priority should be + * balanced based on weight. + * @return The weight of this service. + */ public int getWeight() { return weight; } + /** + * Set the weight of this service. + * @param weight The new weight of this service. + */ public void setWeight(int weight) { this.weight = weight; } + /** + * The target port of this service. + * @return The target port of this service. + */ public int getPort() { return port; } + /** + * Set the target port of this service. + * @param port The new target port. + */ public void setPort(int port) { this.port = port; } + /** + * The name of the target server. + * @return The target servers name. + */ public String getName() { return name; } + /** + * Set the name of the target server. + * @param name The new target servers name. + */ public void setName(String name) { this.name = name; } @Override public byte[] toByteArray() { - // TODO Auto-generated method stub - return null; + throw new UnsupportedOperationException("Not implemented yet"); } @Override diff --git a/src/main/java/de/measite/minidns/util/NameUtil.java b/src/main/java/de/measite/minidns/util/NameUtil.java index 91a6649d..7ae373bc 100644 --- a/src/main/java/de/measite/minidns/util/NameUtil.java +++ b/src/main/java/de/measite/minidns/util/NameUtil.java @@ -8,12 +8,28 @@ import java.net.IDN; import java.util.HashSet; import java.util.Arrays; +/** + * Utilities related to internationalized domain names and dns name handling. + */ public class NameUtil { + /** + * Retrieve the rough binary length of a string + * (length + 2 bytes length prefix). + * @param name The name string. + * @return The binary size of the string (length + 2). + */ public static int size(String name) { return name.length() + 2; } + /** + * Check if two internationalized domain names are equal, possibly causing + * a serialization of both domain names. + * @param name1 The first domain name. + * @param name2 The second domain name. + * @return True if both domain names are the same. + */ public static boolean idnEquals(String name1, String name2) { if (name1 == name2) return true; // catches null, null if (name1 == null) return false; @@ -27,6 +43,12 @@ public class NameUtil { } } + /** + * Serialize a domain name under IDN rules. + * @param name The domain name. + * @return The binary domain name representation. + * @throws IOException Should never happen. + */ public static byte[] toByteArray(String name) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(64); DataOutputStream dos = new DataOutputStream(baos); @@ -38,8 +60,16 @@ public class NameUtil { dos.writeByte(0); dos.flush(); return baos.toByteArray(); - } + } + /** + * Parse a domain name starting at the current offset and moving the input + * stream pointer past this domain name (even if cross references occure). + * @param dis The input stream. + * @param data The raw data (for cross references). + * @return The domain name string. + * @throws IOException Should never happen. + */ public static String parse(DataInputStream dis, byte data[]) throws IOException { @@ -63,6 +93,14 @@ public class NameUtil { return s; } + /** + * Parse a domain name starting at the given offset. + * @param data The raw data. + * @param offset The offset. + * @param jumps The list of jumps (by now). + * @return The parsed domain name. + * @throws IllegalStateException on cycles. + */ public static String parse( byte data[], int offset, -- cgit v1.2.3 From dcf62a8ac59d84072e66e71ec8a5d137784e760d Mon Sep 17 00:00:00 2001 From: Rene Treffer Date: Sun, 22 Jun 2014 16:36:19 +0200 Subject: Prepare 0.1.0 snapshot --- build.gradle | 166 ++++++++--------------- src/main/java/de/measite/minidns/DNSMessage.java | 4 +- src/main/java/de/measite/minidns/Record.java | 2 +- 3 files changed, 58 insertions(+), 114 deletions(-) diff --git a/build.gradle b/build.gradle index 9de89b7c..3345e500 100644 --- a/build.gradle +++ b/build.gradle @@ -1,27 +1,28 @@ apply plugin: 'java' apply plugin: 'eclipse' -apply plugin: 'maven' apply plugin: 'osgi' -apply plugin: 'signing' +apply plugin: 'nexus' -ext { - shortVersion = '0.1' - isSnapshot = true - gitCommit = getGitCommit() - isReleaseVersion = !shortVersion - sonatypeCredentialsAvailable = project.hasProperty('sonatypeUsername') && project.hasProperty('sonatypePassword') - signingRequired = isReleaseVersion - sonatypeSnapshotUrl = 'https://oss.sonatype.org/content/repositories/snapshots' - sonatypeStagingUrl = 'https://oss.sonatype.org/service/local/staging/deploy/maven2' - buildDate = (new java.text.SimpleDateFormat("yyyy-MM-dd")).format(new Date()) +buildscript { + repositories { + jcenter() + mavenLocal() + mavenCentral() + } + + dependencies { + classpath 'org.gradle.api.plugins:gradle-nexus-plugin:0.7.1' + } } group = 'de.measite.minidns' description = "A minimal DNS client library with support for A, AAAA, NS and SRV records" sourceCompatibility = 1.7 -version = shortVersion -if (isSnapshot) { - version += '-SNAPSHOT' +version = 'git tag --points-at HEAD'.execute().text.trim() +isSNAPSHOT = 'git rev-parse --abbrev-ref HEAD'.execute().text.trim() == 'master' + +if (isSNAPSHOT) { + version = version + '-SNAPSHOT' } repositories { @@ -29,103 +30,46 @@ repositories { mavenCentral() } -jar { - manifest { - instruction 'Implementation-GitRevision:', project.ext.gitCommit - } -} - -gradle.taskGraph.whenReady { taskGraph -> - if (signingRequired - && taskGraph.allTasks.any { it instanceof Sign }) { - // Use Java 6's console to read from the console (no good for a CI environment) - Console console = System.console() - console.printf '\n\nWe have to sign some things in this build.\n\nPlease enter your signing details.\n\n' - def password = console.readPassword('GnuPG Private Key Password: ') - - allprojects { ext.'signing.password' = password } - - console.printf '\nThanks.\n\n' - } +nexus { + attachSources = false + attachTests = false + attachJavadoc = false + sign = true } -uploadArchives { - repositories { - mavenDeployer { - if (signingRequired) { - beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } - } - repository(url: project.sonatypeStagingUrl) { - if (sonatypeCredentialsAvailable) { - authentication(userName: sonatypeUsername, password: sonatypePassword) - } - } - snapshotRepository(url: project.sonatypeSnapshotUrl) { - if (sonatypeCredentialsAvailable) { - authentication(userName: sonatypeUsername, password: sonatypePassword) - } - } - - pom.project { - name 'minidns' - packaging 'jar' - inceptionYear '2014' - url 'https://github.com/rtreffer/minidns' - description project.description - - issueManagement { - system 'GitHub' - url 'https://github.com/rtreffer/minidns/issues' - } - - distributionManagement { - snapshotRepository { - id 'minidns.snapshot' - url project.sonatypeSnapshotUrl - } - } - - scm { - url 'https://github.com/rtreffer/minidns' - connection 'scm:git:https://github.com/rtreffer/minidns.git' - developerConnection 'scm:git:https://github.com/rtreffer/minidns.git' - } - - licenses { - license { - name 'The Apache Software License, Version 2.0' - url 'http://www.apache.org/licenses/LICENSE-2.0.txt' - distribution 'repo' - } - } - - developers { - developer { - id 'rtreffer' - name 'Rene Treffer' - } - developer { - id 'flow' - name 'Florian Schmaus' - email 'flow@geekplace.eu' - } - } - } - } - } - signing { - required { signingRequired } - sign configurations.archives - } +modifyPom { + project { + name 'minidns' + description 'Minimal DNS library for java and android systems' + url 'https://github.com/rtreffer/minidns' + inceptionYear '2014' + + scm { + url 'https://github.com/rtreffer/minidns' + connection 'scm:https://github.com/rtreffer/minidns' + developerConnection 'scm:git://github.com/rtreffer/minidns.git' + } + + licenses { + license { + name 'The Apache Software License, Version 2.0' + url 'http://www.apache.org/licenses/LICENSE-2.0.txt' + distribution 'repo' + } + } + + developers { + developer { + id 'rtreffer' + name 'Rene Treffer' + email 'treffer@measite.de' + } + developer { + id 'flow' + name 'Florian Schmaus' + email 'flow@geekplace.eu' + } + } + } } -def getGitCommit() { - def dotGit = new File("$projectDir/.git") - if (!dotGit.isDirectory()) return 'non-git build' - - def cmd = 'git describe --all --dirty=+' - def proc = cmd.execute() - def gitCommit = proc.text.trim() - assert !gitCommit.isEmpty() - gitCommit -} diff --git a/src/main/java/de/measite/minidns/DNSMessage.java b/src/main/java/de/measite/minidns/DNSMessage.java index 977c08d0..6e0ae6c8 100644 --- a/src/main/java/de/measite/minidns/DNSMessage.java +++ b/src/main/java/de/measite/minidns/DNSMessage.java @@ -11,8 +11,8 @@ import java.util.Arrays; * A DNS message as defined by rfc1035. The message consists of a header and * 4 sections: question, answer, nameserver and addition resource record * section. - * A message can either be parsed ({@see #parse(byte[])}) or serialized - * ({@see #toArray()}). + * A message can either be parsed ({@link DNSMessage#parse(byte[])}) or serialized + * ({@link DNSMessage#toArray()}). */ public class DNSMessage { diff --git a/src/main/java/de/measite/minidns/Record.java b/src/main/java/de/measite/minidns/Record.java index 36d3db1a..58740b7d 100644 --- a/src/main/java/de/measite/minidns/Record.java +++ b/src/main/java/de/measite/minidns/Record.java @@ -19,7 +19,7 @@ public class Record { /** * The record type. - * {@see http://www.iana.org/assignments/dns-parameters} + * @see IANA DNS Parameters */ public static enum TYPE { A(1), -- cgit v1.2.3 From c57b0d3ef4939c60ec670dd4ff39c4c247334ce5 Mon Sep 17 00:00:00 2001 From: Rene Treffer Date: Mon, 23 Jun 2014 20:56:38 +0200 Subject: Fix missing source/javadoc archives --- .gitignore | 1 + build.gradle | 4 ++-- gradle.properties.example | 19 +++++++++++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 gradle.properties.example diff --git a/.gitignore b/.gitignore index 8bd3bab3..2362cd05 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ build/ # Local configuration file (sdk path, etc) local.properties +gradle.properties # Proguard folder generated by Eclipse proguard/ diff --git a/build.gradle b/build.gradle index 3345e500..bed210d0 100644 --- a/build.gradle +++ b/build.gradle @@ -31,9 +31,9 @@ repositories { } nexus { - attachSources = false + attachSources = true attachTests = false - attachJavadoc = false + attachJavadoc = true sign = true } diff --git a/gradle.properties.example b/gradle.properties.example new file mode 100644 index 00000000..609aedaa --- /dev/null +++ b/gradle.properties.example @@ -0,0 +1,19 @@ +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# +# GPG settings +# + +# gpg key id +#signing.keyId=DEADBEEF +# gpg keyring (this is the default gnupg keyring containing private keys) +#signing.secretKeyRingFile=/home/ubuntu/.gnupg/secring.gpg + +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# +# nexus settings +# + +# the nexus username used for log in +#nexusUsername=ubuntu +# the nexus password +#nexusPassword=correcthorsebatterystaple -- cgit v1.2.3 From f9f5069e52c1964e0f2b838a8dc79f246d164a29 Mon Sep 17 00:00:00 2001 From: Rene Treffer Date: Mon, 23 Jun 2014 21:02:25 +0200 Subject: Add missing javadoc / missing getter (getTTL) --- src/main/java/de/measite/minidns/Client.java | 8 ++++++-- src/main/java/de/measite/minidns/DNSMessage.java | 5 +++-- src/main/java/de/measite/minidns/LRUCache.java | 2 +- src/main/java/de/measite/minidns/Record.java | 8 ++++++++ 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/main/java/de/measite/minidns/Client.java b/src/main/java/de/measite/minidns/Client.java index 462d52ff..827aa772 100644 --- a/src/main/java/de/measite/minidns/Client.java +++ b/src/main/java/de/measite/minidns/Client.java @@ -75,7 +75,7 @@ public class Client { * @param clazz The class of the request (usually IN for Internet). * @param host The DNS server host. * @param port The DNS server port. - * @return + * @return The response (or null on timeout / failure). * @throws IOException On IO Errors. */ public DNSMessage query(String name, TYPE type, CLASS clazz, String host, int port) @@ -91,7 +91,7 @@ public class Client { * @param type The DNS type to request (SRV, A, AAAA, ...). * @param clazz The class of the request (usually IN for Internet). * @param host The DNS server host. - * @return + * @return The response (or null on timeout / failure). * @throws IOException On IO Errors. */ public DNSMessage query(String name, TYPE type, CLASS clazz, String host) @@ -106,6 +106,7 @@ public class Client { * @param name The DNS name to request. * @param type The DNS type to request (SRV, A, AAAA, ...). * @param clazz The class of the request (usually IN for Internet). + * @return The response (or null on timeout/error). * @return The DNSMessage reply or null. */ public DNSMessage query(String name, TYPE type, CLASS clazz) @@ -118,6 +119,7 @@ public class Client { * Query a specific server for one entry. * @param q The question section of the DNS query. * @param host The dns server host. + * @return The response (or null on timeout/error). * @throws IOException On IOErrors. */ public DNSMessage query(Question q, String host) throws IOException { @@ -129,6 +131,7 @@ public class Client { * @param q The question section of the DNS query. * @param host The dns server host. * @param port the dns port. + * @return The response (or null on timeout/error). * @throws IOException On IOErrors. */ public DNSMessage query(Question q, String host, int port) throws IOException { @@ -167,6 +170,7 @@ public class Client { /** * Query the system DNS server for one entry. * @param q The question section of the DNS query. + * @return The response (or null on timeout/error). */ public DNSMessage query(Question q) { // While this query method does in fact re-use query(Question, String) diff --git a/src/main/java/de/measite/minidns/DNSMessage.java b/src/main/java/de/measite/minidns/DNSMessage.java index 6e0ae6c8..fbee3b19 100644 --- a/src/main/java/de/measite/minidns/DNSMessage.java +++ b/src/main/java/de/measite/minidns/DNSMessage.java @@ -48,7 +48,7 @@ public class DNSMessage { /** * Retrieve the byte value of the response code. - * @return + * @return the response code. */ public byte getValue() { return (byte) value; @@ -219,6 +219,7 @@ public class DNSMessage { /** * Get the receive timestamp if this message was created via parse. * This should be used to evaluate TTLs. + * @return The receive timestamp in milliseconds. */ public long getReceiveTimestamp() { return receiveTimestamp; @@ -331,7 +332,7 @@ public class DNSMessage { /** * Change the check status of this packet. - * @param checkDisabled + * @param checkDisabled The new check disabled value. */ public void setCheckDisabled(boolean checkDisabled) { this.checkDisabled = checkDisabled; diff --git a/src/main/java/de/measite/minidns/LRUCache.java b/src/main/java/de/measite/minidns/LRUCache.java index 0262e4c5..6b9bbdc1 100644 --- a/src/main/java/de/measite/minidns/LRUCache.java +++ b/src/main/java/de/measite/minidns/LRUCache.java @@ -60,7 +60,7 @@ public class LRUCache implements DNSCache { /** * Create a new LRUCache with given capacity. - * @param capacity + * @param capacity The capacity of this cache. */ public LRUCache(final int capacity) { this(capacity, Long.MAX_VALUE); diff --git a/src/main/java/de/measite/minidns/Record.java b/src/main/java/de/measite/minidns/Record.java index 58740b7d..8f8d0d0e 100644 --- a/src/main/java/de/measite/minidns/Record.java +++ b/src/main/java/de/measite/minidns/Record.java @@ -302,4 +302,12 @@ public class Record { return payloadData; } + /** + * Retrieve the record ttl. + * @return The record ttl. + */ + public long getTtl() { + return ttl; + } + } -- cgit v1.2.3 From bc621bfcfbede74251a29c7ef7513498baa4062d Mon Sep 17 00:00:00 2001 From: Rene Treffer Date: Mon, 23 Jun 2014 21:20:39 +0200 Subject: Add missing property --- gradle.properties.example | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gradle.properties.example b/gradle.properties.example index 609aedaa..68ffc418 100644 --- a/gradle.properties.example +++ b/gradle.properties.example @@ -5,6 +5,8 @@ # gpg key id #signing.keyId=DEADBEEF +# the gpg key passphrase +#signing.password=correcthorsebatterystaple # gpg keyring (this is the default gnupg keyring containing private keys) #signing.secretKeyRingFile=/home/ubuntu/.gnupg/secring.gpg -- cgit v1.2.3 From aa9ecf871ce4db462034662675b258f9839d3f61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Sat, 28 Jun 2014 20:16:10 +0200 Subject: Update android gradle plugin to 0.12.0 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 096f358e..184a4b94 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:0.11.1' + classpath 'com.android.tools.build:gradle:0.12.0' } } -- cgit v1.2.3 From cd1a6748dd7a4c7a251a4f75eb289955dfbb1058 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Thu, 17 Jul 2014 20:29:59 +0200 Subject: Extend for NFC --- src/org/openintents/openpgp/util/OpenPgpApi.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/org/openintents/openpgp/util/OpenPgpApi.java b/src/org/openintents/openpgp/util/OpenPgpApi.java index f6a78d0e..c88f9cea 100644 --- a/src/org/openintents/openpgp/util/OpenPgpApi.java +++ b/src/org/openintents/openpgp/util/OpenPgpApi.java @@ -141,6 +141,9 @@ public class OpenPgpApi { // optional extras: public static final String EXTRA_PASSPHRASE = "passphrase"; + // internal + public static final String EXTRA_NFC_DATA = "nfc_data"; + // GET_KEY public static final String EXTRA_KEY_ID = "key_id"; public static final String RESULT_KEY_IDS = "key_ids"; -- cgit v1.2.3 From 869ab96e6dcd4821fd5360248429e49dae6fbaca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Tue, 22 Jul 2014 18:10:06 +0200 Subject: Add values for NFC support --- src/org/openintents/openpgp/util/OpenPgpApi.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/org/openintents/openpgp/util/OpenPgpApi.java b/src/org/openintents/openpgp/util/OpenPgpApi.java index c88f9cea..02958442 100644 --- a/src/org/openintents/openpgp/util/OpenPgpApi.java +++ b/src/org/openintents/openpgp/util/OpenPgpApi.java @@ -141,8 +141,9 @@ public class OpenPgpApi { // optional extras: public static final String EXTRA_PASSPHRASE = "passphrase"; - // internal - public static final String EXTRA_NFC_DATA = "nfc_data"; + // internal NFC states + public static final String EXTRA_NFC_SIGNED_HASH = "nfc_signed_hash"; + public static final String EXTRA_NFC_SIG_CREATION_TIMESTAMP = "nfc_sig_creation_timestamp"; // GET_KEY public static final String EXTRA_KEY_ID = "key_id"; -- cgit v1.2.3 From 6eda21b4162d8a57c26da5b38595581490fbe2d2 Mon Sep 17 00:00:00 2001 From: Rene Treffer Date: Tue, 22 Jul 2014 23:22:35 +0200 Subject: Add PTR record parsing --- src/main/java/de/measite/minidns/DNSMessage.java | 2 +- src/main/java/de/measite/minidns/Record.java | 4 ++++ src/main/java/de/measite/minidns/record/PTR.java | 19 +++++++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 src/main/java/de/measite/minidns/record/PTR.java diff --git a/src/main/java/de/measite/minidns/DNSMessage.java b/src/main/java/de/measite/minidns/DNSMessage.java index fbee3b19..01c09645 100644 --- a/src/main/java/de/measite/minidns/DNSMessage.java +++ b/src/main/java/de/measite/minidns/DNSMessage.java @@ -424,7 +424,7 @@ public class DNSMessage { message.authenticData = ((header >> 5) & 1) == 1; message.checkDisabled = ((header >> 4) & 1) == 1; message.responseCode = RESPONSE_CODE.getResponseCode(header & 0xf); - message.receiveTimestamp = System.currentTimeMillis(); + message.receiveTimestamp = System.currentTimeMillis(); int questionCount = dis.readUnsignedShort(); int answerCount = dis.readUnsignedShort(); int nameserverCount = dis.readUnsignedShort(); diff --git a/src/main/java/de/measite/minidns/Record.java b/src/main/java/de/measite/minidns/Record.java index 8f8d0d0e..f14aa0b8 100644 --- a/src/main/java/de/measite/minidns/Record.java +++ b/src/main/java/de/measite/minidns/Record.java @@ -9,6 +9,7 @@ import de.measite.minidns.record.AAAA; import de.measite.minidns.record.CNAME; import de.measite.minidns.record.Data; import de.measite.minidns.record.NS; +import de.measite.minidns.record.PTR; import de.measite.minidns.record.SRV; import de.measite.minidns.util.NameUtil; @@ -250,6 +251,9 @@ public class Record { case CNAME: this.payloadData = new CNAME(); break; + case PTR: + this.payloadData = new PTR(); + break; default: System.out.println("Unparsed type " + type); this.payloadData = null; diff --git a/src/main/java/de/measite/minidns/record/PTR.java b/src/main/java/de/measite/minidns/record/PTR.java new file mode 100644 index 00000000..6e200655 --- /dev/null +++ b/src/main/java/de/measite/minidns/record/PTR.java @@ -0,0 +1,19 @@ +package de.measite.minidns.record; + +import java.io.DataInputStream; +import java.io.IOException; + +import de.measite.minidns.Record.TYPE; +import de.measite.minidns.util.NameUtil; + +/** + * A PTR record is handled like a CNAME + */ +public class PTR extends CNAME { + + @Override + public TYPE getType() { + return TYPE.PTR; + } + +} -- cgit v1.2.3 From f13813186f7d106ff32e525b2517227108651dd9 Mon Sep 17 00:00:00 2001 From: Rene Treffer Date: Tue, 22 Jul 2014 23:22:35 +0200 Subject: Add PTR record parsing --- src/main/java/de/measite/minidns/DNSMessage.java | 2 +- src/main/java/de/measite/minidns/Record.java | 4 ++++ src/main/java/de/measite/minidns/record/PTR.java | 19 +++++++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 src/main/java/de/measite/minidns/record/PTR.java diff --git a/src/main/java/de/measite/minidns/DNSMessage.java b/src/main/java/de/measite/minidns/DNSMessage.java index fbee3b19..01c09645 100644 --- a/src/main/java/de/measite/minidns/DNSMessage.java +++ b/src/main/java/de/measite/minidns/DNSMessage.java @@ -424,7 +424,7 @@ public class DNSMessage { message.authenticData = ((header >> 5) & 1) == 1; message.checkDisabled = ((header >> 4) & 1) == 1; message.responseCode = RESPONSE_CODE.getResponseCode(header & 0xf); - message.receiveTimestamp = System.currentTimeMillis(); + message.receiveTimestamp = System.currentTimeMillis(); int questionCount = dis.readUnsignedShort(); int answerCount = dis.readUnsignedShort(); int nameserverCount = dis.readUnsignedShort(); diff --git a/src/main/java/de/measite/minidns/Record.java b/src/main/java/de/measite/minidns/Record.java index 8f8d0d0e..f14aa0b8 100644 --- a/src/main/java/de/measite/minidns/Record.java +++ b/src/main/java/de/measite/minidns/Record.java @@ -9,6 +9,7 @@ import de.measite.minidns.record.AAAA; import de.measite.minidns.record.CNAME; import de.measite.minidns.record.Data; import de.measite.minidns.record.NS; +import de.measite.minidns.record.PTR; import de.measite.minidns.record.SRV; import de.measite.minidns.util.NameUtil; @@ -250,6 +251,9 @@ public class Record { case CNAME: this.payloadData = new CNAME(); break; + case PTR: + this.payloadData = new PTR(); + break; default: System.out.println("Unparsed type " + type); this.payloadData = null; diff --git a/src/main/java/de/measite/minidns/record/PTR.java b/src/main/java/de/measite/minidns/record/PTR.java new file mode 100644 index 00000000..6e200655 --- /dev/null +++ b/src/main/java/de/measite/minidns/record/PTR.java @@ -0,0 +1,19 @@ +package de.measite.minidns.record; + +import java.io.DataInputStream; +import java.io.IOException; + +import de.measite.minidns.Record.TYPE; +import de.measite.minidns.util.NameUtil; + +/** + * A PTR record is handled like a CNAME + */ +public class PTR extends CNAME { + + @Override + public TYPE getType() { + return TYPE.PTR; + } + +} -- cgit v1.2.3 From bfcba9169d106fb082857e0ba4eaef7ee3df0782 Mon Sep 17 00:00:00 2001 From: Rene Treffer Date: Wed, 23 Jul 2014 00:00:55 +0200 Subject: Add TXT record --- src/main/java/de/measite/minidns/Record.java | 10 +++- src/main/java/de/measite/minidns/record/TXT.java | 65 ++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 src/main/java/de/measite/minidns/record/TXT.java diff --git a/src/main/java/de/measite/minidns/Record.java b/src/main/java/de/measite/minidns/Record.java index f14aa0b8..6aaf4f7d 100644 --- a/src/main/java/de/measite/minidns/Record.java +++ b/src/main/java/de/measite/minidns/Record.java @@ -11,6 +11,7 @@ import de.measite.minidns.record.Data; import de.measite.minidns.record.NS; import de.measite.minidns.record.PTR; import de.measite.minidns.record.SRV; +import de.measite.minidns.record.TXT; import de.measite.minidns.util.NameUtil; /** @@ -231,7 +232,11 @@ public class Record { public void parse(DataInputStream dis, byte[] data) throws IOException { this.name = NameUtil.parse(dis, data); this.type = TYPE.getType(dis.readUnsignedShort()); - this.clazz = CLASS.getClass(dis.readUnsignedShort()); + int clazzValue = dis.readUnsignedShort(); + this.clazz = CLASS.getClass(clazzValue); + if (this.clazz == null) { + System.out.println("Unknown class " + clazzValue); + } this.ttl = (((long)dis.readUnsignedShort()) << 32) + dis.readUnsignedShort(); int payloadLength = dis.readUnsignedShort(); @@ -254,6 +259,9 @@ public class Record { case PTR: this.payloadData = new PTR(); break; + case TXT: + this.payloadData = new TXT(); + break; default: System.out.println("Unparsed type " + type); this.payloadData = null; diff --git a/src/main/java/de/measite/minidns/record/TXT.java b/src/main/java/de/measite/minidns/record/TXT.java new file mode 100644 index 00000000..03e73040 --- /dev/null +++ b/src/main/java/de/measite/minidns/record/TXT.java @@ -0,0 +1,65 @@ +package de.measite.minidns.record; + +import java.io.DataInputStream; +import java.io.IOException; + +import de.measite.minidns.Record.TYPE; +import de.measite.minidns.util.NameUtil; + +/** + * TXT record (actually a binary blob with wrappers for text content). + */ +public class TXT implements Data { + + protected byte[] blob; + + public byte[] getBlob() { + return blob; + } + + public void setBlob(byte[] blob) { + this.blob = blob; + } + + public String getText() { + try { + return (new String(blob, "UTF-8")).intern(); + } catch (Exception e) { + /* Can't happen for UTF-8 unless it's really a blob */ + return null; + } + } + + public void setText(String text) { + try { + this.blob = text.getBytes("UTF-8"); + } catch (Exception e) { + /* Can't happen, UTF-8 IS supported */ + throw new RuntimeException("UTF-8 not supported", e); + } + } + + @Override + public byte[] toByteArray() { + throw new UnsupportedOperationException("Not implemented yet"); + } + + @Override + public void parse(DataInputStream dis, byte[] data, int length) + throws IOException + { + blob = new byte[length]; + dis.readFully(blob); + } + + @Override + public TYPE getType() { + return TYPE.TXT; + } + + @Override + public String toString() { + return "\"" + getText() + "\""; + } + +} -- cgit v1.2.3 From b0f8bec1cbfa6d21e0873c63d5213bd12d429bd8 Mon Sep 17 00:00:00 2001 From: Rene Treffer Date: Wed, 23 Jul 2014 00:00:55 +0200 Subject: Add TXT record --- src/main/java/de/measite/minidns/Record.java | 10 +++- src/main/java/de/measite/minidns/record/TXT.java | 65 ++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 src/main/java/de/measite/minidns/record/TXT.java diff --git a/src/main/java/de/measite/minidns/Record.java b/src/main/java/de/measite/minidns/Record.java index f14aa0b8..6aaf4f7d 100644 --- a/src/main/java/de/measite/minidns/Record.java +++ b/src/main/java/de/measite/minidns/Record.java @@ -11,6 +11,7 @@ import de.measite.minidns.record.Data; import de.measite.minidns.record.NS; import de.measite.minidns.record.PTR; import de.measite.minidns.record.SRV; +import de.measite.minidns.record.TXT; import de.measite.minidns.util.NameUtil; /** @@ -231,7 +232,11 @@ public class Record { public void parse(DataInputStream dis, byte[] data) throws IOException { this.name = NameUtil.parse(dis, data); this.type = TYPE.getType(dis.readUnsignedShort()); - this.clazz = CLASS.getClass(dis.readUnsignedShort()); + int clazzValue = dis.readUnsignedShort(); + this.clazz = CLASS.getClass(clazzValue); + if (this.clazz == null) { + System.out.println("Unknown class " + clazzValue); + } this.ttl = (((long)dis.readUnsignedShort()) << 32) + dis.readUnsignedShort(); int payloadLength = dis.readUnsignedShort(); @@ -254,6 +259,9 @@ public class Record { case PTR: this.payloadData = new PTR(); break; + case TXT: + this.payloadData = new TXT(); + break; default: System.out.println("Unparsed type " + type); this.payloadData = null; diff --git a/src/main/java/de/measite/minidns/record/TXT.java b/src/main/java/de/measite/minidns/record/TXT.java new file mode 100644 index 00000000..03e73040 --- /dev/null +++ b/src/main/java/de/measite/minidns/record/TXT.java @@ -0,0 +1,65 @@ +package de.measite.minidns.record; + +import java.io.DataInputStream; +import java.io.IOException; + +import de.measite.minidns.Record.TYPE; +import de.measite.minidns.util.NameUtil; + +/** + * TXT record (actually a binary blob with wrappers for text content). + */ +public class TXT implements Data { + + protected byte[] blob; + + public byte[] getBlob() { + return blob; + } + + public void setBlob(byte[] blob) { + this.blob = blob; + } + + public String getText() { + try { + return (new String(blob, "UTF-8")).intern(); + } catch (Exception e) { + /* Can't happen for UTF-8 unless it's really a blob */ + return null; + } + } + + public void setText(String text) { + try { + this.blob = text.getBytes("UTF-8"); + } catch (Exception e) { + /* Can't happen, UTF-8 IS supported */ + throw new RuntimeException("UTF-8 not supported", e); + } + } + + @Override + public byte[] toByteArray() { + throw new UnsupportedOperationException("Not implemented yet"); + } + + @Override + public void parse(DataInputStream dis, byte[] data, int length) + throws IOException + { + blob = new byte[length]; + dis.readFully(blob); + } + + @Override + public TYPE getType() { + return TYPE.TXT; + } + + @Override + public String toString() { + return "\"" + getText() + "\""; + } + +} -- cgit v1.2.3 From 70d403f0b612f6cb26b8fc194e35c622c5383167 Mon Sep 17 00:00:00 2001 From: Rene Treffer Date: Wed, 23 Jul 2014 21:53:38 +0200 Subject: Better / more verbose toString --- src/main/java/de/measite/minidns/DNSMessage.java | 5 ++++- src/main/java/de/measite/minidns/Question.java | 5 +++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/java/de/measite/minidns/DNSMessage.java b/src/main/java/de/measite/minidns/DNSMessage.java index 01c09645..ab2535ce 100644 --- a/src/main/java/de/measite/minidns/DNSMessage.java +++ b/src/main/java/de/measite/minidns/DNSMessage.java @@ -515,7 +515,10 @@ public class DNSMessage { public String toString() { return "-- DNSMessage " + id + " --\n" + - Arrays.toString(answers); + "Q" + Arrays.toString(questions) + + "NS" + Arrays.toString(nameserverRecords) + + "A" + Arrays.toString(answers) + + "ARR" + Arrays.toString(additionalResourceRecords); } } diff --git a/src/main/java/de/measite/minidns/Question.java b/src/main/java/de/measite/minidns/Question.java index 62862251..88300356 100644 --- a/src/main/java/de/measite/minidns/Question.java +++ b/src/main/java/de/measite/minidns/Question.java @@ -134,4 +134,9 @@ public class Question { byte o[] = ((Question)other).toByteArray(); return Arrays.equals(t, o); } + + @Override + public String toString() { + return "Question/" + clazz + "/" + type + ": " + name; + } } -- cgit v1.2.3 From 2bddcc9f997e5b1e565478e99c55488c86a85e76 Mon Sep 17 00:00:00 2001 From: Rene Treffer Date: Wed, 23 Jul 2014 22:43:12 +0200 Subject: Add parsing for mdns QU records (RFC6762) --- src/main/java/de/measite/minidns/Record.java | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/main/java/de/measite/minidns/Record.java b/src/main/java/de/measite/minidns/Record.java index 6aaf4f7d..26af4711 100644 --- a/src/main/java/de/measite/minidns/Record.java +++ b/src/main/java/de/measite/minidns/Record.java @@ -222,6 +222,11 @@ public class Record { */ protected Data payloadData; + /** + * MDNS defines the highest bit of the class as the unicast query bit. + */ + protected boolean unicastQuery; + /** * Parse a given record based on the full message data and the current * stream position. @@ -233,7 +238,8 @@ public class Record { this.name = NameUtil.parse(dis, data); this.type = TYPE.getType(dis.readUnsignedShort()); int clazzValue = dis.readUnsignedShort(); - this.clazz = CLASS.getClass(clazzValue); + this.clazz = CLASS.getClass(clazzValue & 0x7fff); + this.unicastQuery = (clazzValue & 0x8000) > 0; if (this.clazz == null) { System.out.println("Unknown class " + clazzValue); } @@ -298,6 +304,14 @@ public class Record { (q.getName().equals(name)); } + /** + * See if this query/response was a unicast query (highest class bit set). + * @return True if it is a unicast query/response record. + */ + public boolean isUnicastQuery() { + return unicastQuery; + } + /** * The generic record name, e.g. "measite.de". * @return The record name. -- cgit v1.2.3 From e515a49027fc5de36b8977cf8b096afc9838a9be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Sun, 27 Jul 2014 18:28:20 +0200 Subject: Update gradle plugin to 0.12.2 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 184a4b94..6e8fdfb5 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:0.12.0' + classpath 'com.android.tools.build:gradle:0.12.2' } } -- cgit v1.2.3 From 64f25e1783143fbedcf3a3b9fce484449077e7af Mon Sep 17 00:00:00 2001 From: Rene Treffer Date: Wed, 30 Jul 2014 22:29:16 +0200 Subject: Fix style on build.gradle --- build.gradle | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index bed210d0..5941beaf 100644 --- a/build.gradle +++ b/build.gradle @@ -6,8 +6,8 @@ apply plugin: 'nexus' buildscript { repositories { jcenter() - mavenLocal() - mavenCentral() + mavenLocal() + mavenCentral() } dependencies { @@ -73,3 +73,5 @@ modifyPom { } } +dependencies { +} \ No newline at end of file -- cgit v1.2.3 From 858251d52c2ab04b3026573fa3e14b19579cac19 Mon Sep 17 00:00:00 2001 From: Rene Treffer Date: Wed, 30 Jul 2014 22:37:09 +0200 Subject: Remove System.out code --- src/main/java/de/measite/minidns/Record.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/de/measite/minidns/Record.java b/src/main/java/de/measite/minidns/Record.java index 26af4711..ab081426 100644 --- a/src/main/java/de/measite/minidns/Record.java +++ b/src/main/java/de/measite/minidns/Record.java @@ -3,6 +3,8 @@ package de.measite.minidns; import java.io.DataInputStream; import java.io.IOException; import java.util.HashMap; +import java.util.logging.Level; +import java.util.logging.Logger; import de.measite.minidns.record.A; import de.measite.minidns.record.AAAA; @@ -19,6 +21,8 @@ import de.measite.minidns.util.NameUtil; */ public class Record { + private static final Logger LOGGER = Logger.getLogger(Client.class.getName()); + /** * The record type. * @see IANA DNS Parameters @@ -241,7 +245,7 @@ public class Record { this.clazz = CLASS.getClass(clazzValue & 0x7fff); this.unicastQuery = (clazzValue & 0x8000) > 0; if (this.clazz == null) { - System.out.println("Unknown class " + clazzValue); + LOGGER.log(Level.FINE, "Unknown class " + clazzValue); } this.ttl = (((long)dis.readUnsignedShort()) << 32) + dis.readUnsignedShort(); @@ -269,7 +273,7 @@ public class Record { this.payloadData = new TXT(); break; default: - System.out.println("Unparsed type " + type); + LOGGER.log(Level.FINE, "Unparsed type " + type); this.payloadData = null; for (int i = 0; i < payloadLength; i++) { dis.readByte(); -- cgit v1.2.3 From 9e42bff01440c1351946a432126d5a1b87fb7c78 Mon Sep 17 00:00:00 2001 From: Rene Treffer Date: Wed, 30 Jul 2014 22:38:02 +0200 Subject: Allow mdns multicast / unicast reply queries --- src/main/java/de/measite/minidns/Question.java | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/main/java/de/measite/minidns/Question.java b/src/main/java/de/measite/minidns/Question.java index 88300356..3b2fa1a1 100644 --- a/src/main/java/de/measite/minidns/Question.java +++ b/src/main/java/de/measite/minidns/Question.java @@ -30,6 +30,11 @@ public class Question { */ private final CLASS clazz; + /** + * UnicastQueries have the highest bit of the CLASS field set to 1. + */ + private final boolean unicastQuery; + /** * Cache for the serialized object. */ @@ -41,10 +46,21 @@ public class Question { * @param type The type, e.g. A. * @param clazz The class, usually IN (internet). */ - public Question(String name, TYPE type, CLASS clazz) { + public Question(String name, TYPE type, CLASS clazz, boolean unicastQuery) { this.name = name; this.type = type; this.clazz = clazz; + this.unicastQuery = unicastQuery; + } + + /** + * Create a dns question for the given name/type/class. + * @param name The name e.g. "measite.de". + * @param type The type, e.g. A. + * @param clazz The class, usually IN (internet). + */ + public Question(String name, TYPE type, CLASS clazz) { + this(name, type, clazz, false); } /** @@ -106,7 +122,7 @@ public class Question { try { dos.write(NameUtil.toByteArray(this.name)); dos.writeShort(type.getValue()); - dos.writeShort(clazz.getValue()); + dos.writeShort(clazz.getValue() | (unicastQuery ? (1 << 15) : 0)); dos.flush(); } catch (IOException e) { // Should never happen -- cgit v1.2.3 From fab79b605f0317891fe4260dcec0dea1a76fa04f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Mon, 4 Aug 2014 17:09:59 +0200 Subject: Lint fixes --- src/org/openintents/openpgp/util/OpenPgpUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/org/openintents/openpgp/util/OpenPgpUtils.java b/src/org/openintents/openpgp/util/OpenPgpUtils.java index e24c937a..416b2841 100644 --- a/src/org/openintents/openpgp/util/OpenPgpUtils.java +++ b/src/org/openintents/openpgp/util/OpenPgpUtils.java @@ -67,7 +67,7 @@ public class OpenPgpUtils { } private static String convertKeyIdToHex32bit(long keyId) { - String hexString = Long.toHexString(keyId & 0xffffffffL).toLowerCase(Locale.US); + String hexString = Long.toHexString(keyId & 0xffffffffL).toLowerCase(Locale.ENGLISH); while (hexString.length() < 8) { hexString = "0" + hexString; } -- cgit v1.2.3 From 4f6475f11bb8b268775b6633d9f23e3a269aff96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Mon, 11 Aug 2014 17:12:02 +0200 Subject: Add metadata parcel --- .../openpgp/OpenPgpDecryptMetadata.java | 133 +++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 src/org/openintents/openpgp/OpenPgpDecryptMetadata.java diff --git a/src/org/openintents/openpgp/OpenPgpDecryptMetadata.java b/src/org/openintents/openpgp/OpenPgpDecryptMetadata.java new file mode 100644 index 00000000..cfb50745 --- /dev/null +++ b/src/org/openintents/openpgp/OpenPgpDecryptMetadata.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2014 Dominik Schürmann + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openintents.openpgp; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Parcelable versioning has been copied from Dashclock Widget + * https://code.google.com/p/dashclock/source/browse/api/src/main/java/com/google/android/apps/dashclock/api/ExtensionData.java + */ +public class OpenPgpDecryptMetadata implements Parcelable { + /** + * Since there might be a case where new versions of the client using the library getting + * old versions of the protocol (and thus old versions of this class), we need a versioning + * system for the parcels sent between the clients and the providers. + */ + public static final int PARCELABLE_VERSION = 1; + + String filename; + long modificationTime; + int format; + long originalSize; + + public String getFilename() { + return filename; + } + + public long getModificationTime() { + return modificationTime; + } + + public long getOriginalSize() { + return originalSize; + } + + public int getFormat() { + return format; + } + + public OpenPgpDecryptMetadata() { + } + + public OpenPgpDecryptMetadata(String filename, long modificationTime, + int format, long originalSize) { + this.filename = filename; + this.modificationTime = modificationTime; + this.format = format; + this.originalSize = originalSize; + } + + public OpenPgpDecryptMetadata(OpenPgpDecryptMetadata b) { + this.filename = b.filename; + this.modificationTime = b.modificationTime; + this.format = b.format; + this.originalSize = b.originalSize; + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + /** + * NOTE: When adding fields in the process of updating this API, make sure to bump + * {@link #PARCELABLE_VERSION}. + */ + dest.writeInt(PARCELABLE_VERSION); + // Inject a placeholder that will store the parcel size from this point on + // (not including the size itself). + int sizePosition = dest.dataPosition(); + dest.writeInt(0); + int startPosition = dest.dataPosition(); + // version 1 + dest.writeString(filename); + dest.writeLong(modificationTime); + dest.writeInt(format); + dest.writeLong(originalSize); + // Go back and write the size + int parcelableSize = dest.dataPosition() - startPosition; + dest.setDataPosition(sizePosition); + dest.writeInt(parcelableSize); + dest.setDataPosition(startPosition + parcelableSize); + } + + public static final Creator CREATOR = new Creator() { + public OpenPgpDecryptMetadata createFromParcel(final Parcel source) { + int parcelableVersion = source.readInt(); + int parcelableSize = source.readInt(); + int startPosition = source.dataPosition(); + + OpenPgpDecryptMetadata vr = new OpenPgpDecryptMetadata(); + vr.filename = source.readString(); + vr.modificationTime = source.readLong(); + vr.format = source.readInt(); + vr.originalSize = source.readLong(); + + // skip over all fields added in future versions of this parcel + source.setDataPosition(startPosition + parcelableSize); + + return vr; + } + + public OpenPgpDecryptMetadata[] newArray(final int size) { + return new OpenPgpDecryptMetadata[size]; + } + }; + + @Override + public String toString() { + String out = new String(); + out += "\nfilename: " + filename; + out += "\nmodificationTime: " + modificationTime; + out += "\nformat: " + format; + out += "\noriginalSize: " + originalSize; + return out; + } + +} -- cgit v1.2.3 From 6bec5eedde31bacbafcb541a728719eeff99d3b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Mon, 11 Aug 2014 17:19:23 +0200 Subject: Increase version to 4 --- src/org/openintents/openpgp/util/OpenPgpApi.java | 35 ++++++++++++++++++++---- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/src/org/openintents/openpgp/util/OpenPgpApi.java b/src/org/openintents/openpgp/util/OpenPgpApi.java index 02958442..23844750 100644 --- a/src/org/openintents/openpgp/util/OpenPgpApi.java +++ b/src/org/openintents/openpgp/util/OpenPgpApi.java @@ -34,9 +34,22 @@ public class OpenPgpApi { public static final String TAG = "OpenPgp API"; - public static final int API_VERSION = 3; public static final String SERVICE_INTENT = "org.openintents.openpgp.IOpenPgpService"; + /** + * Version history + * --------------- + *

+ * 3: + * - first public stable version + *

+ * 4: + * - No changes to existing methods -> backward compatible + * - Introduction of ACTION_DECRYPT_METADATA, RESULT_METADATA, and OpenPgpDecryptMetadata parcel + * - Introduction of internal NFC extras: EXTRA_NFC_SIGNED_HASH, EXTRA_NFC_SIG_CREATION_TIMESTAMP + */ + public static final int API_VERSION = 4; + /** * General extras * -------------- @@ -54,7 +67,7 @@ public class OpenPgpApi { * Sign only *

* optional extras: - * boolean EXTRA_REQUEST_ASCII_ARMOR (request ascii armor for ouput) + * boolean EXTRA_REQUEST_ASCII_ARMOR (request ascii armor for output) * String EXTRA_PASSPHRASE (key passphrase) */ public static final String ACTION_SIGN = "org.openintents.openpgp.action.SIGN"; @@ -68,7 +81,7 @@ public class OpenPgpApi { * long[] EXTRA_KEY_IDS *

* optional extras: - * boolean EXTRA_REQUEST_ASCII_ARMOR (request ascii armor for ouput) + * boolean EXTRA_REQUEST_ASCII_ARMOR (request ascii armor for output) * String EXTRA_PASSPHRASE (key passphrase) */ public static final String ACTION_ENCRYPT = "org.openintents.openpgp.action.ENCRYPT"; @@ -82,7 +95,7 @@ public class OpenPgpApi { * long[] EXTRA_KEY_IDS *

* optional extras: - * boolean EXTRA_REQUEST_ASCII_ARMOR (request ascii armor for ouput) + * boolean EXTRA_REQUEST_ASCII_ARMOR (request ascii armor for output) * String EXTRA_PASSPHRASE (key passphrase) */ public static final String ACTION_SIGN_AND_ENCRYPT = "org.openintents.openpgp.action.SIGN_AND_ENCRYPT"; @@ -95,13 +108,24 @@ public class OpenPgpApi { * in addition a PendingIntent is returned via RESULT_INTENT to download missing keys. *

* optional extras: - * boolean EXTRA_REQUEST_ASCII_ARMOR (request ascii armor for ouput) + * boolean EXTRA_REQUEST_ASCII_ARMOR (request ascii armor for output) *

* returned extras: * OpenPgpSignatureResult RESULT_SIGNATURE + * OpenPgpDecryptMetadata RESULT_METADATA */ public static final String ACTION_DECRYPT_VERIFY = "org.openintents.openpgp.action.DECRYPT_VERIFY"; + /** + * Decrypts the header of an encrypted file to retrieve metadata such as original filename. + *

+ * This does not decrypt the actual content of the file. + *

+ * returned extras: + * OpenPgpDecryptMetadata RESULT_METADATA + */ + public static final String ACTION_DECRYPT_METADATA = "org.openintents.openpgp.action.DECRYPT_METADATA"; + /** * Get key ids based on given user ids (=emails) *

@@ -165,6 +189,7 @@ public class OpenPgpApi { // DECRYPT_VERIFY public static final String RESULT_SIGNATURE = "signature"; + public static final String RESULT_METADATA = "metadata"; IOpenPgpService mService; Context mContext; -- cgit v1.2.3 From 9aa5f23788f19689fb172fd209de72bc6941afdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Mon, 11 Aug 2014 20:16:30 +0200 Subject: Support mime type in metadata --- .../openpgp/OpenPgpDecryptMetadata.java | 27 +++++++++++----------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/org/openintents/openpgp/OpenPgpDecryptMetadata.java b/src/org/openintents/openpgp/OpenPgpDecryptMetadata.java index cfb50745..832feb8d 100644 --- a/src/org/openintents/openpgp/OpenPgpDecryptMetadata.java +++ b/src/org/openintents/openpgp/OpenPgpDecryptMetadata.java @@ -32,14 +32,18 @@ public class OpenPgpDecryptMetadata implements Parcelable { public static final int PARCELABLE_VERSION = 1; String filename; + String mimeType; long modificationTime; - int format; long originalSize; public String getFilename() { return filename; } + public String getMimeType() { + return mimeType; + } + public long getModificationTime() { return modificationTime; } @@ -48,25 +52,21 @@ public class OpenPgpDecryptMetadata implements Parcelable { return originalSize; } - public int getFormat() { - return format; - } - public OpenPgpDecryptMetadata() { } - public OpenPgpDecryptMetadata(String filename, long modificationTime, - int format, long originalSize) { + public OpenPgpDecryptMetadata(String filename, String mimeType, long modificationTime, + long originalSize) { this.filename = filename; + this.mimeType = mimeType; this.modificationTime = modificationTime; - this.format = format; this.originalSize = originalSize; } public OpenPgpDecryptMetadata(OpenPgpDecryptMetadata b) { this.filename = b.filename; + this.mimeType = b.mimeType; this.modificationTime = b.modificationTime; - this.format = b.format; this.originalSize = b.originalSize; } @@ -87,8 +87,8 @@ public class OpenPgpDecryptMetadata implements Parcelable { int startPosition = dest.dataPosition(); // version 1 dest.writeString(filename); + dest.writeString(mimeType); dest.writeLong(modificationTime); - dest.writeInt(format); dest.writeLong(originalSize); // Go back and write the size int parcelableSize = dest.dataPosition() - startPosition; @@ -105,8 +105,8 @@ public class OpenPgpDecryptMetadata implements Parcelable { OpenPgpDecryptMetadata vr = new OpenPgpDecryptMetadata(); vr.filename = source.readString(); + vr.mimeType = source.readString(); vr.modificationTime = source.readLong(); - vr.format = source.readInt(); vr.originalSize = source.readLong(); // skip over all fields added in future versions of this parcel @@ -122,10 +122,9 @@ public class OpenPgpDecryptMetadata implements Parcelable { @Override public String toString() { - String out = new String(); - out += "\nfilename: " + filename; + String out = "\nfilename: " + filename; + out += "\nmimeType: " + mimeType; out += "\nmodificationTime: " + modificationTime; - out += "\nformat: " + format; out += "\noriginalSize: " + originalSize; return out; } -- cgit v1.2.3 From 777d22d75da9bfecf0bcfe2088da3fef01b2259f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Mon, 11 Aug 2014 21:26:34 +0200 Subject: Rename to OpenPgpMetadata --- .../openpgp/OpenPgpDecryptMetadata.java | 132 --------------------- src/org/openintents/openpgp/OpenPgpMetadata.java | 132 +++++++++++++++++++++ 2 files changed, 132 insertions(+), 132 deletions(-) delete mode 100644 src/org/openintents/openpgp/OpenPgpDecryptMetadata.java create mode 100644 src/org/openintents/openpgp/OpenPgpMetadata.java diff --git a/src/org/openintents/openpgp/OpenPgpDecryptMetadata.java b/src/org/openintents/openpgp/OpenPgpDecryptMetadata.java deleted file mode 100644 index 832feb8d..00000000 --- a/src/org/openintents/openpgp/OpenPgpDecryptMetadata.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright (C) 2014 Dominik Schürmann - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.openintents.openpgp; - -import android.os.Parcel; -import android.os.Parcelable; - -/** - * Parcelable versioning has been copied from Dashclock Widget - * https://code.google.com/p/dashclock/source/browse/api/src/main/java/com/google/android/apps/dashclock/api/ExtensionData.java - */ -public class OpenPgpDecryptMetadata implements Parcelable { - /** - * Since there might be a case where new versions of the client using the library getting - * old versions of the protocol (and thus old versions of this class), we need a versioning - * system for the parcels sent between the clients and the providers. - */ - public static final int PARCELABLE_VERSION = 1; - - String filename; - String mimeType; - long modificationTime; - long originalSize; - - public String getFilename() { - return filename; - } - - public String getMimeType() { - return mimeType; - } - - public long getModificationTime() { - return modificationTime; - } - - public long getOriginalSize() { - return originalSize; - } - - public OpenPgpDecryptMetadata() { - } - - public OpenPgpDecryptMetadata(String filename, String mimeType, long modificationTime, - long originalSize) { - this.filename = filename; - this.mimeType = mimeType; - this.modificationTime = modificationTime; - this.originalSize = originalSize; - } - - public OpenPgpDecryptMetadata(OpenPgpDecryptMetadata b) { - this.filename = b.filename; - this.mimeType = b.mimeType; - this.modificationTime = b.modificationTime; - this.originalSize = b.originalSize; - } - - public int describeContents() { - return 0; - } - - public void writeToParcel(Parcel dest, int flags) { - /** - * NOTE: When adding fields in the process of updating this API, make sure to bump - * {@link #PARCELABLE_VERSION}. - */ - dest.writeInt(PARCELABLE_VERSION); - // Inject a placeholder that will store the parcel size from this point on - // (not including the size itself). - int sizePosition = dest.dataPosition(); - dest.writeInt(0); - int startPosition = dest.dataPosition(); - // version 1 - dest.writeString(filename); - dest.writeString(mimeType); - dest.writeLong(modificationTime); - dest.writeLong(originalSize); - // Go back and write the size - int parcelableSize = dest.dataPosition() - startPosition; - dest.setDataPosition(sizePosition); - dest.writeInt(parcelableSize); - dest.setDataPosition(startPosition + parcelableSize); - } - - public static final Creator CREATOR = new Creator() { - public OpenPgpDecryptMetadata createFromParcel(final Parcel source) { - int parcelableVersion = source.readInt(); - int parcelableSize = source.readInt(); - int startPosition = source.dataPosition(); - - OpenPgpDecryptMetadata vr = new OpenPgpDecryptMetadata(); - vr.filename = source.readString(); - vr.mimeType = source.readString(); - vr.modificationTime = source.readLong(); - vr.originalSize = source.readLong(); - - // skip over all fields added in future versions of this parcel - source.setDataPosition(startPosition + parcelableSize); - - return vr; - } - - public OpenPgpDecryptMetadata[] newArray(final int size) { - return new OpenPgpDecryptMetadata[size]; - } - }; - - @Override - public String toString() { - String out = "\nfilename: " + filename; - out += "\nmimeType: " + mimeType; - out += "\nmodificationTime: " + modificationTime; - out += "\noriginalSize: " + originalSize; - return out; - } - -} diff --git a/src/org/openintents/openpgp/OpenPgpMetadata.java b/src/org/openintents/openpgp/OpenPgpMetadata.java new file mode 100644 index 00000000..2a99e406 --- /dev/null +++ b/src/org/openintents/openpgp/OpenPgpMetadata.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2014 Dominik Schürmann + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openintents.openpgp; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Parcelable versioning has been copied from Dashclock Widget + * https://code.google.com/p/dashclock/source/browse/api/src/main/java/com/google/android/apps/dashclock/api/ExtensionData.java + */ +public class OpenPgpMetadata implements Parcelable { + /** + * Since there might be a case where new versions of the client using the library getting + * old versions of the protocol (and thus old versions of this class), we need a versioning + * system for the parcels sent between the clients and the providers. + */ + public static final int PARCELABLE_VERSION = 1; + + String filename; + String mimeType; + long modificationTime; + long originalSize; + + public String getFilename() { + return filename; + } + + public String getMimeType() { + return mimeType; + } + + public long getModificationTime() { + return modificationTime; + } + + public long getOriginalSize() { + return originalSize; + } + + public OpenPgpMetadata() { + } + + public OpenPgpMetadata(String filename, String mimeType, long modificationTime, + long originalSize) { + this.filename = filename; + this.mimeType = mimeType; + this.modificationTime = modificationTime; + this.originalSize = originalSize; + } + + public OpenPgpMetadata(OpenPgpMetadata b) { + this.filename = b.filename; + this.mimeType = b.mimeType; + this.modificationTime = b.modificationTime; + this.originalSize = b.originalSize; + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + /** + * NOTE: When adding fields in the process of updating this API, make sure to bump + * {@link #PARCELABLE_VERSION}. + */ + dest.writeInt(PARCELABLE_VERSION); + // Inject a placeholder that will store the parcel size from this point on + // (not including the size itself). + int sizePosition = dest.dataPosition(); + dest.writeInt(0); + int startPosition = dest.dataPosition(); + // version 1 + dest.writeString(filename); + dest.writeString(mimeType); + dest.writeLong(modificationTime); + dest.writeLong(originalSize); + // Go back and write the size + int parcelableSize = dest.dataPosition() - startPosition; + dest.setDataPosition(sizePosition); + dest.writeInt(parcelableSize); + dest.setDataPosition(startPosition + parcelableSize); + } + + public static final Creator CREATOR = new Creator() { + public OpenPgpMetadata createFromParcel(final Parcel source) { + int parcelableVersion = source.readInt(); + int parcelableSize = source.readInt(); + int startPosition = source.dataPosition(); + + OpenPgpMetadata vr = new OpenPgpMetadata(); + vr.filename = source.readString(); + vr.mimeType = source.readString(); + vr.modificationTime = source.readLong(); + vr.originalSize = source.readLong(); + + // skip over all fields added in future versions of this parcel + source.setDataPosition(startPosition + parcelableSize); + + return vr; + } + + public OpenPgpMetadata[] newArray(final int size) { + return new OpenPgpMetadata[size]; + } + }; + + @Override + public String toString() { + String out = "\nfilename: " + filename; + out += "\nmimeType: " + mimeType; + out += "\nmodificationTime: " + modificationTime; + out += "\noriginalSize: " + originalSize; + return out; + } + +} -- cgit v1.2.3 From 940a1d8eebe297e6a94f417a24c0403f1d8d650f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Mon, 11 Aug 2014 23:56:25 +0200 Subject: Add EXTRA_ORIGINAL_FILENAME --- src/org/openintents/openpgp/util/OpenPgpApi.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/org/openintents/openpgp/util/OpenPgpApi.java b/src/org/openintents/openpgp/util/OpenPgpApi.java index 23844750..485d3710 100644 --- a/src/org/openintents/openpgp/util/OpenPgpApi.java +++ b/src/org/openintents/openpgp/util/OpenPgpApi.java @@ -45,7 +45,7 @@ public class OpenPgpApi { *

* 4: * - No changes to existing methods -> backward compatible - * - Introduction of ACTION_DECRYPT_METADATA, RESULT_METADATA, and OpenPgpDecryptMetadata parcel + * - Introduction of ACTION_DECRYPT_METADATA, RESULT_METADATA, EXTRA_ORIGINAL_FILENAME, and OpenPgpMetadata parcel * - Introduction of internal NFC extras: EXTRA_NFC_SIGNED_HASH, EXTRA_NFC_SIG_CREATION_TIMESTAMP */ public static final int API_VERSION = 4; @@ -83,6 +83,7 @@ public class OpenPgpApi { * optional extras: * boolean EXTRA_REQUEST_ASCII_ARMOR (request ascii armor for output) * String EXTRA_PASSPHRASE (key passphrase) + * String EXTRA_ORIGINAL_FILENAME (original filename to be encrypted as metadata) */ public static final String ACTION_ENCRYPT = "org.openintents.openpgp.action.ENCRYPT"; @@ -97,6 +98,7 @@ public class OpenPgpApi { * optional extras: * boolean EXTRA_REQUEST_ASCII_ARMOR (request ascii armor for output) * String EXTRA_PASSPHRASE (key passphrase) + * String EXTRA_ORIGINAL_FILENAME (original filename to be encrypted as metadata) */ public static final String ACTION_SIGN_AND_ENCRYPT = "org.openintents.openpgp.action.SIGN_AND_ENCRYPT"; @@ -164,6 +166,7 @@ public class OpenPgpApi { public static final String EXTRA_KEY_IDS = "key_ids"; // optional extras: public static final String EXTRA_PASSPHRASE = "passphrase"; + public static final String EXTRA_ORIGINAL_FILENAME = "original_filename"; // internal NFC states public static final String EXTRA_NFC_SIGNED_HASH = "nfc_signed_hash"; -- cgit v1.2.3 From cbfeb90cec9d9ecea2576eddf42e37154286c667 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Tue, 12 Aug 2014 13:46:34 +0200 Subject: Refactor OpenPgpServiceConnection with better callback --- .../openpgp/util/OpenPgpServiceConnection.java | 29 ++++++++++++---------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/org/openintents/openpgp/util/OpenPgpServiceConnection.java b/src/org/openintents/openpgp/util/OpenPgpServiceConnection.java index 0395a7bc..2700caf0 100644 --- a/src/org/openintents/openpgp/util/OpenPgpServiceConnection.java +++ b/src/org/openintents/openpgp/util/OpenPgpServiceConnection.java @@ -26,9 +26,11 @@ import android.os.IBinder; public class OpenPgpServiceConnection { - // interface to create callbacks for onServiceConnected + // callback interface public interface OnBound { public void onBound(IOpenPgpService service); + + public void onError(Exception e); } private Context mApplicationContext; @@ -39,19 +41,19 @@ public class OpenPgpServiceConnection { private OnBound mOnBoundListener; /** - * Create new OpenPgpServiceConnection + * Create new connection * * @param context * @param providerPackageName specify package name of OpenPGP provider, * e.g., "org.sufficientlysecure.keychain" */ public OpenPgpServiceConnection(Context context, String providerPackageName) { - this.mApplicationContext = context.getApplicationContext(); + this.mApplicationContext = context; this.mProviderPackageName = providerPackageName; } /** - * Create new OpenPgpServiceConnection + * Create new connection with callback * * @param context * @param providerPackageName specify package name of OpenPGP provider, @@ -60,8 +62,7 @@ public class OpenPgpServiceConnection { */ public OpenPgpServiceConnection(Context context, String providerPackageName, OnBound onBoundListener) { - this.mApplicationContext = context.getApplicationContext(); - this.mProviderPackageName = providerPackageName; + this(context, providerPackageName); this.mOnBoundListener = onBoundListener; } @@ -91,23 +92,25 @@ public class OpenPgpServiceConnection { * * @return */ - public boolean bindToService() { + public void bindToService() { // if not already bound... if (mService == null) { try { - Intent serviceIntent = new Intent(); - serviceIntent.setAction(IOpenPgpService.class.getName()); + Intent serviceIntent = new Intent(OpenPgpApi.SERVICE_INTENT); // NOTE: setPackage is very important to restrict the intent to this provider only! serviceIntent.setPackage(mProviderPackageName); mApplicationContext.bindService(serviceIntent, mServiceConnection, Context.BIND_AUTO_CREATE); - - return true; } catch (Exception e) { - return false; + if (mOnBoundListener != null) { + mOnBoundListener.onError(e); + } } } else { - return true; + // already bound, but also inform client about it with callback + if (mOnBoundListener != null) { + mOnBoundListener.onBound(mService); + } } } -- cgit v1.2.3 From 9e1f732b23d3b5df042f627d65a8ca37cec023fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Tue, 12 Aug 2014 14:38:28 +0200 Subject: More fixes for OpenPgpServiceConnection --- src/org/openintents/openpgp/util/OpenPgpServiceConnection.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/org/openintents/openpgp/util/OpenPgpServiceConnection.java b/src/org/openintents/openpgp/util/OpenPgpServiceConnection.java index 2700caf0..dad27ccb 100644 --- a/src/org/openintents/openpgp/util/OpenPgpServiceConnection.java +++ b/src/org/openintents/openpgp/util/OpenPgpServiceConnection.java @@ -16,14 +16,14 @@ package org.openintents.openpgp.util; -import org.openintents.openpgp.IOpenPgpService; - import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.os.IBinder; +import org.openintents.openpgp.IOpenPgpService; + public class OpenPgpServiceConnection { // callback interface @@ -99,8 +99,11 @@ public class OpenPgpServiceConnection { Intent serviceIntent = new Intent(OpenPgpApi.SERVICE_INTENT); // NOTE: setPackage is very important to restrict the intent to this provider only! serviceIntent.setPackage(mProviderPackageName); - mApplicationContext.bindService(serviceIntent, mServiceConnection, + boolean connect = mApplicationContext.bindService(serviceIntent, mServiceConnection, Context.BIND_AUTO_CREATE); + if (!connect) { + throw new Exception("bindService() returned false!"); + } } catch (Exception e) { if (mOnBoundListener != null) { mOnBoundListener.onError(e); -- cgit v1.2.3 From 1d0eeef047c4938f4e25bc06f7e92e83aa46a4c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Tue, 12 Aug 2014 15:03:00 +0200 Subject: Use application context --- src/org/openintents/openpgp/util/OpenPgpServiceConnection.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/org/openintents/openpgp/util/OpenPgpServiceConnection.java b/src/org/openintents/openpgp/util/OpenPgpServiceConnection.java index dad27ccb..15096d9e 100644 --- a/src/org/openintents/openpgp/util/OpenPgpServiceConnection.java +++ b/src/org/openintents/openpgp/util/OpenPgpServiceConnection.java @@ -48,7 +48,7 @@ public class OpenPgpServiceConnection { * e.g., "org.sufficientlysecure.keychain" */ public OpenPgpServiceConnection(Context context, String providerPackageName) { - this.mApplicationContext = context; + this.mApplicationContext = context.getApplicationContext(); this.mProviderPackageName = providerPackageName; } -- cgit v1.2.3 From d15482c22a17c4d07b993d8e576dcb953620ee4b Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Sat, 30 Aug 2014 15:19:43 +0200 Subject: Add my Real-name to the Copyright statement of ParcelFileDescriptorUtil. --- src/org/openintents/openpgp/util/ParcelFileDescriptorUtil.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/org/openintents/openpgp/util/ParcelFileDescriptorUtil.java b/src/org/openintents/openpgp/util/ParcelFileDescriptorUtil.java index 58c62110..4fd4b39a 100644 --- a/src/org/openintents/openpgp/util/ParcelFileDescriptorUtil.java +++ b/src/org/openintents/openpgp/util/ParcelFileDescriptorUtil.java @@ -1,6 +1,6 @@ /* * Copyright (C) 2014 Dominik Schürmann - * 2013 Flow (http://stackoverflow.com/questions/18212152/transfer-inputstream-to-another-service-across-process-boundaries-with-parcelf) + * 2013 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,9 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +/** + * Partially based on Stackoverflow: Transfer InputStream to another Service (across process boundaries) + **/ public class ParcelFileDescriptorUtil { public interface IThreadListener { @@ -100,4 +103,4 @@ public class ParcelFileDescriptorUtil { } } } -} \ No newline at end of file +} -- cgit v1.2.3 From e0be0f648ade84a83c7561c0521853447ca8b3da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Sun, 31 Aug 2014 23:39:56 +0200 Subject: Add SIGNATURE_KEY_REVOKED, SIGNATURE_KEY_EXPIRED, rename SIGNATURE_PUB_KEY_UNKNOWN --- src/org/openintents/openpgp/OpenPgpSignatureResult.java | 12 ++++++++---- src/org/openintents/openpgp/util/OpenPgpApi.java | 6 ++++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/org/openintents/openpgp/OpenPgpSignatureResult.java b/src/org/openintents/openpgp/OpenPgpSignatureResult.java index 7a4d799d..23401a1d 100644 --- a/src/org/openintents/openpgp/OpenPgpSignatureResult.java +++ b/src/org/openintents/openpgp/OpenPgpSignatureResult.java @@ -37,12 +37,16 @@ public class OpenPgpSignatureResult implements Parcelable { // generic error on signature verification public static final int SIGNATURE_ERROR = 0; - // successfully verified signature, with certified public key + // successfully verified signature, with certified key public static final int SIGNATURE_SUCCESS_CERTIFIED = 1; - // no public key was found for this signature verification - public static final int SIGNATURE_UNKNOWN_PUB_KEY = 2; - // successfully verified signature, but with uncertified public key + // no key was found for this signature verification + public static final int SIGNATURE_KEY_MISSING = 2; + // successfully verified signature, but with uncertified key public static final int SIGNATURE_SUCCESS_UNCERTIFIED = 3; + // key has been revoked + public static final int SIGNATURE_KEY_REVOKED = 4; + // key is expired + public static final int SIGNATURE_KEY_EXPIRED = 5; int status; boolean signatureOnly; diff --git a/src/org/openintents/openpgp/util/OpenPgpApi.java b/src/org/openintents/openpgp/util/OpenPgpApi.java index 485d3710..a248a0e2 100644 --- a/src/org/openintents/openpgp/util/OpenPgpApi.java +++ b/src/org/openintents/openpgp/util/OpenPgpApi.java @@ -47,8 +47,10 @@ public class OpenPgpApi { * - No changes to existing methods -> backward compatible * - Introduction of ACTION_DECRYPT_METADATA, RESULT_METADATA, EXTRA_ORIGINAL_FILENAME, and OpenPgpMetadata parcel * - Introduction of internal NFC extras: EXTRA_NFC_SIGNED_HASH, EXTRA_NFC_SIG_CREATION_TIMESTAMP + * 5: + * - OpenPgpSignatureResult: new SIGNATURE_KEY_REVOKED and SIGNATURE_KEY_EXPIRED */ - public static final int API_VERSION = 4; + public static final int API_VERSION = 5; /** * General extras @@ -106,7 +108,7 @@ public class OpenPgpApi { * Decrypts and verifies given input stream. This methods handles encrypted-only, signed-and-encrypted, * and also signed-only input. *

- * If OpenPgpSignatureResult.getStatus() == OpenPgpSignatureResult.SIGNATURE_UNKNOWN_PUB_KEY + * If OpenPgpSignatureResult.getStatus() == OpenPgpSignatureResult.SIGNATURE_KEY_MISSING * in addition a PendingIntent is returned via RESULT_INTENT to download missing keys. *

* optional extras: -- cgit v1.2.3 From 575b9d2279cd36b607316b76af6a968d54102660 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Sun, 31 Aug 2014 23:48:21 +0200 Subject: Add userIds to OpenPgpSignatureResult --- .../openpgp/OpenPgpSignatureResult.java | 43 +++++++++++++++------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/src/org/openintents/openpgp/OpenPgpSignatureResult.java b/src/org/openintents/openpgp/OpenPgpSignatureResult.java index 23401a1d..3b78abcf 100644 --- a/src/org/openintents/openpgp/OpenPgpSignatureResult.java +++ b/src/org/openintents/openpgp/OpenPgpSignatureResult.java @@ -21,6 +21,7 @@ import android.os.Parcelable; import org.openintents.openpgp.util.OpenPgpUtils; +import java.util.ArrayList; import java.util.Locale; /** @@ -33,7 +34,7 @@ public class OpenPgpSignatureResult implements Parcelable { * old versions of the protocol (and thus old versions of this class), we need a versioning * system for the parcels sent between the clients and the providers. */ - public static final int PARCELABLE_VERSION = 1; + public static final int PARCELABLE_VERSION = 2; // generic error on signature verification public static final int SIGNATURE_ERROR = 0; @@ -50,7 +51,8 @@ public class OpenPgpSignatureResult implements Parcelable { int status; boolean signatureOnly; - String userId; + String primaryUserId; + ArrayList userIds; long keyId; public int getStatus() { @@ -69,12 +71,20 @@ public class OpenPgpSignatureResult implements Parcelable { this.signatureOnly = signatureOnly; } - public String getUserId() { - return userId; + public String getPrimaryUserId() { + return primaryUserId; } - public void setUserId(String userId) { - this.userId = userId; + public void setPrimaryUserId(String primaryUserId) { + this.primaryUserId = primaryUserId; + } + + public ArrayList getUserIds() { + return userIds; + } + + public void setUserIds(ArrayList userIds) { + this.userIds = userIds; } public long getKeyId() { @@ -90,18 +100,20 @@ public class OpenPgpSignatureResult implements Parcelable { } public OpenPgpSignatureResult(int signatureStatus, String signatureUserId, - boolean signatureOnly, long keyId) { + boolean signatureOnly, long keyId, ArrayList userIds) { this.status = signatureStatus; this.signatureOnly = signatureOnly; - this.userId = signatureUserId; + this.primaryUserId = signatureUserId; this.keyId = keyId; + this.userIds = userIds; } public OpenPgpSignatureResult(OpenPgpSignatureResult b) { this.status = b.status; - this.userId = b.userId; + this.primaryUserId = b.primaryUserId; this.signatureOnly = b.signatureOnly; this.keyId = b.keyId; + this.userIds = b.userIds; } public int describeContents() { @@ -122,8 +134,10 @@ public class OpenPgpSignatureResult implements Parcelable { // version 1 dest.writeInt(status); dest.writeByte((byte) (signatureOnly ? 1 : 0)); - dest.writeString(userId); + dest.writeString(primaryUserId); dest.writeLong(keyId); + // version 2 + dest.writeList(userIds); // Go back and write the size int parcelableSize = dest.dataPosition() - startPosition; dest.setDataPosition(sizePosition); @@ -140,8 +154,9 @@ public class OpenPgpSignatureResult implements Parcelable { OpenPgpSignatureResult vr = new OpenPgpSignatureResult(); vr.status = source.readInt(); vr.signatureOnly = source.readByte() == 1; - vr.userId = source.readString(); + vr.primaryUserId = source.readString(); vr.keyId = source.readLong(); + source.readStringList(vr.userIds); // skip over all fields added in future versions of this parcel source.setDataPosition(startPosition + parcelableSize); @@ -156,9 +171,9 @@ public class OpenPgpSignatureResult implements Parcelable { @Override public String toString() { - String out = new String(); - out += "\nstatus: " + status; - out += "\nuserId: " + userId; + String out = "\nstatus: " + status; + out += "\nprimaryUserId: " + primaryUserId; + out += "\nuserIds: " + userIds; out += "\nsignatureOnly: " + signatureOnly; out += "\nkeyId: " + OpenPgpUtils.convertKeyIdToHex(keyId); return out; -- cgit v1.2.3 From 8b36d286680ee57b2181e86a3f02ba1278a81166 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Sun, 31 Aug 2014 23:49:34 +0200 Subject: Changelog --- src/org/openintents/openpgp/util/OpenPgpApi.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/org/openintents/openpgp/util/OpenPgpApi.java b/src/org/openintents/openpgp/util/OpenPgpApi.java index a248a0e2..a7c2ad6f 100644 --- a/src/org/openintents/openpgp/util/OpenPgpApi.java +++ b/src/org/openintents/openpgp/util/OpenPgpApi.java @@ -48,7 +48,8 @@ public class OpenPgpApi { * - Introduction of ACTION_DECRYPT_METADATA, RESULT_METADATA, EXTRA_ORIGINAL_FILENAME, and OpenPgpMetadata parcel * - Introduction of internal NFC extras: EXTRA_NFC_SIGNED_HASH, EXTRA_NFC_SIG_CREATION_TIMESTAMP * 5: - * - OpenPgpSignatureResult: new SIGNATURE_KEY_REVOKED and SIGNATURE_KEY_EXPIRED + * - OpenPgpSignatureResult: new consts SIGNATURE_KEY_REVOKED and SIGNATURE_KEY_EXPIRED + * - OpenPgpSignatureResult: ArrayList userIds */ public static final int API_VERSION = 5; -- cgit v1.2.3 From 3bcf7266c3944c5aad417ae339eaf4420783877d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Tue, 2 Sep 2014 16:11:07 +0200 Subject: Update README --- README.md | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index da8361e8..ac54aa29 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,21 @@ -# OpenPGP Remote API library +# OpenPGP API library + +The OpenPGP API provides methods to do common OpenPGP operations, such as sign, encrypt, decrypt, verify, and more without user interaction from background threads. This is done by connecting your client application to a remote service provided by OpenKeychain or other OpenPGP providers. + +For usage instructions, please consult our Wiki page about the [OpenPGP API](https://github.com/open-keychain/open-keychain/wiki/OpenPGP-API). + +License +======= + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. -For usage instructions, please consult our Wiki page about the [OpenPGP Remote API](https://github.com/open-keychain/open-keychain/wiki/OpenPGP-API). \ No newline at end of file -- cgit v1.2.3 From bba03f2f2ff16550853cbf573f4cbf30b0e0f641 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Tue, 2 Sep 2014 16:11:46 +0200 Subject: Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ac54aa29..c9491400 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # OpenPGP API library -The OpenPGP API provides methods to do common OpenPGP operations, such as sign, encrypt, decrypt, verify, and more without user interaction from background threads. This is done by connecting your client application to a remote service provided by OpenKeychain or other OpenPGP providers. +The OpenPGP API provides methods to execute OpenPGP operations, such as sign, encrypt, decrypt, verify, and more without user interaction from background threads. This is done by connecting your client application to a remote service provided by OpenKeychain or other OpenPGP providers. For usage instructions, please consult our Wiki page about the [OpenPGP API](https://github.com/open-keychain/open-keychain/wiki/OpenPGP-API). -- cgit v1.2.3 From 9dd15131e1a5cd67f90574a9513940215a142661 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Tue, 2 Sep 2014 16:12:35 +0200 Subject: Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c9491400..aefc9ed3 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # OpenPGP API library -The OpenPGP API provides methods to execute OpenPGP operations, such as sign, encrypt, decrypt, verify, and more without user interaction from background threads. This is done by connecting your client application to a remote service provided by OpenKeychain or other OpenPGP providers. +The OpenPGP API provides methods to execute OpenPGP operations, such as sign, encrypt, decrypt, verify, and more without user interaction from background threads. This is done by connecting your client application to a remote service provided by [OpenKeychain](http://www.openkeychain.org) or other OpenPGP providers. For usage instructions, please consult our Wiki page about the [OpenPGP API](https://github.com/open-keychain/open-keychain/wiki/OpenPGP-API). -- cgit v1.2.3 From ec234ebf010a268922e8f3c08f21f9efd8cf1cea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Thu, 4 Sep 2014 01:28:06 +0200 Subject: Pull from transifex --- res/values-cs/strings.xml | 5 +++++ res/values-de/strings.xml | 5 +++++ res/values-es/strings.xml | 5 +++++ res/values-et/strings.xml | 2 ++ res/values-fi/strings.xml | 2 ++ res/values-fr/strings.xml | 5 +++++ res/values-is/strings.xml | 2 ++ res/values-it/strings.xml | 5 +++++ res/values-ja/strings.xml | 5 +++++ res/values-nl/strings.xml | 2 ++ res/values-pl/strings.xml | 2 ++ res/values-pt/strings.xml | 2 ++ res/values-ru/strings.xml | 5 +++++ res/values-sl/strings.xml | 5 +++++ res/values-tr/strings.xml | 2 ++ res/values-uk/strings.xml | 5 +++++ res/values-zh/strings.xml | 2 ++ 17 files changed, 61 insertions(+) create mode 100644 res/values-cs/strings.xml create mode 100644 res/values-de/strings.xml create mode 100644 res/values-es/strings.xml create mode 100644 res/values-et/strings.xml create mode 100644 res/values-fi/strings.xml create mode 100644 res/values-fr/strings.xml create mode 100644 res/values-is/strings.xml create mode 100644 res/values-it/strings.xml create mode 100644 res/values-ja/strings.xml create mode 100644 res/values-nl/strings.xml create mode 100644 res/values-pl/strings.xml create mode 100644 res/values-pt/strings.xml create mode 100644 res/values-ru/strings.xml create mode 100644 res/values-sl/strings.xml create mode 100644 res/values-tr/strings.xml create mode 100644 res/values-uk/strings.xml create mode 100644 res/values-zh/strings.xml diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml new file mode 100644 index 00000000..c9fe1fab --- /dev/null +++ b/res/values-cs/strings.xml @@ -0,0 +1,5 @@ + + + Žádný + Instalovat OpenKeychain pomocí %s + diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml new file mode 100644 index 00000000..91e800ad --- /dev/null +++ b/res/values-de/strings.xml @@ -0,0 +1,5 @@ + + + Keine Auswahl + Installiere OpenKeychain mit %s + diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml new file mode 100644 index 00000000..da8979b4 --- /dev/null +++ b/res/values-es/strings.xml @@ -0,0 +1,5 @@ + + + Ninguno + Instalar OpenKeychain mediante %s + diff --git a/res/values-et/strings.xml b/res/values-et/strings.xml new file mode 100644 index 00000000..c757504a --- /dev/null +++ b/res/values-et/strings.xml @@ -0,0 +1,2 @@ + + diff --git a/res/values-fi/strings.xml b/res/values-fi/strings.xml new file mode 100644 index 00000000..c757504a --- /dev/null +++ b/res/values-fi/strings.xml @@ -0,0 +1,2 @@ + + diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml new file mode 100644 index 00000000..9b36df2d --- /dev/null +++ b/res/values-fr/strings.xml @@ -0,0 +1,5 @@ + + + Aucun + Installer OpenKeychain par %s + diff --git a/res/values-is/strings.xml b/res/values-is/strings.xml new file mode 100644 index 00000000..c757504a --- /dev/null +++ b/res/values-is/strings.xml @@ -0,0 +1,2 @@ + + diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml new file mode 100644 index 00000000..23e8e801 --- /dev/null +++ b/res/values-it/strings.xml @@ -0,0 +1,5 @@ + + + Nessuno + Installa OpenKeychain via %s + diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml new file mode 100644 index 00000000..5e337f5a --- /dev/null +++ b/res/values-ja/strings.xml @@ -0,0 +1,5 @@ + + + 無し + %s 経由でOpenKeychainをインストール + diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml new file mode 100644 index 00000000..c757504a --- /dev/null +++ b/res/values-nl/strings.xml @@ -0,0 +1,2 @@ + + diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml new file mode 100644 index 00000000..c757504a --- /dev/null +++ b/res/values-pl/strings.xml @@ -0,0 +1,2 @@ + + diff --git a/res/values-pt/strings.xml b/res/values-pt/strings.xml new file mode 100644 index 00000000..c757504a --- /dev/null +++ b/res/values-pt/strings.xml @@ -0,0 +1,2 @@ + + diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml new file mode 100644 index 00000000..e8fd1ddf --- /dev/null +++ b/res/values-ru/strings.xml @@ -0,0 +1,5 @@ + + + Нет + Установить OpenKeychain через %s + diff --git a/res/values-sl/strings.xml b/res/values-sl/strings.xml new file mode 100644 index 00000000..20bf70b0 --- /dev/null +++ b/res/values-sl/strings.xml @@ -0,0 +1,5 @@ + + + Brez + Namesti OpenKeychain prek %s + diff --git a/res/values-tr/strings.xml b/res/values-tr/strings.xml new file mode 100644 index 00000000..c757504a --- /dev/null +++ b/res/values-tr/strings.xml @@ -0,0 +1,2 @@ + + diff --git a/res/values-uk/strings.xml b/res/values-uk/strings.xml new file mode 100644 index 00000000..baf600a9 --- /dev/null +++ b/res/values-uk/strings.xml @@ -0,0 +1,5 @@ + + + Жоден + Встановити OpenKeychain через %s + diff --git a/res/values-zh/strings.xml b/res/values-zh/strings.xml new file mode 100644 index 00000000..c757504a --- /dev/null +++ b/res/values-zh/strings.xml @@ -0,0 +1,2 @@ + + -- cgit v1.2.3 From f6d678cbe50ac2b68264ba62e87c2978765e78e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Sun, 7 Sep 2014 19:23:04 +0200 Subject: Fix nullpointer with new OpenPgpSignatureResult version --- src/org/openintents/openpgp/OpenPgpSignatureResult.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/org/openintents/openpgp/OpenPgpSignatureResult.java b/src/org/openintents/openpgp/OpenPgpSignatureResult.java index 3b78abcf..8ce4d3c0 100644 --- a/src/org/openintents/openpgp/OpenPgpSignatureResult.java +++ b/src/org/openintents/openpgp/OpenPgpSignatureResult.java @@ -156,6 +156,7 @@ public class OpenPgpSignatureResult implements Parcelable { vr.signatureOnly = source.readByte() == 1; vr.primaryUserId = source.readString(); vr.keyId = source.readLong(); + vr.userIds = new ArrayList(); source.readStringList(vr.userIds); // skip over all fields added in future versions of this parcel -- cgit v1.2.3 From 2cebac6a23e55627131947ba4d515c781fb5d31c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Sun, 7 Sep 2014 20:07:13 +0200 Subject: Fix for user ids in API --- src/org/openintents/openpgp/OpenPgpSignatureResult.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/org/openintents/openpgp/OpenPgpSignatureResult.java b/src/org/openintents/openpgp/OpenPgpSignatureResult.java index 8ce4d3c0..dbcd74b6 100644 --- a/src/org/openintents/openpgp/OpenPgpSignatureResult.java +++ b/src/org/openintents/openpgp/OpenPgpSignatureResult.java @@ -137,7 +137,7 @@ public class OpenPgpSignatureResult implements Parcelable { dest.writeString(primaryUserId); dest.writeLong(keyId); // version 2 - dest.writeList(userIds); + dest.writeStringList(userIds); // Go back and write the size int parcelableSize = dest.dataPosition() - startPosition; dest.setDataPosition(sizePosition); -- cgit v1.2.3 From 0be263d5d3effd2df5f976fa4a127017268749cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Mon, 8 Sep 2014 14:09:15 +0200 Subject: Work on Yubikey decryption --- src/org/openintents/openpgp/util/OpenPgpApi.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/org/openintents/openpgp/util/OpenPgpApi.java b/src/org/openintents/openpgp/util/OpenPgpApi.java index a7c2ad6f..3e18ab0c 100644 --- a/src/org/openintents/openpgp/util/OpenPgpApi.java +++ b/src/org/openintents/openpgp/util/OpenPgpApi.java @@ -174,6 +174,7 @@ public class OpenPgpApi { // internal NFC states public static final String EXTRA_NFC_SIGNED_HASH = "nfc_signed_hash"; public static final String EXTRA_NFC_SIG_CREATION_TIMESTAMP = "nfc_sig_creation_timestamp"; + public static final String EXTRA_NFC_DECRYPTED_SESSION_KEY = "nfc_decrypted_session_key"; // GET_KEY public static final String EXTRA_KEY_ID = "key_id"; -- cgit v1.2.3 From 64bdd7e731c11407bfe6b8b1ec6a254ffb989057 Mon Sep 17 00:00:00 2001 From: Sam Whited Date: Wed, 22 Oct 2014 12:38:44 -0400 Subject: Use Gradle build system --- .gitignore | 41 + .gitmodules | 9 + build.gradle | 16 + conversations/build.gradle | 28 + conversations/src/main/AndroidManifest.xml | 124 ++ .../main/java/eu/siacs/conversations/Config.java | 25 + .../eu/siacs/conversations/crypto/OtrEngine.java | 231 +++ .../eu/siacs/conversations/crypto/PgpEngine.java | 385 ++++ .../conversations/entities/AbstractEntity.java | 21 + .../eu/siacs/conversations/entities/Account.java | 399 ++++ .../eu/siacs/conversations/entities/Bookmark.java | 137 ++ .../eu/siacs/conversations/entities/Contact.java | 367 ++++ .../siacs/conversations/entities/Conversation.java | 500 +++++ .../siacs/conversations/entities/Downloadable.java | 21 + .../conversations/entities/DownloadableFile.java | 154 ++ .../eu/siacs/conversations/entities/ListItem.java | 7 + .../eu/siacs/conversations/entities/Message.java | 478 +++++ .../siacs/conversations/entities/MucOptions.java | 369 ++++ .../eu/siacs/conversations/entities/Presences.java | 76 + .../eu/siacs/conversations/entities/Roster.java | 83 + .../conversations/generator/AbstractGenerator.java | 48 + .../siacs/conversations/generator/IqGenerator.java | 96 + .../conversations/generator/MessageGenerator.java | 178 ++ .../conversations/generator/PresenceGenerator.java | 57 + .../siacs/conversations/http/HttpConnection.java | 255 +++ .../conversations/http/HttpConnectionManager.java | 28 + .../siacs/conversations/parser/AbstractParser.java | 92 + .../eu/siacs/conversations/parser/IqParser.java | 92 + .../siacs/conversations/parser/MessageParser.java | 517 ++++++ .../siacs/conversations/parser/PresenceParser.java | 133 ++ .../conversations/persistance/DatabaseBackend.java | 335 ++++ .../conversations/persistance/FileBackend.java | 480 +++++ .../persistance/OnPhoneContactsMerged.java | 5 + .../services/AbstractConnectionManager.java | 23 + .../conversations/services/AvatarService.java | 298 +++ .../conversations/services/EventReceiver.java | 24 + .../services/NotificationService.java | 237 +++ .../services/XmppConnectionService.java | 1927 ++++++++++++++++++++ .../conversations/ui/ChooseContactActivity.java | 145 ++ .../ui/ConferenceDetailsActivity.java | 280 +++ .../conversations/ui/ContactDetailsActivity.java | 436 +++++ .../conversations/ui/ConversationActivity.java | 947 ++++++++++ .../conversations/ui/ConversationFragment.java | 781 ++++++++ .../conversations/ui/EditAccountActivity.java | 423 +++++ .../eu/siacs/conversations/ui/EditMessage.java | 39 + .../conversations/ui/ManageAccountActivity.java | 217 +++ .../ui/PublishProfilePictureActivity.java | 242 +++ .../siacs/conversations/ui/SettingsActivity.java | 74 + .../siacs/conversations/ui/SettingsFragment.java | 15 + .../siacs/conversations/ui/ShareWithActivity.java | 185 ++ .../ui/StartConversationActivity.java | 677 +++++++ .../java/eu/siacs/conversations/ui/UiCallback.java | 11 + .../eu/siacs/conversations/ui/XmppActivity.java | 637 +++++++ .../conversations/ui/adapter/AccountAdapter.java | 102 ++ .../ui/adapter/ConversationAdapter.java | 135 ++ .../ui/adapter/KnownHostsAdapter.java | 74 + .../conversations/ui/adapter/ListItemAdapter.java | 44 + .../conversations/ui/adapter/MessageAdapter.java | 560 ++++++ .../eu/siacs/conversations/utils/CryptoHelper.java | 112 ++ .../eu/siacs/conversations/utils/DNSHelper.java | 185 ++ .../conversations/utils/ExceptionHandler.java | 44 + .../siacs/conversations/utils/ExceptionHelper.java | 117 ++ .../utils/OnPhoneContactsLoadedListener.java | 9 + .../eu/siacs/conversations/utils/PRNGFixes.java | 327 ++++ .../eu/siacs/conversations/utils/PhoneHelper.java | 95 + .../eu/siacs/conversations/utils/UIHelper.java | 225 +++ .../eu/siacs/conversations/utils/Validator.java | 14 + .../eu/siacs/conversations/utils/XmlHelper.java | 12 + .../conversations/utils/zlib/ZLibInputStream.java | 54 + .../conversations/utils/zlib/ZLibOutputStream.java | 95 + .../java/eu/siacs/conversations/xml/Element.java | 148 ++ .../main/java/eu/siacs/conversations/xml/Tag.java | 104 ++ .../java/eu/siacs/conversations/xml/TagWriter.java | 114 ++ .../java/eu/siacs/conversations/xml/XmlReader.java | 141 ++ .../siacs/conversations/xmpp/OnBindListener.java | 7 + .../conversations/xmpp/OnContactStatusChanged.java | 7 + .../conversations/xmpp/OnIqPacketReceived.java | 8 + .../conversations/xmpp/OnMessageAcknowledged.java | 7 + .../xmpp/OnMessagePacketReceived.java | 8 + .../xmpp/OnPresencePacketReceived.java | 8 + .../siacs/conversations/xmpp/OnStatusChanged.java | 7 + .../siacs/conversations/xmpp/PacketReceived.java | 5 + .../siacs/conversations/xmpp/XmppConnection.java | 1130 ++++++++++++ .../conversations/xmpp/jingle/JingleCandidate.java | 143 ++ .../xmpp/jingle/JingleConnection.java | 910 +++++++++ .../xmpp/jingle/JingleConnectionManager.java | 163 ++ .../xmpp/jingle/JingleInbandTransport.java | 191 ++ .../xmpp/jingle/JingleSocks5Transport.java | 212 +++ .../conversations/xmpp/jingle/JingleTransport.java | 13 + .../jingle/OnFileTransmissionStatusChanged.java | 9 + .../xmpp/jingle/OnJinglePacketReceived.java | 9 + .../xmpp/jingle/OnPrimaryCandidateFound.java | 6 + .../xmpp/jingle/OnTransportConnected.java | 7 + .../conversations/xmpp/jingle/stanzas/Content.java | 102 ++ .../xmpp/jingle/stanzas/JinglePacket.java | 95 + .../conversations/xmpp/jingle/stanzas/Reason.java | 13 + .../eu/siacs/conversations/xmpp/pep/Avatar.java | 71 + .../conversations/xmpp/stanzas/AbstractStanza.java | 34 + .../siacs/conversations/xmpp/stanzas/IqPacket.java | 76 + .../conversations/xmpp/stanzas/MessagePacket.java | 66 + .../conversations/xmpp/stanzas/PresencePacket.java | 8 + .../xmpp/stanzas/csi/ActivePacket.java | 10 + .../xmpp/stanzas/csi/InactivePacket.java | 10 + .../xmpp/stanzas/streammgmt/AckPacket.java | 13 + .../xmpp/stanzas/streammgmt/EnablePacket.java | 13 + .../xmpp/stanzas/streammgmt/RequestPacket.java | 12 + .../xmpp/stanzas/streammgmt/ResumePacket.java | 14 + .../main/res/drawable-hdpi/ic_action_add_group.png | Bin 0 -> 876 bytes .../res/drawable-hdpi/ic_action_add_person.png | Bin 0 -> 616 bytes .../src/main/res/drawable-hdpi/ic_action_chat.png | Bin 0 -> 295 bytes .../src/main/res/drawable-hdpi/ic_action_copy.png | Bin 0 -> 381 bytes .../main/res/drawable-hdpi/ic_action_discard.png | Bin 0 -> 450 bytes .../src/main/res/drawable-hdpi/ic_action_edit.png | Bin 0 -> 765 bytes .../main/res/drawable-hdpi/ic_action_edit_dark.png | Bin 0 -> 884 bytes .../src/main/res/drawable-hdpi/ic_action_group.png | Bin 0 -> 776 bytes .../src/main/res/drawable-hdpi/ic_action_new.png | Bin 0 -> 262 bytes .../res/drawable-hdpi/ic_action_new_attachment.png | Bin 0 -> 587 bytes .../res/drawable-hdpi/ic_action_not_secure.png | Bin 0 -> 367 bytes .../main/res/drawable-hdpi/ic_action_refresh.png | Bin 0 -> 678 bytes .../main/res/drawable-hdpi/ic_action_remove.png | Bin 0 -> 448 bytes .../main/res/drawable-hdpi/ic_action_search.png | Bin 0 -> 650 bytes .../main/res/drawable-hdpi/ic_action_secure.png | Bin 0 -> 384 bytes .../res/drawable-hdpi/ic_action_send_now_away.png | Bin 0 -> 932 bytes .../res/drawable-hdpi/ic_action_send_now_dnd.png | Bin 0 -> 1135 bytes .../drawable-hdpi/ic_action_send_now_offline.png | Bin 0 -> 767 bytes .../drawable-hdpi/ic_action_send_now_online.png | Bin 0 -> 1095 bytes .../src/main/res/drawable-hdpi/ic_activity.png | Bin 0 -> 3040 bytes .../src/main/res/drawable-hdpi/ic_indicator.png | Bin 0 -> 684 bytes .../src/main/res/drawable-hdpi/ic_launcher.png | Bin 0 -> 4416 bytes .../src/main/res/drawable-hdpi/ic_notification.png | Bin 0 -> 1033 bytes .../src/main/res/drawable-hdpi/ic_profile.png | Bin 0 -> 999 bytes .../res/drawable-hdpi/ic_received_indicator.png | Bin 0 -> 686 bytes .../main/res/drawable-hdpi/ic_secure_indicator.png | Bin 0 -> 294 bytes .../drawable-hdpi/tab_selected_conversations.9.png | Bin 0 -> 99 bytes .../tab_selected_focused_conversations.9.png | Bin 0 -> 99 bytes .../tab_selected_pressed_conversations.9.png | Bin 0 -> 105 bytes .../tab_unselected_conversations.9.png | Bin 0 -> 101 bytes .../tab_unselected_focused_conversations.9.png | Bin 0 -> 93 bytes .../tab_unselected_pressed_conversations.9.png | Bin 0 -> 100 bytes .../main/res/drawable-mdpi/ic_action_add_group.png | Bin 0 -> 634 bytes .../res/drawable-mdpi/ic_action_add_person.png | Bin 0 -> 469 bytes .../src/main/res/drawable-mdpi/ic_action_chat.png | Bin 0 -> 261 bytes .../src/main/res/drawable-mdpi/ic_action_copy.png | Bin 0 -> 288 bytes .../main/res/drawable-mdpi/ic_action_discard.png | Bin 0 -> 324 bytes .../src/main/res/drawable-mdpi/ic_action_edit.png | Bin 0 -> 522 bytes .../main/res/drawable-mdpi/ic_action_edit_dark.png | Bin 0 -> 587 bytes .../src/main/res/drawable-mdpi/ic_action_group.png | Bin 0 -> 546 bytes .../src/main/res/drawable-mdpi/ic_action_new.png | Bin 0 -> 185 bytes .../res/drawable-mdpi/ic_action_new_attachment.png | Bin 0 -> 415 bytes .../res/drawable-mdpi/ic_action_not_secure.png | Bin 0 -> 298 bytes .../main/res/drawable-mdpi/ic_action_refresh.png | Bin 0 -> 507 bytes .../main/res/drawable-mdpi/ic_action_remove.png | Bin 0 -> 282 bytes .../main/res/drawable-mdpi/ic_action_search.png | Bin 0 -> 449 bytes .../main/res/drawable-mdpi/ic_action_secure.png | Bin 0 -> 304 bytes .../res/drawable-mdpi/ic_action_send_now_away.png | Bin 0 -> 650 bytes .../res/drawable-mdpi/ic_action_send_now_dnd.png | Bin 0 -> 784 bytes .../drawable-mdpi/ic_action_send_now_offline.png | Bin 0 -> 535 bytes .../drawable-mdpi/ic_action_send_now_online.png | Bin 0 -> 779 bytes .../src/main/res/drawable-mdpi/ic_activity.png | Bin 0 -> 1854 bytes .../src/main/res/drawable-mdpi/ic_indicator.png | Bin 0 -> 490 bytes .../src/main/res/drawable-mdpi/ic_launcher.png | Bin 0 -> 2726 bytes .../src/main/res/drawable-mdpi/ic_notification.png | Bin 0 -> 681 bytes .../src/main/res/drawable-mdpi/ic_profile.png | Bin 0 -> 622 bytes .../res/drawable-mdpi/ic_received_indicator.png | Bin 0 -> 447 bytes .../main/res/drawable-mdpi/ic_secure_indicator.png | Bin 0 -> 295 bytes .../drawable-mdpi/tab_selected_conversations.9.png | Bin 0 -> 96 bytes .../tab_selected_focused_conversations.9.png | Bin 0 -> 96 bytes .../tab_selected_pressed_conversations.9.png | Bin 0 -> 102 bytes .../tab_unselected_conversations.9.png | Bin 0 -> 105 bytes .../tab_unselected_focused_conversations.9.png | Bin 0 -> 90 bytes .../tab_unselected_pressed_conversations.9.png | Bin 0 -> 97 bytes .../res/drawable-xhdpi/ic_action_add_group.png | Bin 0 -> 1122 bytes .../res/drawable-xhdpi/ic_action_add_person.png | Bin 0 -> 798 bytes .../src/main/res/drawable-xhdpi/ic_action_chat.png | Bin 0 -> 310 bytes .../src/main/res/drawable-xhdpi/ic_action_copy.png | Bin 0 -> 353 bytes .../main/res/drawable-xhdpi/ic_action_discard.png | Bin 0 -> 543 bytes .../src/main/res/drawable-xhdpi/ic_action_edit.png | Bin 0 -> 994 bytes .../res/drawable-xhdpi/ic_action_edit_dark.png | Bin 0 -> 1179 bytes .../main/res/drawable-xhdpi/ic_action_group.png | Bin 0 -> 1048 bytes .../src/main/res/drawable-xhdpi/ic_action_new.png | Bin 0 -> 234 bytes .../drawable-xhdpi/ic_action_new_attachment.png | Bin 0 -> 753 bytes .../res/drawable-xhdpi/ic_action_not_secure.png | Bin 0 -> 482 bytes .../main/res/drawable-xhdpi/ic_action_refresh.png | Bin 0 -> 901 bytes .../main/res/drawable-xhdpi/ic_action_remove.png | Bin 0 -> 513 bytes .../main/res/drawable-xhdpi/ic_action_search.png | Bin 0 -> 827 bytes .../main/res/drawable-xhdpi/ic_action_secure.png | Bin 0 -> 468 bytes .../res/drawable-xhdpi/ic_action_send_now_away.png | Bin 0 -> 1180 bytes .../res/drawable-xhdpi/ic_action_send_now_dnd.png | Bin 0 -> 1438 bytes .../drawable-xhdpi/ic_action_send_now_offline.png | Bin 0 -> 968 bytes .../drawable-xhdpi/ic_action_send_now_online.png | Bin 0 -> 1395 bytes .../src/main/res/drawable-xhdpi/ic_activity.png | Bin 0 -> 4349 bytes .../src/main/res/drawable-xhdpi/ic_indicator.png | Bin 0 -> 915 bytes .../src/main/res/drawable-xhdpi/ic_launcher.png | Bin 0 -> 6503 bytes .../main/res/drawable-xhdpi/ic_notification.png | Bin 0 -> 1407 bytes .../src/main/res/drawable-xhdpi/ic_profile.png | Bin 0 -> 1374 bytes .../res/drawable-xhdpi/ic_received_indicator.png | Bin 0 -> 855 bytes .../res/drawable-xhdpi/ic_secure_indicator.png | Bin 0 -> 410 bytes .../tab_selected_conversations.9.png | Bin 0 -> 104 bytes .../tab_selected_focused_conversations.9.png | Bin 0 -> 103 bytes .../tab_selected_pressed_conversations.9.png | Bin 0 -> 110 bytes .../tab_unselected_conversations.9.png | Bin 0 -> 112 bytes .../tab_unselected_focused_conversations.9.png | Bin 0 -> 93 bytes .../tab_unselected_pressed_conversations.9.png | Bin 0 -> 101 bytes .../res/drawable-xxhdpi/ic_action_add_group.png | Bin 0 -> 1643 bytes .../res/drawable-xxhdpi/ic_action_add_person.png | Bin 0 -> 1088 bytes .../main/res/drawable-xxhdpi/ic_action_chat.png | Bin 0 -> 383 bytes .../main/res/drawable-xxhdpi/ic_action_copy.png | Bin 0 -> 470 bytes .../main/res/drawable-xxhdpi/ic_action_discard.png | Bin 0 -> 765 bytes .../main/res/drawable-xxhdpi/ic_action_edit.png | Bin 0 -> 1458 bytes .../res/drawable-xxhdpi/ic_action_edit_dark.png | Bin 0 -> 1670 bytes .../main/res/drawable-xxhdpi/ic_action_group.png | Bin 0 -> 1475 bytes .../src/main/res/drawable-xxhdpi/ic_action_new.png | Bin 0 -> 288 bytes .../drawable-xxhdpi/ic_action_new_attachment.png | Bin 0 -> 1048 bytes .../res/drawable-xxhdpi/ic_action_not_secure.png | Bin 0 -> 593 bytes .../main/res/drawable-xxhdpi/ic_action_refresh.png | Bin 0 -> 1274 bytes .../main/res/drawable-xxhdpi/ic_action_remove.png | Bin 0 -> 681 bytes .../main/res/drawable-xxhdpi/ic_action_search.png | Bin 0 -> 1152 bytes .../main/res/drawable-xxhdpi/ic_action_secure.png | Bin 0 -> 586 bytes .../drawable-xxhdpi/ic_action_send_now_away.png | Bin 0 -> 1426 bytes .../res/drawable-xxhdpi/ic_action_send_now_dnd.png | Bin 0 -> 1456 bytes .../drawable-xxhdpi/ic_action_send_now_offline.png | Bin 0 -> 1433 bytes .../drawable-xxhdpi/ic_action_send_now_online.png | Bin 0 -> 1458 bytes .../src/main/res/drawable-xxhdpi/ic_activity.png | Bin 0 -> 7209 bytes .../src/main/res/drawable-xxhdpi/ic_indicator.png | Bin 0 -> 1298 bytes .../src/main/res/drawable-xxhdpi/ic_launcher.png | Bin 0 -> 11054 bytes .../main/res/drawable-xxhdpi/ic_notification.png | Bin 0 -> 2250 bytes .../src/main/res/drawable-xxhdpi/ic_profile.png | Bin 0 -> 2137 bytes .../res/drawable-xxhdpi/ic_received_indicator.png | Bin 0 -> 1236 bytes .../res/drawable-xxhdpi/ic_secure_indicator.png | Bin 0 -> 380 bytes .../tab_selected_conversations.9.png | Bin 0 -> 108 bytes .../tab_selected_focused_conversations.9.png | Bin 0 -> 108 bytes .../tab_selected_pressed_conversations.9.png | Bin 0 -> 114 bytes .../tab_unselected_conversations.9.png | Bin 0 -> 109 bytes .../tab_unselected_focused_conversations.9.png | Bin 0 -> 95 bytes .../tab_unselected_pressed_conversations.9.png | Bin 0 -> 102 bytes .../main/res/drawable/actionbar_tab_indicator.xml | 21 + .../main/res/drawable/es_slidingpane_shadow.xml | 12 + conversations/src/main/res/drawable/grey.xml | 7 + .../src/main/res/drawable/greybackground.xml | 6 + .../src/main/res/drawable/infocard_border.xml | 19 + .../src/main/res/drawable/message_border.xml | 15 + conversations/src/main/res/drawable/snackbar.xml | 14 + .../fragment_conversations_overview.xml | 30 + .../fragment_conversations_overview.xml | 30 + .../fragment_conversations_overview.xml | 30 + .../fragment_conversations_overview.xml | 32 + conversations/src/main/res/layout/account_row.xml | 43 + .../src/main/res/layout/actionview_search.xml | 19 + .../main/res/layout/activity_choose_contact.xml | 13 + .../main/res/layout/activity_contact_details.xml | 114 ++ .../src/main/res/layout/activity_edit_account.xml | 272 +++ .../src/main/res/layout/activity_muc_details.xml | 119 ++ .../layout/activity_publish_profile_picture.xml | 106 ++ .../res/layout/activity_start_conversation.xml | 8 + conversations/src/main/res/layout/contact.xml | 51 + conversations/src/main/res/layout/contact_key.xml | 41 + .../src/main/res/layout/conversation_list_row.xml | 68 + .../src/main/res/layout/create_contact_dialog.xml | 39 + .../src/main/res/layout/dialog_clear_history.xml | 21 + .../src/main/res/layout/dialog_verify_otr.xml | 60 + .../src/main/res/layout/fragment_conversation.xml | 102 ++ .../res/layout/fragment_conversations_overview.xml | 30 + .../src/main/res/layout/join_conference_dialog.xml | 47 + .../src/main/res/layout/manage_accounts.xml | 16 + conversations/src/main/res/layout/message_null.xml | 7 + .../src/main/res/layout/message_received.xml | 97 + conversations/src/main/res/layout/message_sent.xml | 108 ++ .../src/main/res/layout/message_status.xml | 22 + conversations/src/main/res/layout/quickedit.xml | 19 + conversations/src/main/res/layout/share_with.xml | 13 + .../src/main/res/menu/attachment_choices.xml | 15 + conversations/src/main/res/menu/choose_contact.xml | 11 + .../src/main/res/menu/conference_context.xml | 11 + .../src/main/res/menu/contact_context.xml | 14 + .../src/main/res/menu/contact_details.xml | 27 + conversations/src/main/res/menu/conversations.xml | 63 + .../src/main/res/menu/encryption_choices.xml | 16 + conversations/src/main/res/menu/manageaccounts.xml | 15 + .../src/main/res/menu/manageaccounts_context.xml | 21 + conversations/src/main/res/menu/muc_details.xml | 21 + conversations/src/main/res/menu/share_with.xml | 11 + .../src/main/res/menu/start_conversation.xml | 31 + conversations/src/main/res/values-ca/arrays.xml | 24 + conversations/src/main/res/values-ca/strings.xml | 83 + conversations/src/main/res/values-cs/arrays.xml | 39 + conversations/src/main/res/values-cs/strings.xml | 260 +++ conversations/src/main/res/values-de/arrays.xml | 31 + conversations/src/main/res/values-de/strings.xml | 269 +++ conversations/src/main/res/values-es/arrays.xml | 39 + conversations/src/main/res/values-es/strings.xml | 269 +++ conversations/src/main/res/values-eu/arrays.xml | 39 + conversations/src/main/res/values-eu/strings.xml | 276 +++ conversations/src/main/res/values-fr/arrays.xml | 24 + conversations/src/main/res/values-fr/strings.xml | 273 +++ conversations/src/main/res/values-gl/arrays.xml | 24 + conversations/src/main/res/values-gl/strings.xml | 130 ++ conversations/src/main/res/values-it/arrays.xml | 39 + conversations/src/main/res/values-it/strings.xml | 260 +++ conversations/src/main/res/values-iw/arrays.xml | 24 + conversations/src/main/res/values-iw/strings.xml | 224 +++ conversations/src/main/res/values-nl/arrays.xml | 24 + conversations/src/main/res/values-nl/strings.xml | 233 +++ conversations/src/main/res/values-ru/arrays.xml | 24 + conversations/src/main/res/values-ru/strings.xml | 260 +++ conversations/src/main/res/values-sv/arrays.xml | 24 + conversations/src/main/res/values-sv/strings.xml | 260 +++ .../src/main/res/values-zh-rCN/arrays.xml | 39 + .../src/main/res/values-zh-rCN/strings.xml | 260 +++ .../src/main/res/values-zh-rTW/arrays.xml | 39 + .../src/main/res/values-zh-rTW/strings.xml | 263 +++ conversations/src/main/res/values/arrays.xml | 39 + conversations/src/main/res/values/attrs.xml | 8 + conversations/src/main/res/values/colors.xml | 17 + conversations/src/main/res/values/strings.xml | 276 +++ conversations/src/main/res/values/styles.xml | 8 + conversations/src/main/res/values/themes.xml | 35 + conversations/src/main/res/xml/preferences.xml | 114 ++ gradlew | 164 ++ gradlew.bat | 90 + memorizingTrustManager | 1 + minidns | 1 + openpgpapilib | 1 + settings.gradle | 4 + 323 files changed, 25921 insertions(+) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 build.gradle create mode 100644 conversations/build.gradle create mode 100644 conversations/src/main/AndroidManifest.xml create mode 100644 conversations/src/main/java/eu/siacs/conversations/Config.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/crypto/OtrEngine.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/entities/AbstractEntity.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/entities/Account.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/entities/Bookmark.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/entities/Contact.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/entities/Conversation.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/entities/Downloadable.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/entities/ListItem.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/entities/Message.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/entities/MucOptions.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/entities/Presences.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/entities/Roster.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/generator/IqGenerator.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/http/HttpConnection.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/parser/AbstractParser.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/parser/IqParser.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/parser/MessageParser.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/parser/PresenceParser.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/persistance/FileBackend.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/persistance/OnPhoneContactsMerged.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/services/AvatarService.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/services/EventReceiver.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/services/NotificationService.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/ui/EditMessage.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/ui/SettingsFragment.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/ui/UiCallback.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/ui/XmppActivity.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/utils/DNSHelper.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/utils/OnPhoneContactsLoadedListener.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/utils/PRNGFixes.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/utils/UIHelper.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/utils/Validator.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/utils/XmlHelper.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/utils/zlib/ZLibInputStream.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/utils/zlib/ZLibOutputStream.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xml/Element.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xml/Tag.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xml/TagWriter.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xml/XmlReader.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/OnBindListener.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/OnContactStatusChanged.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/OnIqPacketReceived.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/OnMessageAcknowledged.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/OnMessagePacketReceived.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/OnPresencePacketReceived.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/OnStatusChanged.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/PacketReceived.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/OnFileTransmissionStatusChanged.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/OnJinglePacketReceived.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/OnPrimaryCandidateFound.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/OnTransportConnected.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractStanza.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/IqPacket.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/PresencePacket.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/ActivePacket.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/InactivePacket.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/AckPacket.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/EnablePacket.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/RequestPacket.java create mode 100644 conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/ResumePacket.java create mode 100644 conversations/src/main/res/drawable-hdpi/ic_action_add_group.png create mode 100644 conversations/src/main/res/drawable-hdpi/ic_action_add_person.png create mode 100644 conversations/src/main/res/drawable-hdpi/ic_action_chat.png create mode 100644 conversations/src/main/res/drawable-hdpi/ic_action_copy.png create mode 100644 conversations/src/main/res/drawable-hdpi/ic_action_discard.png create mode 100644 conversations/src/main/res/drawable-hdpi/ic_action_edit.png create mode 100644 conversations/src/main/res/drawable-hdpi/ic_action_edit_dark.png create mode 100644 conversations/src/main/res/drawable-hdpi/ic_action_group.png create mode 100644 conversations/src/main/res/drawable-hdpi/ic_action_new.png create mode 100644 conversations/src/main/res/drawable-hdpi/ic_action_new_attachment.png create mode 100644 conversations/src/main/res/drawable-hdpi/ic_action_not_secure.png create mode 100644 conversations/src/main/res/drawable-hdpi/ic_action_refresh.png create mode 100644 conversations/src/main/res/drawable-hdpi/ic_action_remove.png create mode 100644 conversations/src/main/res/drawable-hdpi/ic_action_search.png create mode 100644 conversations/src/main/res/drawable-hdpi/ic_action_secure.png create mode 100644 conversations/src/main/res/drawable-hdpi/ic_action_send_now_away.png create mode 100644 conversations/src/main/res/drawable-hdpi/ic_action_send_now_dnd.png create mode 100644 conversations/src/main/res/drawable-hdpi/ic_action_send_now_offline.png create mode 100644 conversations/src/main/res/drawable-hdpi/ic_action_send_now_online.png create mode 100644 conversations/src/main/res/drawable-hdpi/ic_activity.png create mode 100644 conversations/src/main/res/drawable-hdpi/ic_indicator.png create mode 100644 conversations/src/main/res/drawable-hdpi/ic_launcher.png create mode 100644 conversations/src/main/res/drawable-hdpi/ic_notification.png create mode 100644 conversations/src/main/res/drawable-hdpi/ic_profile.png create mode 100644 conversations/src/main/res/drawable-hdpi/ic_received_indicator.png create mode 100644 conversations/src/main/res/drawable-hdpi/ic_secure_indicator.png create mode 100644 conversations/src/main/res/drawable-hdpi/tab_selected_conversations.9.png create mode 100644 conversations/src/main/res/drawable-hdpi/tab_selected_focused_conversations.9.png create mode 100644 conversations/src/main/res/drawable-hdpi/tab_selected_pressed_conversations.9.png create mode 100644 conversations/src/main/res/drawable-hdpi/tab_unselected_conversations.9.png create mode 100644 conversations/src/main/res/drawable-hdpi/tab_unselected_focused_conversations.9.png create mode 100644 conversations/src/main/res/drawable-hdpi/tab_unselected_pressed_conversations.9.png create mode 100644 conversations/src/main/res/drawable-mdpi/ic_action_add_group.png create mode 100644 conversations/src/main/res/drawable-mdpi/ic_action_add_person.png create mode 100644 conversations/src/main/res/drawable-mdpi/ic_action_chat.png create mode 100644 conversations/src/main/res/drawable-mdpi/ic_action_copy.png create mode 100644 conversations/src/main/res/drawable-mdpi/ic_action_discard.png create mode 100644 conversations/src/main/res/drawable-mdpi/ic_action_edit.png create mode 100644 conversations/src/main/res/drawable-mdpi/ic_action_edit_dark.png create mode 100644 conversations/src/main/res/drawable-mdpi/ic_action_group.png create mode 100644 conversations/src/main/res/drawable-mdpi/ic_action_new.png create mode 100644 conversations/src/main/res/drawable-mdpi/ic_action_new_attachment.png create mode 100644 conversations/src/main/res/drawable-mdpi/ic_action_not_secure.png create mode 100644 conversations/src/main/res/drawable-mdpi/ic_action_refresh.png create mode 100644 conversations/src/main/res/drawable-mdpi/ic_action_remove.png create mode 100644 conversations/src/main/res/drawable-mdpi/ic_action_search.png create mode 100644 conversations/src/main/res/drawable-mdpi/ic_action_secure.png create mode 100644 conversations/src/main/res/drawable-mdpi/ic_action_send_now_away.png create mode 100644 conversations/src/main/res/drawable-mdpi/ic_action_send_now_dnd.png create mode 100644 conversations/src/main/res/drawable-mdpi/ic_action_send_now_offline.png create mode 100644 conversations/src/main/res/drawable-mdpi/ic_action_send_now_online.png create mode 100644 conversations/src/main/res/drawable-mdpi/ic_activity.png create mode 100644 conversations/src/main/res/drawable-mdpi/ic_indicator.png create mode 100644 conversations/src/main/res/drawable-mdpi/ic_launcher.png create mode 100644 conversations/src/main/res/drawable-mdpi/ic_notification.png create mode 100644 conversations/src/main/res/drawable-mdpi/ic_profile.png create mode 100644 conversations/src/main/res/drawable-mdpi/ic_received_indicator.png create mode 100644 conversations/src/main/res/drawable-mdpi/ic_secure_indicator.png create mode 100644 conversations/src/main/res/drawable-mdpi/tab_selected_conversations.9.png create mode 100644 conversations/src/main/res/drawable-mdpi/tab_selected_focused_conversations.9.png create mode 100644 conversations/src/main/res/drawable-mdpi/tab_selected_pressed_conversations.9.png create mode 100644 conversations/src/main/res/drawable-mdpi/tab_unselected_conversations.9.png create mode 100644 conversations/src/main/res/drawable-mdpi/tab_unselected_focused_conversations.9.png create mode 100644 conversations/src/main/res/drawable-mdpi/tab_unselected_pressed_conversations.9.png create mode 100644 conversations/src/main/res/drawable-xhdpi/ic_action_add_group.png create mode 100644 conversations/src/main/res/drawable-xhdpi/ic_action_add_person.png create mode 100644 conversations/src/main/res/drawable-xhdpi/ic_action_chat.png create mode 100644 conversations/src/main/res/drawable-xhdpi/ic_action_copy.png create mode 100644 conversations/src/main/res/drawable-xhdpi/ic_action_discard.png create mode 100644 conversations/src/main/res/drawable-xhdpi/ic_action_edit.png create mode 100644 conversations/src/main/res/drawable-xhdpi/ic_action_edit_dark.png create mode 100644 conversations/src/main/res/drawable-xhdpi/ic_action_group.png create mode 100644 conversations/src/main/res/drawable-xhdpi/ic_action_new.png create mode 100644 conversations/src/main/res/drawable-xhdpi/ic_action_new_attachment.png create mode 100644 conversations/src/main/res/drawable-xhdpi/ic_action_not_secure.png create mode 100644 conversations/src/main/res/drawable-xhdpi/ic_action_refresh.png create mode 100644 conversations/src/main/res/drawable-xhdpi/ic_action_remove.png create mode 100644 conversations/src/main/res/drawable-xhdpi/ic_action_search.png create mode 100644 conversations/src/main/res/drawable-xhdpi/ic_action_secure.png create mode 100644 conversations/src/main/res/drawable-xhdpi/ic_action_send_now_away.png create mode 100644 conversations/src/main/res/drawable-xhdpi/ic_action_send_now_dnd.png create mode 100644 conversations/src/main/res/drawable-xhdpi/ic_action_send_now_offline.png create mode 100644 conversations/src/main/res/drawable-xhdpi/ic_action_send_now_online.png create mode 100644 conversations/src/main/res/drawable-xhdpi/ic_activity.png create mode 100644 conversations/src/main/res/drawable-xhdpi/ic_indicator.png create mode 100644 conversations/src/main/res/drawable-xhdpi/ic_launcher.png create mode 100644 conversations/src/main/res/drawable-xhdpi/ic_notification.png create mode 100644 conversations/src/main/res/drawable-xhdpi/ic_profile.png create mode 100644 conversations/src/main/res/drawable-xhdpi/ic_received_indicator.png create mode 100644 conversations/src/main/res/drawable-xhdpi/ic_secure_indicator.png create mode 100644 conversations/src/main/res/drawable-xhdpi/tab_selected_conversations.9.png create mode 100644 conversations/src/main/res/drawable-xhdpi/tab_selected_focused_conversations.9.png create mode 100644 conversations/src/main/res/drawable-xhdpi/tab_selected_pressed_conversations.9.png create mode 100644 conversations/src/main/res/drawable-xhdpi/tab_unselected_conversations.9.png create mode 100644 conversations/src/main/res/drawable-xhdpi/tab_unselected_focused_conversations.9.png create mode 100644 conversations/src/main/res/drawable-xhdpi/tab_unselected_pressed_conversations.9.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/ic_action_add_group.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/ic_action_add_person.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/ic_action_chat.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/ic_action_copy.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/ic_action_discard.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/ic_action_edit.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/ic_action_edit_dark.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/ic_action_group.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/ic_action_new.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/ic_action_new_attachment.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/ic_action_not_secure.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/ic_action_refresh.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/ic_action_remove.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/ic_action_search.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/ic_action_secure.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/ic_action_send_now_away.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/ic_action_send_now_dnd.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/ic_action_send_now_offline.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/ic_action_send_now_online.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/ic_activity.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/ic_indicator.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/ic_launcher.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/ic_notification.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/ic_profile.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/ic_received_indicator.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/ic_secure_indicator.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/tab_selected_conversations.9.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/tab_selected_focused_conversations.9.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/tab_selected_pressed_conversations.9.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/tab_unselected_conversations.9.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/tab_unselected_focused_conversations.9.png create mode 100644 conversations/src/main/res/drawable-xxhdpi/tab_unselected_pressed_conversations.9.png create mode 100644 conversations/src/main/res/drawable/actionbar_tab_indicator.xml create mode 100644 conversations/src/main/res/drawable/es_slidingpane_shadow.xml create mode 100644 conversations/src/main/res/drawable/grey.xml create mode 100644 conversations/src/main/res/drawable/greybackground.xml create mode 100644 conversations/src/main/res/drawable/infocard_border.xml create mode 100644 conversations/src/main/res/drawable/message_border.xml create mode 100644 conversations/src/main/res/drawable/snackbar.xml create mode 100644 conversations/src/main/res/layout-w360dp/fragment_conversations_overview.xml create mode 100644 conversations/src/main/res/layout-w384dp/fragment_conversations_overview.xml create mode 100644 conversations/src/main/res/layout-w600dp/fragment_conversations_overview.xml create mode 100644 conversations/src/main/res/layout-w960dp/fragment_conversations_overview.xml create mode 100644 conversations/src/main/res/layout/account_row.xml create mode 100644 conversations/src/main/res/layout/actionview_search.xml create mode 100644 conversations/src/main/res/layout/activity_choose_contact.xml create mode 100644 conversations/src/main/res/layout/activity_contact_details.xml create mode 100644 conversations/src/main/res/layout/activity_edit_account.xml create mode 100644 conversations/src/main/res/layout/activity_muc_details.xml create mode 100644 conversations/src/main/res/layout/activity_publish_profile_picture.xml create mode 100644 conversations/src/main/res/layout/activity_start_conversation.xml create mode 100644 conversations/src/main/res/layout/contact.xml create mode 100644 conversations/src/main/res/layout/contact_key.xml create mode 100644 conversations/src/main/res/layout/conversation_list_row.xml create mode 100644 conversations/src/main/res/layout/create_contact_dialog.xml create mode 100644 conversations/src/main/res/layout/dialog_clear_history.xml create mode 100644 conversations/src/main/res/layout/dialog_verify_otr.xml create mode 100644 conversations/src/main/res/layout/fragment_conversation.xml create mode 100644 conversations/src/main/res/layout/fragment_conversations_overview.xml create mode 100644 conversations/src/main/res/layout/join_conference_dialog.xml create mode 100644 conversations/src/main/res/layout/manage_accounts.xml create mode 100644 conversations/src/main/res/layout/message_null.xml create mode 100644 conversations/src/main/res/layout/message_received.xml create mode 100644 conversations/src/main/res/layout/message_sent.xml create mode 100644 conversations/src/main/res/layout/message_status.xml create mode 100644 conversations/src/main/res/layout/quickedit.xml create mode 100644 conversations/src/main/res/layout/share_with.xml create mode 100644 conversations/src/main/res/menu/attachment_choices.xml create mode 100644 conversations/src/main/res/menu/choose_contact.xml create mode 100644 conversations/src/main/res/menu/conference_context.xml create mode 100644 conversations/src/main/res/menu/contact_context.xml create mode 100644 conversations/src/main/res/menu/contact_details.xml create mode 100644 conversations/src/main/res/menu/conversations.xml create mode 100644 conversations/src/main/res/menu/encryption_choices.xml create mode 100644 conversations/src/main/res/menu/manageaccounts.xml create mode 100644 conversations/src/main/res/menu/manageaccounts_context.xml create mode 100644 conversations/src/main/res/menu/muc_details.xml create mode 100644 conversations/src/main/res/menu/share_with.xml create mode 100644 conversations/src/main/res/menu/start_conversation.xml create mode 100644 conversations/src/main/res/values-ca/arrays.xml create mode 100644 conversations/src/main/res/values-ca/strings.xml create mode 100644 conversations/src/main/res/values-cs/arrays.xml create mode 100644 conversations/src/main/res/values-cs/strings.xml create mode 100644 conversations/src/main/res/values-de/arrays.xml create mode 100644 conversations/src/main/res/values-de/strings.xml create mode 100644 conversations/src/main/res/values-es/arrays.xml create mode 100644 conversations/src/main/res/values-es/strings.xml create mode 100644 conversations/src/main/res/values-eu/arrays.xml create mode 100644 conversations/src/main/res/values-eu/strings.xml create mode 100644 conversations/src/main/res/values-fr/arrays.xml create mode 100644 conversations/src/main/res/values-fr/strings.xml create mode 100644 conversations/src/main/res/values-gl/arrays.xml create mode 100644 conversations/src/main/res/values-gl/strings.xml create mode 100644 conversations/src/main/res/values-it/arrays.xml create mode 100644 conversations/src/main/res/values-it/strings.xml create mode 100644 conversations/src/main/res/values-iw/arrays.xml create mode 100644 conversations/src/main/res/values-iw/strings.xml create mode 100644 conversations/src/main/res/values-nl/arrays.xml create mode 100644 conversations/src/main/res/values-nl/strings.xml create mode 100644 conversations/src/main/res/values-ru/arrays.xml create mode 100644 conversations/src/main/res/values-ru/strings.xml create mode 100644 conversations/src/main/res/values-sv/arrays.xml create mode 100644 conversations/src/main/res/values-sv/strings.xml create mode 100644 conversations/src/main/res/values-zh-rCN/arrays.xml create mode 100644 conversations/src/main/res/values-zh-rCN/strings.xml create mode 100644 conversations/src/main/res/values-zh-rTW/arrays.xml create mode 100644 conversations/src/main/res/values-zh-rTW/strings.xml create mode 100644 conversations/src/main/res/values/arrays.xml create mode 100644 conversations/src/main/res/values/attrs.xml create mode 100644 conversations/src/main/res/values/colors.xml create mode 100644 conversations/src/main/res/values/strings.xml create mode 100644 conversations/src/main/res/values/styles.xml create mode 100644 conversations/src/main/res/values/themes.xml create mode 100644 conversations/src/main/res/xml/preferences.xml create mode 100755 gradlew create mode 100644 gradlew.bat create mode 160000 memorizingTrustManager create mode 160000 minidns create mode 160000 openpgpapilib create mode 100644 settings.gradle diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..e7f0d7fe --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +.classpath +*.swp +.settings + +# https://github.com/github/gitignore/blob/master/Gradle.gitignore +.gradle/ +gradle/ +build/ +# Ignore Gradle GUI config +gradle-app.setting + +# https://github.com/github/gitignore/blob/master/Android.gitignore +# Built application files +*.apk +*.ap_ + +# Files for the Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +*.iml +.idea + +import-summary.txt + +*.jar diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..e9290245 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,9 @@ +[submodule "minidns"] + path = minidns + url = https://github.com/rtreffer/minidns.git +[submodule "openpgpapilib"] + path = openpgpapilib + url = https://github.com/open-keychain/openpgp-api-lib.git +[submodule "memorizingTrustManager"] + path = memorizingTrustManager + url = https://github.com/iNPUTmice/MemorizingTrustManager.git diff --git a/build.gradle b/build.gradle new file mode 100644 index 00000000..8942ba1c --- /dev/null +++ b/build.gradle @@ -0,0 +1,16 @@ +// Top-level build file where you can add configuration options common to all +// sub-projects/modules. +buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:0.12.2' + } +} + +allprojects { + repositories { + jcenter() + } +} diff --git a/conversations/build.gradle b/conversations/build.gradle new file mode 100644 index 00000000..08060e59 --- /dev/null +++ b/conversations/build.gradle @@ -0,0 +1,28 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 19 + buildToolsVersion "20.0.0" + + defaultConfig { + applicationId "eu.siacs.conversations" + minSdkVersion 14 + targetSdkVersion 19 + } + + buildTypes { + release { + runProguard false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' + } + } +} + +dependencies { + compile project(':minidns') + compile project(':openpgpapilib') + compile project(':memorizingTrustManager') + compile files('libs/android-support-v13.jar') + compile files('libs/bcprov-jdk15on-150.jar') + compile files('libs/otr4j-0.10.jar') +} diff --git a/conversations/src/main/AndroidManifest.xml b/conversations/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a66de8ba --- /dev/null +++ b/conversations/src/main/AndroidManifest.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/conversations/src/main/java/eu/siacs/conversations/Config.java b/conversations/src/main/java/eu/siacs/conversations/Config.java new file mode 100644 index 00000000..1725eca6 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/Config.java @@ -0,0 +1,25 @@ +package eu.siacs.conversations; + +import android.graphics.Bitmap; + +public final class Config { + + public static final String LOGTAG = "conversations"; + + public static final int PING_MAX_INTERVAL = 300; + public static final int PING_MIN_INTERVAL = 30; + public static final int PING_TIMEOUT = 10; + public static final int CONNECT_TIMEOUT = 90; + public static final int CARBON_GRACE_PERIOD = 60; + + public static final int AVATAR_SIZE = 192; + public static final Bitmap.CompressFormat AVATAR_FORMAT = Bitmap.CompressFormat.WEBP; + + public static final int MESSAGE_MERGE_WINDOW = 20; + + public static final boolean PARSE_EMOTICONS = false; + + private Config() { + + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/crypto/OtrEngine.java b/conversations/src/main/java/eu/siacs/conversations/crypto/OtrEngine.java new file mode 100644 index 00000000..e0bd0e79 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/crypto/OtrEngine.java @@ -0,0 +1,231 @@ +package eu.siacs.conversations.crypto; + +import java.math.BigInteger; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.DSAPrivateKeySpec; +import java.security.spec.DSAPublicKeySpec; +import java.security.spec.InvalidKeySpecException; + +import org.json.JSONException; +import org.json.JSONObject; + +import android.util.Log; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xmpp.stanzas.MessagePacket; + +import net.java.otr4j.OtrEngineHost; +import net.java.otr4j.OtrException; +import net.java.otr4j.OtrPolicy; +import net.java.otr4j.OtrPolicyImpl; +import net.java.otr4j.session.InstanceTag; +import net.java.otr4j.session.SessionID; + +public class OtrEngine implements OtrEngineHost { + + private Account account; + private OtrPolicy otrPolicy; + private KeyPair keyPair; + private XmppConnectionService mXmppConnectionService; + + public OtrEngine(XmppConnectionService service, Account account) { + this.account = account; + this.otrPolicy = new OtrPolicyImpl(); + this.otrPolicy.setAllowV1(false); + this.otrPolicy.setAllowV2(true); + this.otrPolicy.setAllowV3(true); + this.keyPair = loadKey(account.getKeys()); + this.mXmppConnectionService = service; + } + + private KeyPair loadKey(JSONObject keys) { + if (keys == null) { + return null; + } + try { + BigInteger x = new BigInteger(keys.getString("otr_x"), 16); + BigInteger y = new BigInteger(keys.getString("otr_y"), 16); + BigInteger p = new BigInteger(keys.getString("otr_p"), 16); + BigInteger q = new BigInteger(keys.getString("otr_q"), 16); + BigInteger g = new BigInteger(keys.getString("otr_g"), 16); + KeyFactory keyFactory = KeyFactory.getInstance("DSA"); + DSAPublicKeySpec pubKeySpec = new DSAPublicKeySpec(y, p, q, g); + DSAPrivateKeySpec privateKeySpec = new DSAPrivateKeySpec(x, p, q, g); + PublicKey publicKey = keyFactory.generatePublic(pubKeySpec); + PrivateKey privateKey = keyFactory.generatePrivate(privateKeySpec); + return new KeyPair(publicKey, privateKey); + } catch (JSONException e) { + return null; + } catch (NoSuchAlgorithmException e) { + return null; + } catch (InvalidKeySpecException e) { + return null; + } + } + + private void saveKey() { + PublicKey publicKey = keyPair.getPublic(); + PrivateKey privateKey = keyPair.getPrivate(); + KeyFactory keyFactory; + try { + keyFactory = KeyFactory.getInstance("DSA"); + DSAPrivateKeySpec privateKeySpec = keyFactory.getKeySpec( + privateKey, DSAPrivateKeySpec.class); + DSAPublicKeySpec publicKeySpec = keyFactory.getKeySpec(publicKey, + DSAPublicKeySpec.class); + this.account.setKey("otr_x", privateKeySpec.getX().toString(16)); + this.account.setKey("otr_g", privateKeySpec.getG().toString(16)); + this.account.setKey("otr_p", privateKeySpec.getP().toString(16)); + this.account.setKey("otr_q", privateKeySpec.getQ().toString(16)); + this.account.setKey("otr_y", publicKeySpec.getY().toString(16)); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } catch (InvalidKeySpecException e) { + e.printStackTrace(); + } + + } + + @Override + public void askForSecret(SessionID arg0, InstanceTag arg1, String arg2) { + // TODO Auto-generated method stub + + } + + @Override + public void finishedSessionMessage(SessionID arg0, String arg1) + throws OtrException { + + } + + @Override + public String getFallbackMessage(SessionID arg0) { + return "I would like to start a private (OTR encrypted) conversation but your client doesn’t seem to support that"; + } + + @Override + public byte[] getLocalFingerprintRaw(SessionID arg0) { + // TODO Auto-generated method stub + return null; + } + + public PublicKey getPublicKey() { + if (this.keyPair == null) { + return null; + } + return this.keyPair.getPublic(); + } + + @Override + public KeyPair getLocalKeyPair(SessionID arg0) throws OtrException { + if (this.keyPair == null) { + KeyPairGenerator kg; + try { + kg = KeyPairGenerator.getInstance("DSA"); + this.keyPair = kg.genKeyPair(); + this.saveKey(); + mXmppConnectionService.databaseBackend.updateAccount(account); + } catch (NoSuchAlgorithmException e) { + Log.d(Config.LOGTAG, + "error generating key pair " + e.getMessage()); + } + } + return this.keyPair; + } + + @Override + public String getReplyForUnreadableMessage(SessionID arg0) { + // TODO Auto-generated method stub + return null; + } + + @Override + public OtrPolicy getSessionPolicy(SessionID arg0) { + return otrPolicy; + } + + @Override + public void injectMessage(SessionID session, String body) + throws OtrException { + MessagePacket packet = new MessagePacket(); + packet.setFrom(account.getFullJid()); + if (session.getUserID().isEmpty()) { + packet.setTo(session.getAccountID()); + } else { + packet.setTo(session.getAccountID() + "/" + session.getUserID()); + } + packet.setBody(body); + packet.addChild("private", "urn:xmpp:carbons:2"); + packet.addChild("no-copy", "urn:xmpp:hints"); + packet.setType(MessagePacket.TYPE_CHAT); + account.getXmppConnection().sendMessagePacket(packet); + } + + @Override + public void messageFromAnotherInstanceReceived(SessionID id) { + Log.d(Config.LOGTAG, + "unreadable message received from " + id.getAccountID()); + } + + @Override + public void multipleInstancesDetected(SessionID arg0) { + // TODO Auto-generated method stub + + } + + @Override + public void requireEncryptedMessage(SessionID arg0, String arg1) + throws OtrException { + // TODO Auto-generated method stub + + } + + @Override + public void showError(SessionID arg0, String arg1) throws OtrException { + // TODO Auto-generated method stub + + } + + @Override + public void smpAborted(SessionID arg0) throws OtrException { + // TODO Auto-generated method stub + + } + + @Override + public void smpError(SessionID arg0, int arg1, boolean arg2) + throws OtrException { + throw new OtrException(new Exception("smp error")); + } + + @Override + public void unencryptedMessageReceived(SessionID arg0, String arg1) + throws OtrException { + throw new OtrException(new Exception("unencrypted message received")); + } + + @Override + public void unreadableMessageReceived(SessionID arg0) throws OtrException { + throw new OtrException(new Exception("unreadable message received")); + } + + @Override + public void unverify(SessionID arg0, String arg1) { + // TODO Auto-generated method stub + + } + + @Override + public void verify(SessionID arg0, String arg1, boolean arg2) { + // TODO Auto-generated method stub + + } + +} diff --git a/conversations/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java b/conversations/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java new file mode 100644 index 00000000..2696c7d2 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java @@ -0,0 +1,385 @@ +package eu.siacs.conversations.crypto; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.openintents.openpgp.OpenPgpError; +import org.openintents.openpgp.OpenPgpSignatureResult; +import org.openintents.openpgp.util.OpenPgpApi; +import org.openintents.openpgp.util.OpenPgpApi.IOpenPgpCallback; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.DownloadableFile; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.ui.UiCallback; +import android.app.PendingIntent; +import android.content.Intent; +import android.graphics.BitmapFactory; +import android.util.Log; + +public class PgpEngine { + private OpenPgpApi api; + private XmppConnectionService mXmppConnectionService; + + public PgpEngine(OpenPgpApi api, XmppConnectionService service) { + this.api = api; + this.mXmppConnectionService = service; + } + + public void decrypt(final Message message, + final UiCallback callback) { + Log.d(Config.LOGTAG, "decrypting message " + message.getUuid()); + Intent params = new Intent(); + params.setAction(OpenPgpApi.ACTION_DECRYPT_VERIFY); + params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, message + .getConversation().getAccount().getJid()); + if (message.getType() == Message.TYPE_TEXT) { + InputStream is = new ByteArrayInputStream(message.getBody() + .getBytes()); + final OutputStream os = new ByteArrayOutputStream(); + api.executeApiAsync(params, is, os, new IOpenPgpCallback() { + + @Override + public void onReturn(Intent result) { + switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, + OpenPgpApi.RESULT_CODE_ERROR)) { + case OpenPgpApi.RESULT_CODE_SUCCESS: + try { + os.flush(); + if (message.getEncryption() == Message.ENCRYPTION_PGP) { + message.setBody(os.toString()); + message.setEncryption(Message.ENCRYPTION_DECRYPTED); + callback.success(message); + } + } catch (IOException e) { + callback.error(R.string.openpgp_error, message); + return; + } + + return; + case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: + callback.userInputRequried((PendingIntent) result + .getParcelableExtra(OpenPgpApi.RESULT_INTENT), + message); + return; + case OpenPgpApi.RESULT_CODE_ERROR: + OpenPgpError error = result + .getParcelableExtra(OpenPgpApi.RESULT_ERROR); + Log.d(Config.LOGTAG, + "openpgp error: " + error.getMessage()); + callback.error(R.string.openpgp_error, message); + return; + default: + return; + } + } + }); + } else if (message.getType() == Message.TYPE_IMAGE) { + try { + final DownloadableFile inputFile = this.mXmppConnectionService + .getFileBackend().getFile(message, false); + final DownloadableFile outputFile = this.mXmppConnectionService + .getFileBackend().getFile(message, true); + outputFile.createNewFile(); + InputStream is = new FileInputStream(inputFile); + OutputStream os = new FileOutputStream(outputFile); + api.executeApiAsync(params, is, os, new IOpenPgpCallback() { + + @Override + public void onReturn(Intent result) { + switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, + OpenPgpApi.RESULT_CODE_ERROR)) { + case OpenPgpApi.RESULT_CODE_SUCCESS: + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFile( + outputFile.getAbsolutePath(), options); + int imageHeight = options.outHeight; + int imageWidth = options.outWidth; + message.setBody(Long.toString(outputFile.getSize()) + + ',' + imageWidth + ',' + imageHeight); + message.setEncryption(Message.ENCRYPTION_DECRYPTED); + PgpEngine.this.mXmppConnectionService + .updateMessage(message); + ; + callback.success(message); + return; + case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: + callback.userInputRequried( + (PendingIntent) result + .getParcelableExtra(OpenPgpApi.RESULT_INTENT), + message); + return; + case OpenPgpApi.RESULT_CODE_ERROR: + callback.error(R.string.openpgp_error, message); + return; + default: + return; + } + } + }); + } catch (FileNotFoundException e) { + callback.error(R.string.error_decrypting_file, message); + } catch (IOException e) { + callback.error(R.string.error_decrypting_file, message); + } + + } + } + + public void encrypt(final Message message, + final UiCallback callback) { + + Intent params = new Intent(); + params.setAction(OpenPgpApi.ACTION_ENCRYPT); + if (message.getConversation().getMode() == Conversation.MODE_SINGLE) { + long[] keys = { message.getConversation().getContact() + .getPgpKeyId() }; + params.putExtra(OpenPgpApi.EXTRA_KEY_IDS, keys); + } else { + params.putExtra(OpenPgpApi.EXTRA_KEY_IDS, message.getConversation() + .getMucOptions().getPgpKeyIds()); + } + params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, message + .getConversation().getAccount().getJid()); + + if (message.getType() == Message.TYPE_TEXT) { + params.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true); + + InputStream is = new ByteArrayInputStream(message.getBody() + .getBytes()); + final OutputStream os = new ByteArrayOutputStream(); + api.executeApiAsync(params, is, os, new IOpenPgpCallback() { + + @Override + public void onReturn(Intent result) { + switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, + OpenPgpApi.RESULT_CODE_ERROR)) { + case OpenPgpApi.RESULT_CODE_SUCCESS: + try { + os.flush(); + StringBuilder encryptedMessageBody = new StringBuilder(); + String[] lines = os.toString().split("\n"); + for (int i = 2; i < lines.length - 1; ++i) { + if (!lines[i].contains("Version")) { + encryptedMessageBody.append(lines[i].trim()); + } + } + message.setEncryptedBody(encryptedMessageBody + .toString()); + callback.success(message); + } catch (IOException e) { + callback.error(R.string.openpgp_error, message); + } + + break; + case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: + callback.userInputRequried((PendingIntent) result + .getParcelableExtra(OpenPgpApi.RESULT_INTENT), + message); + break; + case OpenPgpApi.RESULT_CODE_ERROR: + callback.error(R.string.openpgp_error, message); + break; + } + } + }); + } else if (message.getType() == Message.TYPE_IMAGE) { + try { + DownloadableFile inputFile = this.mXmppConnectionService + .getFileBackend().getFile(message, true); + DownloadableFile outputFile = this.mXmppConnectionService + .getFileBackend().getFile(message, false); + outputFile.createNewFile(); + InputStream is = new FileInputStream(inputFile); + OutputStream os = new FileOutputStream(outputFile); + api.executeApiAsync(params, is, os, new IOpenPgpCallback() { + + @Override + public void onReturn(Intent result) { + switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, + OpenPgpApi.RESULT_CODE_ERROR)) { + case OpenPgpApi.RESULT_CODE_SUCCESS: + callback.success(message); + break; + case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: + callback.userInputRequried( + (PendingIntent) result + .getParcelableExtra(OpenPgpApi.RESULT_INTENT), + message); + break; + case OpenPgpApi.RESULT_CODE_ERROR: + callback.error(R.string.openpgp_error, message); + break; + } + } + }); + } catch (FileNotFoundException e) { + Log.d(Config.LOGTAG, "file not found: " + e.getMessage()); + } catch (IOException e) { + Log.d(Config.LOGTAG, "io exception during file encrypt"); + } + } + } + + public long fetchKeyId(Account account, String status, String signature) { + if ((signature == null) || (api == null)) { + return 0; + } + if (status == null) { + status = ""; + } + StringBuilder pgpSig = new StringBuilder(); + pgpSig.append("-----BEGIN PGP SIGNED MESSAGE-----"); + pgpSig.append('\n'); + pgpSig.append('\n'); + pgpSig.append(status); + pgpSig.append('\n'); + pgpSig.append("-----BEGIN PGP SIGNATURE-----"); + pgpSig.append('\n'); + pgpSig.append('\n'); + pgpSig.append(signature.replace("\n", "").trim()); + pgpSig.append('\n'); + pgpSig.append("-----END PGP SIGNATURE-----"); + Intent params = new Intent(); + params.setAction(OpenPgpApi.ACTION_DECRYPT_VERIFY); + params.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true); + params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, account.getJid()); + InputStream is = new ByteArrayInputStream(pgpSig.toString().getBytes()); + ByteArrayOutputStream os = new ByteArrayOutputStream(); + Intent result = api.executeApi(params, is, os); + switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, + OpenPgpApi.RESULT_CODE_ERROR)) { + case OpenPgpApi.RESULT_CODE_SUCCESS: + OpenPgpSignatureResult sigResult = result + .getParcelableExtra(OpenPgpApi.RESULT_SIGNATURE); + if (sigResult != null) { + return sigResult.getKeyId(); + } else { + return 0; + } + case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: + return 0; + case OpenPgpApi.RESULT_CODE_ERROR: + Log.d(Config.LOGTAG, + "openpgp error: " + + ((OpenPgpError) result + .getParcelableExtra(OpenPgpApi.RESULT_ERROR)) + .getMessage()); + return 0; + } + return 0; + } + + public void generateSignature(final Account account, String status, + final UiCallback callback) { + Intent params = new Intent(); + params.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true); + params.setAction(OpenPgpApi.ACTION_SIGN); + params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, account.getJid()); + InputStream is = new ByteArrayInputStream(status.getBytes()); + final OutputStream os = new ByteArrayOutputStream(); + api.executeApiAsync(params, is, os, new IOpenPgpCallback() { + + @Override + public void onReturn(Intent result) { + switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, 0)) { + case OpenPgpApi.RESULT_CODE_SUCCESS: + StringBuilder signatureBuilder = new StringBuilder(); + try { + os.flush(); + String[] lines = os.toString().split("\n"); + boolean sig = false; + for (String line : lines) { + if (sig) { + if (line.contains("END PGP SIGNATURE")) { + sig = false; + } else { + if (!line.contains("Version")) { + signatureBuilder.append(line.trim()); + } + } + } + if (line.contains("BEGIN PGP SIGNATURE")) { + sig = true; + } + } + } catch (IOException e) { + callback.error(R.string.openpgp_error, account); + return; + } + account.setKey("pgp_signature", signatureBuilder.toString()); + callback.success(account); + return; + case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: + callback.userInputRequried((PendingIntent) result + .getParcelableExtra(OpenPgpApi.RESULT_INTENT), + account); + return; + case OpenPgpApi.RESULT_CODE_ERROR: + callback.error(R.string.openpgp_error, account); + return; + } + } + }); + } + + public void hasKey(final Contact contact, final UiCallback callback) { + Intent params = new Intent(); + params.setAction(OpenPgpApi.ACTION_GET_KEY); + params.putExtra(OpenPgpApi.EXTRA_KEY_ID, contact.getPgpKeyId()); + params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, contact.getAccount() + .getJid()); + api.executeApiAsync(params, null, null, new IOpenPgpCallback() { + + @Override + public void onReturn(Intent result) { + switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, 0)) { + case OpenPgpApi.RESULT_CODE_SUCCESS: + callback.success(contact); + return; + case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: + callback.userInputRequried((PendingIntent) result + .getParcelableExtra(OpenPgpApi.RESULT_INTENT), + contact); + return; + case OpenPgpApi.RESULT_CODE_ERROR: + callback.error(R.string.openpgp_error, contact); + return; + } + } + }); + } + + public PendingIntent getIntentForKey(Contact contact) { + Intent params = new Intent(); + params.setAction(OpenPgpApi.ACTION_GET_KEY); + params.putExtra(OpenPgpApi.EXTRA_KEY_ID, contact.getPgpKeyId()); + params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, contact.getAccount() + .getJid()); + Intent result = api.executeApi(params, null, null); + return (PendingIntent) result + .getParcelableExtra(OpenPgpApi.RESULT_INTENT); + } + + public PendingIntent getIntentForKey(Account account, long pgpKeyId) { + Intent params = new Intent(); + params.setAction(OpenPgpApi.ACTION_GET_KEY); + params.putExtra(OpenPgpApi.EXTRA_KEY_ID, pgpKeyId); + params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, account.getJid()); + Intent result = api.executeApi(params, null, null); + return (PendingIntent) result + .getParcelableExtra(OpenPgpApi.RESULT_INTENT); + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/entities/AbstractEntity.java b/conversations/src/main/java/eu/siacs/conversations/entities/AbstractEntity.java new file mode 100644 index 00000000..92b8a729 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/entities/AbstractEntity.java @@ -0,0 +1,21 @@ +package eu.siacs.conversations.entities; + +import android.content.ContentValues; + +public abstract class AbstractEntity { + + public static final String UUID = "uuid"; + + protected String uuid; + + public String getUuid() { + return this.uuid; + } + + public abstract ContentValues getContentValues(); + + public boolean equals(AbstractEntity entity) { + return this.getUuid().equals(entity.getUuid()); + } + +} diff --git a/conversations/src/main/java/eu/siacs/conversations/entities/Account.java b/conversations/src/main/java/eu/siacs/conversations/entities/Account.java new file mode 100644 index 00000000..80a9d62f --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/entities/Account.java @@ -0,0 +1,399 @@ +package eu.siacs.conversations.entities; + +import java.security.interfaces.DSAPublicKey; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.CopyOnWriteArrayList; + +import net.java.otr4j.crypto.OtrCryptoEngineImpl; +import net.java.otr4j.crypto.OtrCryptoException; + +import org.json.JSONException; +import org.json.JSONObject; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.crypto.OtrEngine; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xmpp.XmppConnection; +import android.content.ContentValues; +import android.database.Cursor; +import android.os.SystemClock; + +public class Account extends AbstractEntity { + + public static final String TABLENAME = "accounts"; + + public static final String USERNAME = "username"; + public static final String SERVER = "server"; + public static final String PASSWORD = "password"; + public static final String OPTIONS = "options"; + public static final String ROSTERVERSION = "rosterversion"; + public static final String KEYS = "keys"; + public static final String AVATAR = "avatar"; + + public static final int OPTION_USETLS = 0; + public static final int OPTION_DISABLED = 1; + public static final int OPTION_REGISTER = 2; + public static final int OPTION_USECOMPRESSION = 3; + + public static final int STATUS_CONNECTING = 0; + public static final int STATUS_DISABLED = -2; + public static final int STATUS_OFFLINE = -1; + public static final int STATUS_ONLINE = 1; + public static final int STATUS_NO_INTERNET = 2; + public static final int STATUS_UNAUTHORIZED = 3; + public static final int STATUS_SERVER_NOT_FOUND = 5; + + public static final int STATUS_REGISTRATION_FAILED = 7; + public static final int STATUS_REGISTRATION_CONFLICT = 8; + public static final int STATUS_REGISTRATION_SUCCESSFULL = 9; + public static final int STATUS_REGISTRATION_NOT_SUPPORTED = 10; + + protected String username; + protected String server; + protected String password; + protected int options = 0; + protected String rosterVersion; + protected String resource = "mobile"; + protected int status = -1; + protected JSONObject keys = new JSONObject(); + protected String avatar; + + protected boolean online = false; + + private OtrEngine otrEngine = null; + private XmppConnection xmppConnection = null; + private Presences presences = new Presences(); + private long mEndGracePeriod = 0L; + private String otrFingerprint; + private Roster roster = null; + + private List bookmarks = new CopyOnWriteArrayList(); + public List pendingConferenceJoins = new CopyOnWriteArrayList(); + public List pendingConferenceLeaves = new CopyOnWriteArrayList(); + + public Account() { + this.uuid = "0"; + } + + public Account(String username, String server, String password) { + this(java.util.UUID.randomUUID().toString(), username, server, + password, 0, null, "", null); + } + + public Account(String uuid, String username, String server, + String password, int options, String rosterVersion, String keys, + String avatar) { + this.uuid = uuid; + this.username = username; + this.server = server; + this.password = password; + this.options = options; + this.rosterVersion = rosterVersion; + try { + this.keys = new JSONObject(keys); + } catch (JSONException e) { + + } + this.avatar = avatar; + } + + public boolean isOptionSet(int option) { + return ((options & (1 << option)) != 0); + } + + public void setOption(int option, boolean value) { + if (value) { + this.options |= 1 << option; + } else { + this.options &= ~(1 << option); + } + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getServer() { + return server; + } + + public void setServer(String server) { + this.server = server; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public void setStatus(int status) { + this.status = status; + } + + public int getStatus() { + if (isOptionSet(OPTION_DISABLED)) { + return STATUS_DISABLED; + } else { + return this.status; + } + } + + public boolean errorStatus() { + int s = getStatus(); + return (s == STATUS_REGISTRATION_FAILED + || s == STATUS_REGISTRATION_CONFLICT + || s == STATUS_REGISTRATION_NOT_SUPPORTED + || s == STATUS_SERVER_NOT_FOUND || s == STATUS_UNAUTHORIZED); + } + + public boolean hasErrorStatus() { + if (getXmppConnection() == null) { + return false; + } else { + return getStatus() > STATUS_NO_INTERNET + && (getXmppConnection().getAttempt() >= 2); + } + } + + public void setResource(String resource) { + this.resource = resource; + } + + public String getResource() { + return this.resource; + } + + public String getJid() { + return username.toLowerCase(Locale.getDefault()) + "@" + + server.toLowerCase(Locale.getDefault()); + } + + public JSONObject getKeys() { + return keys; + } + + public String getSSLFingerprint() { + if (keys.has("ssl_cert")) { + try { + return keys.getString("ssl_cert"); + } catch (JSONException e) { + return null; + } + } else { + return null; + } + } + + public void setSSLCertFingerprint(String fingerprint) { + this.setKey("ssl_cert", fingerprint); + } + + public boolean setKey(String keyName, String keyValue) { + try { + this.keys.put(keyName, keyValue); + return true; + } catch (JSONException e) { + return false; + } + } + + @Override + public ContentValues getContentValues() { + ContentValues values = new ContentValues(); + values.put(UUID, uuid); + values.put(USERNAME, username); + values.put(SERVER, server); + values.put(PASSWORD, password); + values.put(OPTIONS, options); + values.put(KEYS, this.keys.toString()); + values.put(ROSTERVERSION, rosterVersion); + values.put(AVATAR, avatar); + return values; + } + + public static Account fromCursor(Cursor cursor) { + return new Account(cursor.getString(cursor.getColumnIndex(UUID)), + cursor.getString(cursor.getColumnIndex(USERNAME)), + cursor.getString(cursor.getColumnIndex(SERVER)), + cursor.getString(cursor.getColumnIndex(PASSWORD)), + cursor.getInt(cursor.getColumnIndex(OPTIONS)), + cursor.getString(cursor.getColumnIndex(ROSTERVERSION)), + cursor.getString(cursor.getColumnIndex(KEYS)), + cursor.getString(cursor.getColumnIndex(AVATAR))); + } + + public OtrEngine getOtrEngine(XmppConnectionService context) { + if (otrEngine == null) { + otrEngine = new OtrEngine(context, this); + } + return this.otrEngine; + } + + public XmppConnection getXmppConnection() { + return this.xmppConnection; + } + + public void setXmppConnection(XmppConnection connection) { + this.xmppConnection = connection; + } + + public String getFullJid() { + return this.getJid() + "/" + this.resource; + } + + public String getOtrFingerprint() { + if (this.otrFingerprint == null) { + try { + DSAPublicKey pubkey = (DSAPublicKey) this.otrEngine + .getPublicKey(); + if (pubkey == null) { + return null; + } + StringBuilder builder = new StringBuilder( + new OtrCryptoEngineImpl().getFingerprint(pubkey)); + builder.insert(8, " "); + builder.insert(17, " "); + builder.insert(26, " "); + builder.insert(35, " "); + this.otrFingerprint = builder.toString(); + } catch (OtrCryptoException e) { + + } + } + return this.otrFingerprint; + } + + public String getRosterVersion() { + if (this.rosterVersion == null) { + return ""; + } else { + return this.rosterVersion; + } + } + + public void setRosterVersion(String version) { + this.rosterVersion = version; + } + + public String getOtrFingerprint(XmppConnectionService service) { + this.getOtrEngine(service); + return this.getOtrFingerprint(); + } + + public void updatePresence(String resource, int status) { + this.presences.updatePresence(resource, status); + } + + public void removePresence(String resource) { + this.presences.removePresence(resource); + } + + public void clearPresences() { + this.presences = new Presences(); + } + + public int countPresences() { + return this.presences.size(); + } + + public String getPgpSignature() { + if (keys.has("pgp_signature")) { + try { + return keys.getString("pgp_signature"); + } catch (JSONException e) { + return null; + } + } else { + return null; + } + } + + public Roster getRoster() { + if (this.roster == null) { + this.roster = new Roster(this); + } + return this.roster; + } + + public void setBookmarks(List bookmarks) { + this.bookmarks = bookmarks; + } + + public List getBookmarks() { + return this.bookmarks; + } + + public boolean hasBookmarkFor(String conferenceJid) { + for (Bookmark bmark : this.bookmarks) { + if (bmark.getJid().equals(conferenceJid)) { + return true; + } + } + return false; + } + + public boolean setAvatar(String filename) { + if (this.avatar != null && this.avatar.equals(filename)) { + return false; + } else { + this.avatar = filename; + return true; + } + } + + public String getAvatar() { + return this.avatar; + } + + public int getReadableStatusId() { + switch (getStatus()) { + + case Account.STATUS_DISABLED: + return R.string.account_status_disabled; + case Account.STATUS_ONLINE: + return R.string.account_status_online; + case Account.STATUS_CONNECTING: + return R.string.account_status_connecting; + case Account.STATUS_OFFLINE: + return R.string.account_status_offline; + case Account.STATUS_UNAUTHORIZED: + return R.string.account_status_unauthorized; + case Account.STATUS_SERVER_NOT_FOUND: + return R.string.account_status_not_found; + case Account.STATUS_NO_INTERNET: + return R.string.account_status_no_internet; + case Account.STATUS_REGISTRATION_FAILED: + return R.string.account_status_regis_fail; + case Account.STATUS_REGISTRATION_CONFLICT: + return R.string.account_status_regis_conflict; + case Account.STATUS_REGISTRATION_SUCCESSFULL: + return R.string.account_status_regis_success; + case Account.STATUS_REGISTRATION_NOT_SUPPORTED: + return R.string.account_status_regis_not_sup; + default: + return R.string.account_status_unknown; + } + } + + public void activateGracePeriod() { + this.mEndGracePeriod = SystemClock.elapsedRealtime() + + (Config.CARBON_GRACE_PERIOD * 1000); + } + + public void deactivateGracePeriod() { + this.mEndGracePeriod = 0L; + } + + public boolean inGracePeriod() { + return SystemClock.elapsedRealtime() < this.mEndGracePeriod; + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/entities/Bookmark.java b/conversations/src/main/java/eu/siacs/conversations/entities/Bookmark.java new file mode 100644 index 00000000..dd9e805c --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/entities/Bookmark.java @@ -0,0 +1,137 @@ +package eu.siacs.conversations.entities; + +import java.util.Locale; + +import eu.siacs.conversations.xml.Element; + +public class Bookmark extends Element implements ListItem { + + private Account account; + private Conversation mJoinedConversation; + + public Bookmark(Account account, String jid) { + super("conference"); + this.setAttribute("jid", jid); + this.account = account; + } + + private Bookmark(Account account) { + super("conference"); + this.account = account; + } + + public static Bookmark parse(Element element, Account account) { + Bookmark bookmark = new Bookmark(account); + bookmark.setAttributes(element.getAttributes()); + bookmark.setChildren(element.getChildren()); + return bookmark; + } + + public void setAutojoin(boolean autojoin) { + if (autojoin) { + this.setAttribute("autojoin", "true"); + } else { + this.setAttribute("autojoin", "false"); + } + } + + public void setName(String name) { + this.name = name; + } + + public void setNick(String nick) { + Element element = this.findChild("nick"); + if (element == null) { + element = this.addChild("nick"); + } + element.setContent(nick); + } + + public void setPassword(String password) { + Element element = this.findChild("password"); + if (element != null) { + element.setContent(password); + } + } + + @Override + public int compareTo(ListItem another) { + return this.getDisplayName().compareToIgnoreCase( + another.getDisplayName()); + } + + @Override + public String getDisplayName() { + if (this.mJoinedConversation != null + && (this.mJoinedConversation.getMucOptions().getSubject() != null)) { + return this.mJoinedConversation.getMucOptions().getSubject(); + } else if (getName() != null) { + return getName(); + } else { + return this.getJid().split("@")[0]; + } + } + + @Override + public String getJid() { + String jid = this.getAttribute("jid"); + if (jid != null) { + return jid.toLowerCase(Locale.US); + } else { + return null; + } + } + + public String getNick() { + Element nick = this.findChild("nick"); + if (nick != null) { + return nick.getContent(); + } else { + return null; + } + } + + public boolean autojoin() { + String autojoin = this.getAttribute("autojoin"); + return (autojoin != null && (autojoin.equalsIgnoreCase("true") || autojoin + .equalsIgnoreCase("1"))); + } + + public String getPassword() { + Element password = this.findChild("password"); + if (password != null) { + return password.getContent(); + } else { + return null; + } + } + + public boolean match(String needle) { + return needle == null + || getJid().contains(needle.toLowerCase(Locale.US)) + || getDisplayName().toLowerCase(Locale.US).contains( + needle.toLowerCase(Locale.US)); + } + + public Account getAccount() { + return this.account; + } + + public void setConversation(Conversation conversation) { + this.mJoinedConversation = conversation; + } + + public Conversation getConversation() { + return this.mJoinedConversation; + } + + public String getName() { + return this.getAttribute("name"); + } + + public void unregisterConversation() { + if (this.mJoinedConversation != null) { + this.mJoinedConversation.deregisterWithBookmark(); + } + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/entities/Contact.java b/conversations/src/main/java/eu/siacs/conversations/entities/Contact.java new file mode 100644 index 00000000..60c31a42 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/entities/Contact.java @@ -0,0 +1,367 @@ +package eu.siacs.conversations.entities; + +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import eu.siacs.conversations.xml.Element; +import android.content.ContentValues; +import android.database.Cursor; + +public class Contact implements ListItem { + public static final String TABLENAME = "contacts"; + + public static final String SYSTEMNAME = "systemname"; + public static final String SERVERNAME = "servername"; + public static final String JID = "jid"; + public static final String OPTIONS = "options"; + public static final String SYSTEMACCOUNT = "systemaccount"; + public static final String PHOTOURI = "photouri"; + public static final String KEYS = "pgpkey"; + public static final String ACCOUNT = "accountUuid"; + public static final String AVATAR = "avatar"; + + protected String accountUuid; + protected String systemName; + protected String serverName; + protected String presenceName; + protected String jid; + protected int subscription = 0; + protected String systemAccount; + protected String photoUri; + protected String avatar; + protected JSONObject keys = new JSONObject(); + protected Presences presences = new Presences(); + + protected Account account; + + protected boolean inRoster = true; + + public Lastseen lastseen = new Lastseen(); + + public Contact(String account, String systemName, String serverName, + String jid, int subscription, String photoUri, + String systemAccount, String keys, String avatar) { + this.accountUuid = account; + this.systemName = systemName; + this.serverName = serverName; + this.jid = jid; + this.subscription = subscription; + this.photoUri = photoUri; + this.systemAccount = systemAccount; + if (keys == null) { + keys = ""; + } + try { + this.keys = new JSONObject(keys); + } catch (JSONException e) { + this.keys = new JSONObject(); + } + this.avatar = avatar; + } + + public Contact(String jid) { + this.jid = jid; + } + + public String getDisplayName() { + if (this.systemName != null) { + return this.systemName; + } else if (this.serverName != null) { + return this.serverName; + } else if (this.presenceName != null) { + return this.presenceName; + } else { + return this.jid.split("@")[0]; + } + } + + public String getProfilePhoto() { + return this.photoUri; + } + + public String getJid() { + return this.jid.toLowerCase(Locale.getDefault()); + } + + public boolean match(String needle) { + return needle == null + || jid.contains(needle.toLowerCase()) + || getDisplayName().toLowerCase() + .contains(needle.toLowerCase()); + } + + public ContentValues getContentValues() { + ContentValues values = new ContentValues(); + values.put(ACCOUNT, accountUuid); + values.put(SYSTEMNAME, systemName); + values.put(SERVERNAME, serverName); + values.put(JID, jid); + values.put(OPTIONS, subscription); + values.put(SYSTEMACCOUNT, systemAccount); + values.put(PHOTOURI, photoUri); + values.put(KEYS, keys.toString()); + values.put(AVATAR, avatar); + return values; + } + + public static Contact fromCursor(Cursor cursor) { + return new Contact(cursor.getString(cursor.getColumnIndex(ACCOUNT)), + cursor.getString(cursor.getColumnIndex(SYSTEMNAME)), + cursor.getString(cursor.getColumnIndex(SERVERNAME)), + cursor.getString(cursor.getColumnIndex(JID)), + cursor.getInt(cursor.getColumnIndex(OPTIONS)), + cursor.getString(cursor.getColumnIndex(PHOTOURI)), + cursor.getString(cursor.getColumnIndex(SYSTEMACCOUNT)), + cursor.getString(cursor.getColumnIndex(KEYS)), + cursor.getString(cursor.getColumnIndex(AVATAR))); + } + + public int getSubscription() { + return this.subscription; + } + + public void setSystemAccount(String account) { + this.systemAccount = account; + } + + public void setAccount(Account account) { + this.account = account; + this.accountUuid = account.getUuid(); + } + + public Account getAccount() { + return this.account; + } + + public Presences getPresences() { + return this.presences; + } + + public void updatePresence(String resource, int status) { + this.presences.updatePresence(resource, status); + } + + public void removePresence(String resource) { + this.presences.removePresence(resource); + } + + public void clearPresences() { + this.presences.clearPresences(); + this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST); + } + + public int getMostAvailableStatus() { + return this.presences.getMostAvailableStatus(); + } + + public void setPresences(Presences pres) { + this.presences = pres; + } + + public void setPhotoUri(String uri) { + this.photoUri = uri; + } + + public void setServerName(String serverName) { + this.serverName = serverName; + } + + public void setSystemName(String systemName) { + this.systemName = systemName; + } + + public void setPresenceName(String presenceName) { + this.presenceName = presenceName; + } + + public String getSystemAccount() { + return systemAccount; + } + + public Set getOtrFingerprints() { + Set set = new HashSet(); + try { + if (this.keys.has("otr_fingerprints")) { + JSONArray fingerprints = this.keys + .getJSONArray("otr_fingerprints"); + for (int i = 0; i < fingerprints.length(); ++i) { + set.add(fingerprints.getString(i)); + } + } + } catch (JSONException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return set; + } + + public void addOtrFingerprint(String print) { + try { + JSONArray fingerprints; + if (!this.keys.has("otr_fingerprints")) { + fingerprints = new JSONArray(); + + } else { + fingerprints = this.keys.getJSONArray("otr_fingerprints"); + } + fingerprints.put(print); + this.keys.put("otr_fingerprints", fingerprints); + } catch (JSONException e) { + + } + } + + public void setPgpKeyId(long keyId) { + try { + this.keys.put("pgp_keyid", keyId); + } catch (JSONException e) { + + } + } + + public long getPgpKeyId() { + if (this.keys.has("pgp_keyid")) { + try { + return this.keys.getLong("pgp_keyid"); + } catch (JSONException e) { + return 0; + } + } else { + return 0; + } + } + + public void setOption(int option) { + this.subscription |= 1 << option; + } + + public void resetOption(int option) { + this.subscription &= ~(1 << option); + } + + public boolean getOption(int option) { + return ((this.subscription & (1 << option)) != 0); + } + + public boolean showInRoster() { + return (this.getOption(Contact.Options.IN_ROSTER) && (!this + .getOption(Contact.Options.DIRTY_DELETE))) + || (this.getOption(Contact.Options.DIRTY_PUSH)); + } + + public void parseSubscriptionFromElement(Element item) { + String ask = item.getAttribute("ask"); + String subscription = item.getAttribute("subscription"); + + if (subscription != null) { + if (subscription.equals("to")) { + this.resetOption(Contact.Options.FROM); + this.setOption(Contact.Options.TO); + } else if (subscription.equals("from")) { + this.resetOption(Contact.Options.TO); + this.setOption(Contact.Options.FROM); + this.resetOption(Contact.Options.PREEMPTIVE_GRANT); + } else if (subscription.equals("both")) { + this.setOption(Contact.Options.TO); + this.setOption(Contact.Options.FROM); + this.resetOption(Contact.Options.PREEMPTIVE_GRANT); + } else if (subscription.equals("none")) { + this.resetOption(Contact.Options.FROM); + this.resetOption(Contact.Options.TO); + } + } + + // do NOT override asking if pending push request + if (!this.getOption(Contact.Options.DIRTY_PUSH)) { + if ((ask != null) && (ask.equals("subscribe"))) { + this.setOption(Contact.Options.ASKING); + } else { + this.resetOption(Contact.Options.ASKING); + } + } + } + + public Element asElement() { + Element item = new Element("item"); + item.setAttribute("jid", this.jid); + if (this.serverName != null) { + item.setAttribute("name", this.serverName); + } + return item; + } + + public class Options { + public static final int TO = 0; + public static final int FROM = 1; + public static final int ASKING = 2; + public static final int PREEMPTIVE_GRANT = 3; + public static final int IN_ROSTER = 4; + public static final int PENDING_SUBSCRIPTION_REQUEST = 5; + public static final int DIRTY_PUSH = 6; + public static final int DIRTY_DELETE = 7; + } + + public class Lastseen { + public long time = 0; + public String presence = null; + } + + @Override + public int compareTo(ListItem another) { + return this.getDisplayName().compareToIgnoreCase( + another.getDisplayName()); + } + + public String getServer() { + String[] split = getJid().split("@"); + if (split.length >= 2) { + return split[1]; + } else { + return null; + } + } + + public boolean setAvatar(String filename) { + if (this.avatar != null && this.avatar.equals(filename)) { + return false; + } else { + this.avatar = filename; + return true; + } + } + + public String getAvatar() { + return this.avatar; + } + + public boolean deleteOtrFingerprint(String fingerprint) { + boolean success = false; + try { + if (this.keys.has("otr_fingerprints")) { + JSONArray newPrints = new JSONArray(); + JSONArray oldPrints = this.keys + .getJSONArray("otr_fingerprints"); + for (int i = 0; i < oldPrints.length(); ++i) { + if (!oldPrints.getString(i).equals(fingerprint)) { + newPrints.put(oldPrints.getString(i)); + } else { + success = true; + } + } + this.keys.put("otr_fingerprints", newPrints); + } + return success; + } catch (JSONException e) { + return false; + } + } + + public boolean trusted() { + return getOption(Options.FROM) && getOption(Options.TO); + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/entities/Conversation.java b/conversations/src/main/java/eu/siacs/conversations/entities/Conversation.java new file mode 100644 index 00000000..9d4c36db --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -0,0 +1,500 @@ +package eu.siacs.conversations.entities; + +import java.security.interfaces.DSAPublicKey; +import java.util.ArrayList; +import java.util.List; + +import org.json.JSONException; +import org.json.JSONObject; + +import eu.siacs.conversations.services.XmppConnectionService; + +import net.java.otr4j.OtrException; +import net.java.otr4j.crypto.OtrCryptoEngineImpl; +import net.java.otr4j.crypto.OtrCryptoException; +import net.java.otr4j.session.SessionID; +import net.java.otr4j.session.SessionImpl; +import net.java.otr4j.session.SessionStatus; +import android.content.ContentValues; +import android.database.Cursor; +import android.os.SystemClock; + +public class Conversation extends AbstractEntity { + public static final String TABLENAME = "conversations"; + + public static final int STATUS_AVAILABLE = 0; + public static final int STATUS_ARCHIVED = 1; + public static final int STATUS_DELETED = 2; + + public static final int MODE_MULTI = 1; + public static final int MODE_SINGLE = 0; + + public static final String NAME = "name"; + public static final String ACCOUNT = "accountUuid"; + public static final String CONTACT = "contactUuid"; + public static final String CONTACTJID = "contactJid"; + public static final String STATUS = "status"; + public static final String CREATED = "created"; + public static final String MODE = "mode"; + public static final String ATTRIBUTES = "attributes"; + + public static final String ATTRIBUTE_NEXT_ENCRYPTION = "next_encryption"; + public static final String ATTRIBUTE_MUC_PASSWORD = "muc_password"; + public static final String ATTRIBUTE_MUTED_TILL = "muted_till"; + + private String name; + private String contactUuid; + private String accountUuid; + private String contactJid; + private int status; + private long created; + private int mode; + + private JSONObject attributes = new JSONObject(); + + private String nextPresence; + + protected ArrayList messages = new ArrayList(); + protected Account account = null; + + private transient SessionImpl otrSession; + + private transient String otrFingerprint = null; + + private String nextMessage; + + private transient MucOptions mucOptions = null; + + // private transient String latestMarkableMessageId; + + private byte[] symmetricKey; + + private Bookmark bookmark; + + public Conversation(String name, Account account, String contactJid, + int mode) { + this(java.util.UUID.randomUUID().toString(), name, null, account + .getUuid(), contactJid, System.currentTimeMillis(), + STATUS_AVAILABLE, mode, ""); + this.account = account; + } + + public Conversation(String uuid, String name, String contactUuid, + String accountUuid, String contactJid, long created, int status, + int mode, String attributes) { + this.uuid = uuid; + this.name = name; + this.contactUuid = contactUuid; + this.accountUuid = accountUuid; + this.contactJid = contactJid; + this.created = created; + this.status = status; + this.mode = mode; + try { + if (attributes == null) { + attributes = new String(); + } + this.attributes = new JSONObject(attributes); + } catch (JSONException e) { + this.attributes = new JSONObject(); + } + } + + public List getMessages() { + return messages; + } + + public boolean isRead() { + if ((this.messages == null) || (this.messages.size() == 0)) + return true; + return this.messages.get(this.messages.size() - 1).isRead(); + } + + public void markRead() { + if (this.messages == null) { + return; + } + for (int i = this.messages.size() - 1; i >= 0; --i) { + if (messages.get(i).isRead()) { + break; + } + this.messages.get(i).markRead(); + } + } + + public String getLatestMarkableMessageId() { + if (this.messages == null) { + return null; + } + for (int i = this.messages.size() - 1; i >= 0; --i) { + if (this.messages.get(i).getStatus() <= Message.STATUS_RECEIVED + && this.messages.get(i).markable) { + if (this.messages.get(i).isRead()) { + return null; + } else { + return this.messages.get(i).getRemoteMsgId(); + } + } + } + return null; + } + + public Message getLatestMessage() { + if ((this.messages == null) || (this.messages.size() == 0)) { + Message message = new Message(this, "", Message.ENCRYPTION_NONE); + message.setTime(getCreated()); + return message; + } else { + Message message = this.messages.get(this.messages.size() - 1); + message.setConversation(this); + return message; + } + } + + public void setMessages(ArrayList msgs) { + this.messages = msgs; + } + + public String getName() { + if (getMode() == MODE_MULTI && getMucOptions().getSubject() != null) { + return getMucOptions().getSubject(); + } else if (getMode() == MODE_MULTI && bookmark != null + && bookmark.getName() != null) { + return bookmark.getName(); + } else { + return this.getContact().getDisplayName(); + } + } + + public String getProfilePhotoString() { + return this.getContact().getProfilePhoto(); + } + + public String getAccountUuid() { + return this.accountUuid; + } + + public Account getAccount() { + return this.account; + } + + public Contact getContact() { + return this.account.getRoster().getContact(this.contactJid); + } + + public void setAccount(Account account) { + this.account = account; + } + + public String getContactJid() { + return this.contactJid; + } + + public int getStatus() { + return this.status; + } + + public long getCreated() { + return this.created; + } + + public ContentValues getContentValues() { + ContentValues values = new ContentValues(); + values.put(UUID, uuid); + values.put(NAME, name); + values.put(CONTACT, contactUuid); + values.put(ACCOUNT, accountUuid); + values.put(CONTACTJID, contactJid); + values.put(CREATED, created); + values.put(STATUS, status); + values.put(MODE, mode); + values.put(ATTRIBUTES, attributes.toString()); + return values; + } + + public static Conversation fromCursor(Cursor cursor) { + return new Conversation(cursor.getString(cursor.getColumnIndex(UUID)), + cursor.getString(cursor.getColumnIndex(NAME)), + cursor.getString(cursor.getColumnIndex(CONTACT)), + cursor.getString(cursor.getColumnIndex(ACCOUNT)), + cursor.getString(cursor.getColumnIndex(CONTACTJID)), + cursor.getLong(cursor.getColumnIndex(CREATED)), + cursor.getInt(cursor.getColumnIndex(STATUS)), + cursor.getInt(cursor.getColumnIndex(MODE)), + cursor.getString(cursor.getColumnIndex(ATTRIBUTES))); + } + + public void setStatus(int status) { + this.status = status; + } + + public int getMode() { + return this.mode; + } + + public void setMode(int mode) { + this.mode = mode; + } + + public SessionImpl startOtrSession(XmppConnectionService service, + String presence, boolean sendStart) { + if (this.otrSession != null) { + return this.otrSession; + } else { + SessionID sessionId = new SessionID(this.getContactJid().split("/", + 2)[0], presence, "xmpp"); + this.otrSession = new SessionImpl(sessionId, getAccount() + .getOtrEngine(service)); + try { + if (sendStart) { + this.otrSession.startSession(); + return this.otrSession; + } + return this.otrSession; + } catch (OtrException e) { + return null; + } + } + + } + + public SessionImpl getOtrSession() { + return this.otrSession; + } + + public void resetOtrSession() { + this.otrFingerprint = null; + this.otrSession = null; + } + + public void startOtrIfNeeded() { + if (this.otrSession != null + && this.otrSession.getSessionStatus() != SessionStatus.ENCRYPTED) { + try { + this.otrSession.startSession(); + } catch (OtrException e) { + this.resetOtrSession(); + } + } + } + + public boolean endOtrIfNeeded() { + if (this.otrSession != null) { + if (this.otrSession.getSessionStatus() == SessionStatus.ENCRYPTED) { + try { + this.otrSession.endSession(); + this.resetOtrSession(); + return true; + } catch (OtrException e) { + this.resetOtrSession(); + return false; + } + } else { + this.resetOtrSession(); + return false; + } + } else { + return false; + } + } + + public boolean hasValidOtrSession() { + return this.otrSession != null; + } + + public String getOtrFingerprint() { + if (this.otrFingerprint == null) { + try { + if (getOtrSession() == null) { + return ""; + } + DSAPublicKey remotePubKey = (DSAPublicKey) getOtrSession() + .getRemotePublicKey(); + StringBuilder builder = new StringBuilder( + new OtrCryptoEngineImpl().getFingerprint(remotePubKey)); + builder.insert(8, " "); + builder.insert(17, " "); + builder.insert(26, " "); + builder.insert(35, " "); + this.otrFingerprint = builder.toString(); + } catch (OtrCryptoException e) { + + } + } + return this.otrFingerprint; + } + + public synchronized MucOptions getMucOptions() { + if (this.mucOptions == null) { + this.mucOptions = new MucOptions(this); + } + return this.mucOptions; + } + + public void resetMucOptions() { + this.mucOptions = null; + } + + public void setContactJid(String jid) { + this.contactJid = jid; + } + + public void setNextPresence(String presence) { + this.nextPresence = presence; + } + + public String getNextPresence() { + return this.nextPresence; + } + + public int getLatestEncryption() { + int latestEncryption = this.getLatestMessage().getEncryption(); + if ((latestEncryption == Message.ENCRYPTION_DECRYPTED) + || (latestEncryption == Message.ENCRYPTION_DECRYPTION_FAILED)) { + return Message.ENCRYPTION_PGP; + } else { + return latestEncryption; + } + } + + public int getNextEncryption(boolean force) { + int next = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, -1); + if (next == -1) { + int latest = this.getLatestEncryption(); + if (latest == Message.ENCRYPTION_NONE) { + if (force && getMode() == MODE_SINGLE) { + return Message.ENCRYPTION_OTR; + } else if (getContact().getPresences().size() == 1) { + if (getContact().getOtrFingerprints().size() >= 1) { + return Message.ENCRYPTION_OTR; + } else { + return latest; + } + } else { + return latest; + } + } else { + return latest; + } + } + if (next == Message.ENCRYPTION_NONE && force + && getMode() == MODE_SINGLE) { + return Message.ENCRYPTION_OTR; + } else { + return next; + } + } + + public void setNextEncryption(int encryption) { + this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, String.valueOf(encryption)); + } + + public String getNextMessage() { + if (this.nextMessage == null) { + return ""; + } else { + return this.nextMessage; + } + } + + public void setNextMessage(String message) { + this.nextMessage = message; + } + + public void setSymmetricKey(byte[] key) { + this.symmetricKey = key; + } + + public byte[] getSymmetricKey() { + return this.symmetricKey; + } + + public void setBookmark(Bookmark bookmark) { + this.bookmark = bookmark; + this.bookmark.setConversation(this); + } + + public void deregisterWithBookmark() { + if (this.bookmark != null) { + this.bookmark.setConversation(null); + } + } + + public Bookmark getBookmark() { + return this.bookmark; + } + + public boolean hasDuplicateMessage(Message message) { + for (int i = this.getMessages().size() - 1; i >= 0; --i) { + if (this.messages.get(i).equals(message)) { + return true; + } + } + return false; + } + + public void setMutedTill(long value) { + this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value)); + } + + public boolean isMuted() { + return SystemClock.elapsedRealtime() < this.getLongAttribute( + ATTRIBUTE_MUTED_TILL, 0); + } + + public boolean setAttribute(String key, String value) { + try { + this.attributes.put(key, value); + return true; + } catch (JSONException e) { + return false; + } + } + + public String getAttribute(String key) { + try { + return this.attributes.getString(key); + } catch (JSONException e) { + return null; + } + } + + public int getIntAttribute(String key, int defaultValue) { + String value = this.getAttribute(key); + if (value == null) { + return defaultValue; + } else { + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + return defaultValue; + } + } + } + + public long getLongAttribute(String key, long defaultValue) { + String value = this.getAttribute(key); + if (value == null) { + return defaultValue; + } else { + try { + return Long.parseLong(value); + } catch (NumberFormatException e) { + return defaultValue; + } + } + } + + public void add(Message message) { + message.setConversation(this); + synchronized (this.messages) { + this.messages.add(message); + } + } + + public void addAll(int index, List messages) { + synchronized (this.messages) { + this.messages.addAll(index, messages); + } + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/entities/Downloadable.java b/conversations/src/main/java/eu/siacs/conversations/entities/Downloadable.java new file mode 100644 index 00000000..70516b20 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/entities/Downloadable.java @@ -0,0 +1,21 @@ +package eu.siacs.conversations.entities; + +public interface Downloadable { + + public final String[] VALID_EXTENSIONS = { "webp", "jpeg", "jpg", "png" }; + public final String[] VALID_CRYPTO_EXTENSIONS = { "pgp", "gpg", "otr" }; + + public static final int STATUS_UNKNOWN = 0x200; + public static final int STATUS_CHECKING = 0x201; + public static final int STATUS_FAILED = 0x202; + public static final int STATUS_OFFER = 0x203; + public static final int STATUS_DOWNLOADING = 0x204; + public static final int STATUS_DELETED = 0x205; + public static final int STATUS_OFFER_CHECK_FILESIZE = 0x206; + + public boolean start(); + + public int getStatus(); + + public long getFileSize(); +} diff --git a/conversations/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java b/conversations/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java new file mode 100644 index 00000000..1605c75b --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java @@ -0,0 +1,154 @@ +package eu.siacs.conversations.entities; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.NoSuchAlgorithmException; + +import javax.crypto.Cipher; +import javax.crypto.CipherInputStream; +import javax.crypto.CipherOutputStream; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +import eu.siacs.conversations.Config; +import android.util.Log; + +public class DownloadableFile extends File { + + private static final long serialVersionUID = 2247012619505115863L; + + private long expectedSize = 0; + private String sha1sum; + private Key aeskey; + + private byte[] iv = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0xf }; + + public DownloadableFile(String path) { + super(path); + } + + public long getSize() { + return super.length(); + } + + public long getExpectedSize() { + if (this.aeskey != null) { + if (this.expectedSize == 0) { + return 0; + } else { + return (this.expectedSize / 16 + 1) * 16; + } + } else { + return this.expectedSize; + } + } + + public void setExpectedSize(long size) { + this.expectedSize = size; + } + + public String getSha1Sum() { + return this.sha1sum; + } + + public void setSha1Sum(String sum) { + this.sha1sum = sum; + } + + public void setKey(byte[] key) { + if (key.length == 48) { + byte[] secretKey = new byte[32]; + byte[] iv = new byte[16]; + System.arraycopy(key, 0, iv, 0, 16); + System.arraycopy(key, 16, secretKey, 0, 32); + this.aeskey = new SecretKeySpec(secretKey, "AES"); + this.iv = iv; + } else if (key.length >= 32) { + byte[] secretKey = new byte[32]; + System.arraycopy(key, 0, secretKey, 0, 32); + this.aeskey = new SecretKeySpec(secretKey, "AES"); + } else if (key.length >= 16) { + byte[] secretKey = new byte[16]; + System.arraycopy(key, 0, secretKey, 0, 16); + this.aeskey = new SecretKeySpec(secretKey, "AES"); + } + } + + public Key getKey() { + return this.aeskey; + } + + public InputStream createInputStream() { + if (this.getKey() == null) { + try { + return new FileInputStream(this); + } catch (FileNotFoundException e) { + return null; + } + } else { + try { + IvParameterSpec ips = new IvParameterSpec(iv); + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + cipher.init(Cipher.ENCRYPT_MODE, this.getKey(), ips); + Log.d(Config.LOGTAG, "opening encrypted input stream"); + return new CipherInputStream(new FileInputStream(this), cipher); + } catch (NoSuchAlgorithmException e) { + Log.d(Config.LOGTAG, "no such algo: " + e.getMessage()); + return null; + } catch (NoSuchPaddingException e) { + Log.d(Config.LOGTAG, "no such padding: " + e.getMessage()); + return null; + } catch (InvalidKeyException e) { + Log.d(Config.LOGTAG, "invalid key: " + e.getMessage()); + return null; + } catch (InvalidAlgorithmParameterException e) { + Log.d(Config.LOGTAG, "invavid iv:" + e.getMessage()); + return null; + } catch (FileNotFoundException e) { + return null; + } + } + } + + public OutputStream createOutputStream() { + if (this.getKey() == null) { + try { + return new FileOutputStream(this); + } catch (FileNotFoundException e) { + return null; + } + } else { + try { + IvParameterSpec ips = new IvParameterSpec(this.iv); + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + cipher.init(Cipher.DECRYPT_MODE, this.getKey(), ips); + Log.d(Config.LOGTAG, "opening encrypted output stream"); + return new CipherOutputStream(new FileOutputStream(this), + cipher); + } catch (NoSuchAlgorithmException e) { + Log.d(Config.LOGTAG, "no such algo: " + e.getMessage()); + return null; + } catch (NoSuchPaddingException e) { + Log.d(Config.LOGTAG, "no such padding: " + e.getMessage()); + return null; + } catch (InvalidKeyException e) { + Log.d(Config.LOGTAG, "invalid key: " + e.getMessage()); + return null; + } catch (InvalidAlgorithmParameterException e) { + Log.d(Config.LOGTAG, "invavid iv:" + e.getMessage()); + return null; + } catch (FileNotFoundException e) { + return null; + } + } + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/entities/ListItem.java b/conversations/src/main/java/eu/siacs/conversations/entities/ListItem.java new file mode 100644 index 00000000..a1872d2f --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/entities/ListItem.java @@ -0,0 +1,7 @@ +package eu.siacs.conversations.entities; + +public interface ListItem extends Comparable { + public String getDisplayName(); + + public String getJid(); +} diff --git a/conversations/src/main/java/eu/siacs/conversations/entities/Message.java b/conversations/src/main/java/eu/siacs/conversations/entities/Message.java new file mode 100644 index 00000000..a390c7ca --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/entities/Message.java @@ -0,0 +1,478 @@ +package eu.siacs.conversations.entities; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Arrays; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; + +public class Message extends AbstractEntity { + + public static final String TABLENAME = "messages"; + + public static final int STATUS_RECEIVED = 0; + public static final int STATUS_UNSEND = 1; + public static final int STATUS_SEND = 2; + public static final int STATUS_SEND_FAILED = 3; + public static final int STATUS_SEND_REJECTED = 4; + public static final int STATUS_WAITING = 5; + public static final int STATUS_OFFERED = 6; + public static final int STATUS_SEND_RECEIVED = 7; + public static final int STATUS_SEND_DISPLAYED = 8; + + public static final int ENCRYPTION_NONE = 0; + public static final int ENCRYPTION_PGP = 1; + public static final int ENCRYPTION_OTR = 2; + public static final int ENCRYPTION_DECRYPTED = 3; + public static final int ENCRYPTION_DECRYPTION_FAILED = 4; + + public static final int TYPE_TEXT = 0; + public static final int TYPE_IMAGE = 1; + public static final int TYPE_AUDIO = 2; + public static final int TYPE_STATUS = 3; + public static final int TYPE_PRIVATE = 4; + + public static String CONVERSATION = "conversationUuid"; + public static String COUNTERPART = "counterpart"; + public static String TRUE_COUNTERPART = "trueCounterpart"; + public static String BODY = "body"; + public static String TIME_SENT = "timeSent"; + public static String ENCRYPTION = "encryption"; + public static String STATUS = "status"; + public static String TYPE = "type"; + public static String REMOTE_MSG_ID = "remoteMsgId"; + + protected String conversationUuid; + protected String counterpart; + protected String trueCounterpart; + protected String body; + protected String encryptedBody; + protected long timeSent; + protected int encryption; + protected int status; + protected int type; + protected boolean read = true; + protected String remoteMsgId = null; + + protected Conversation conversation = null; + protected Downloadable downloadable = null; + public boolean markable = false; + + private Message mNextMessage = null; + private Message mPreviousMessage = null; + + private Message() { + + } + + public Message(Conversation conversation, String body, int encryption) { + this(java.util.UUID.randomUUID().toString(), conversation.getUuid(), + conversation.getContactJid(), null, body, System + .currentTimeMillis(), encryption, + Message.STATUS_UNSEND, TYPE_TEXT, null); + this.conversation = conversation; + } + + public Message(Conversation conversation, String counterpart, String body, + int encryption, int status) { + this(java.util.UUID.randomUUID().toString(), conversation.getUuid(), + counterpart, null, body, System.currentTimeMillis(), + encryption, status, TYPE_TEXT, null); + this.conversation = conversation; + } + + public Message(String uuid, String conversationUUid, String counterpart, + String trueCounterpart, String body, long timeSent, int encryption, + int status, int type, String remoteMsgId) { + this.uuid = uuid; + this.conversationUuid = conversationUUid; + this.counterpart = counterpart; + this.trueCounterpart = trueCounterpart; + this.body = body; + this.timeSent = timeSent; + this.encryption = encryption; + this.status = status; + this.type = type; + this.remoteMsgId = remoteMsgId; + } + + @Override + public ContentValues getContentValues() { + ContentValues values = new ContentValues(); + values.put(UUID, uuid); + values.put(CONVERSATION, conversationUuid); + values.put(COUNTERPART, counterpart); + values.put(TRUE_COUNTERPART, trueCounterpart); + values.put(BODY, body); + values.put(TIME_SENT, timeSent); + values.put(ENCRYPTION, encryption); + values.put(STATUS, status); + values.put(TYPE, type); + values.put(REMOTE_MSG_ID, remoteMsgId); + return values; + } + + public String getConversationUuid() { + return conversationUuid; + } + + public Conversation getConversation() { + return this.conversation; + } + + public String getCounterpart() { + return counterpart; + } + + public Contact getContact() { + if (this.conversation.getMode() == Conversation.MODE_SINGLE) { + return this.conversation.getContact(); + } else { + if (this.trueCounterpart == null) { + return null; + } else { + return this.conversation.getAccount().getRoster() + .getContactFromRoster(this.trueCounterpart); + } + } + } + + public String getBody() { + return body; + } + + public String getReadableBody(Context context) { + if (encryption == ENCRYPTION_PGP) { + return context.getText(R.string.encrypted_message_received) + .toString(); + } else if (encryption == ENCRYPTION_DECRYPTION_FAILED) { + return context.getText(R.string.decryption_failed).toString(); + } else if (type == TYPE_IMAGE) { + return context.getText(R.string.image_file).toString(); + } else { + return body.trim(); + } + } + + public long getTimeSent() { + return timeSent; + } + + public int getEncryption() { + return encryption; + } + + public int getStatus() { + return status; + } + + public String getRemoteMsgId() { + return this.remoteMsgId; + } + + public void setRemoteMsgId(String id) { + this.remoteMsgId = id; + } + + public static Message fromCursor(Cursor cursor) { + return new Message(cursor.getString(cursor.getColumnIndex(UUID)), + cursor.getString(cursor.getColumnIndex(CONVERSATION)), + cursor.getString(cursor.getColumnIndex(COUNTERPART)), + cursor.getString(cursor.getColumnIndex(TRUE_COUNTERPART)), + cursor.getString(cursor.getColumnIndex(BODY)), + cursor.getLong(cursor.getColumnIndex(TIME_SENT)), + cursor.getInt(cursor.getColumnIndex(ENCRYPTION)), + cursor.getInt(cursor.getColumnIndex(STATUS)), + cursor.getInt(cursor.getColumnIndex(TYPE)), + cursor.getString(cursor.getColumnIndex(REMOTE_MSG_ID))); + } + + public void setConversation(Conversation conv) { + this.conversation = conv; + } + + public void setStatus(int status) { + this.status = status; + } + + public boolean isRead() { + return this.read; + } + + public void markRead() { + this.read = true; + } + + public void markUnread() { + this.read = false; + } + + public void setTime(long time) { + this.timeSent = time; + } + + public void setEncryption(int encryption) { + this.encryption = encryption; + } + + public void setBody(String body) { + this.body = body; + } + + public String getEncryptedBody() { + return this.encryptedBody; + } + + public void setEncryptedBody(String body) { + this.encryptedBody = body; + } + + public void setType(int type) { + this.type = type; + } + + public int getType() { + return this.type; + } + + public void setPresence(String presence) { + if (presence == null) { + this.counterpart = this.counterpart.split("/", 2)[0]; + } else { + this.counterpart = this.counterpart.split("/", 2)[0] + "/" + + presence; + } + } + + public void setTrueCounterpart(String trueCounterpart) { + this.trueCounterpart = trueCounterpart; + } + + public String getPresence() { + String[] counterparts = this.counterpart.split("/", 2); + if (counterparts.length == 2) { + return counterparts[1]; + } else { + if (this.counterpart.contains("/")) { + return ""; + } else { + return null; + } + } + } + + public void setDownloadable(Downloadable downloadable) { + this.downloadable = downloadable; + } + + public Downloadable getDownloadable() { + return this.downloadable; + } + + public static Message createStatusMessage(Conversation conversation) { + Message message = new Message(); + message.setType(Message.TYPE_STATUS); + message.setConversation(conversation); + return message; + } + + public void setCounterpart(String counterpart) { + this.counterpart = counterpart; + } + + public boolean equals(Message message) { + if ((this.remoteMsgId != null) && (this.body != null) + && (this.counterpart != null)) { + return this.remoteMsgId.equals(message.getRemoteMsgId()) + && this.body.equals(message.getBody()) + && this.counterpart.equals(message.getCounterpart()); + } else { + return false; + } + } + + public Message next() { + if (this.mNextMessage == null) { + synchronized (this.conversation.messages) { + int index = this.conversation.messages.indexOf(this); + if (index < 0 + || index >= this.conversation.getMessages().size() - 1) { + this.mNextMessage = null; + } else { + this.mNextMessage = this.conversation.messages + .get(index + 1); + } + } + } + return this.mNextMessage; + } + + public Message prev() { + if (this.mPreviousMessage == null) { + synchronized (this.conversation.messages) { + int index = this.conversation.messages.indexOf(this); + if (index <= 0 || index > this.conversation.messages.size()) { + this.mPreviousMessage = null; + } else { + this.mPreviousMessage = this.conversation.messages + .get(index - 1); + } + } + } + return this.mPreviousMessage; + } + + public boolean mergable(Message message) { + if (message == null) { + return false; + } + return (message.getType() == Message.TYPE_TEXT + && this.getDownloadable() == null + && message.getDownloadable() == null + && message.getEncryption() != Message.ENCRYPTION_PGP + && this.getType() == message.getType() + && this.getEncryption() == message.getEncryption() + && this.getCounterpart().equals(message.getCounterpart()) + && (message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) && ((this + .getStatus() == message.getStatus() || ((this.getStatus() == Message.STATUS_SEND || this + .getStatus() == Message.STATUS_SEND_RECEIVED) && (message + .getStatus() == Message.STATUS_UNSEND + || message.getStatus() == Message.STATUS_SEND || message + .getStatus() == Message.STATUS_SEND_DISPLAYED))))); + } + + public String getMergedBody() { + Message next = this.next(); + if (this.mergable(next)) { + return body.trim() + '\n' + next.getMergedBody(); + } + return body.trim(); + } + + public int getMergedStatus() { + Message next = this.next(); + if (this.mergable(next)) { + return next.getMergedStatus(); + } else { + return getStatus(); + } + } + + public long getMergedTimeSent() { + Message next = this.next(); + if (this.mergable(next)) { + return next.getMergedTimeSent(); + } else { + return getTimeSent(); + } + } + + public boolean wasMergedIntoPrevious() { + Message prev = this.prev(); + if (prev == null) { + return false; + } else { + return prev.mergable(this); + } + } + + public boolean bodyContainsDownloadable() { + Contact contact = this.getContact(); + if (status <= STATUS_RECEIVED + && (contact == null || !contact.trusted())) { + return false; + } + try { + URL url = new URL(this.getBody()); + if (!url.getProtocol().equalsIgnoreCase("http") + && !url.getProtocol().equalsIgnoreCase("https")) { + return false; + } + if (url.getPath() == null) { + return false; + } + String[] pathParts = url.getPath().split("/"); + String filename = pathParts[pathParts.length - 1]; + String[] extensionParts = filename.split("\\."); + if (extensionParts.length == 2 + && Arrays.asList(Downloadable.VALID_EXTENSIONS).contains( + extensionParts[extensionParts.length - 1])) { + return true; + } else if (extensionParts.length == 3 + && Arrays + .asList(Downloadable.VALID_CRYPTO_EXTENSIONS) + .contains(extensionParts[extensionParts.length - 1]) + && Arrays.asList(Downloadable.VALID_EXTENSIONS).contains( + extensionParts[extensionParts.length - 2])) { + return true; + } else { + return false; + } + } catch (MalformedURLException e) { + return false; + } + } + + public ImageParams getImageParams() { + ImageParams params = new ImageParams(); + if (this.downloadable != null) { + params.size = this.downloadable.getFileSize(); + } + if (body == null) { + return params; + } + String parts[] = body.split(","); + if (parts.length == 1) { + try { + params.size = Long.parseLong(parts[0]); + } catch (NumberFormatException e) { + params.origin = parts[0]; + } + } else if (parts.length == 3) { + try { + params.size = Long.parseLong(parts[0]); + } catch (NumberFormatException e) { + params.size = 0; + } + try { + params.width = Integer.parseInt(parts[1]); + } catch (NumberFormatException e) { + params.width = 0; + } + try { + params.height = Integer.parseInt(parts[2]); + } catch (NumberFormatException e) { + params.height = 0; + } + } else if (parts.length == 4) { + params.origin = parts[0]; + try { + params.size = Long.parseLong(parts[1]); + } catch (NumberFormatException e) { + params.size = 0; + } + try { + params.width = Integer.parseInt(parts[2]); + } catch (NumberFormatException e) { + params.width = 0; + } + try { + params.height = Integer.parseInt(parts[3]); + } catch (NumberFormatException e) { + params.height = 0; + } + } + return params; + } + + public class ImageParams { + public long size = 0; + public int width = 0; + public int height = 0; + public String origin; + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/entities/MucOptions.java b/conversations/src/main/java/eu/siacs/conversations/entities/MucOptions.java new file mode 100644 index 00000000..d7407cd5 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/entities/MucOptions.java @@ -0,0 +1,369 @@ +package eu.siacs.conversations.entities; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import eu.siacs.conversations.crypto.PgpEngine; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.stanzas.PresencePacket; +import android.annotation.SuppressLint; + +@SuppressLint("DefaultLocale") +public class MucOptions { + public static final int ERROR_NO_ERROR = 0; + public static final int ERROR_NICK_IN_USE = 1; + public static final int ERROR_ROOM_NOT_FOUND = 2; + public static final int ERROR_PASSWORD_REQUIRED = 3; + public static final int ERROR_BANNED = 4; + public static final int ERROR_MEMBERS_ONLY = 5; + + public static final int KICKED_FROM_ROOM = 9; + + public static final String STATUS_CODE_BANNED = "301"; + public static final String STATUS_CODE_KICKED = "307"; + + public interface OnRenameListener { + public void onRename(boolean success); + } + + public class User { + public static final int ROLE_MODERATOR = 3; + public static final int ROLE_NONE = 0; + public static final int ROLE_PARTICIPANT = 2; + public static final int ROLE_VISITOR = 1; + public static final int AFFILIATION_ADMIN = 4; + public static final int AFFILIATION_OWNER = 3; + public static final int AFFILIATION_MEMBER = 2; + public static final int AFFILIATION_OUTCAST = 1; + public static final int AFFILIATION_NONE = 0; + + private int role; + private int affiliation; + private String name; + private String jid; + private long pgpKeyId = 0; + + public String getName() { + return name; + } + + public void setName(String user) { + this.name = user; + } + + public void setJid(String jid) { + this.jid = jid; + } + + public String getJid() { + return this.jid; + } + + public int getRole() { + return this.role; + } + + public void setRole(String role) { + role = role.toLowerCase(); + if (role.equals("moderator")) { + this.role = ROLE_MODERATOR; + } else if (role.equals("participant")) { + this.role = ROLE_PARTICIPANT; + } else if (role.equals("visitor")) { + this.role = ROLE_VISITOR; + } else { + this.role = ROLE_NONE; + } + } + + public int getAffiliation() { + return this.affiliation; + } + + public void setAffiliation(String affiliation) { + if (affiliation.equalsIgnoreCase("admin")) { + this.affiliation = AFFILIATION_ADMIN; + } else if (affiliation.equalsIgnoreCase("owner")) { + this.affiliation = AFFILIATION_OWNER; + } else if (affiliation.equalsIgnoreCase("member")) { + this.affiliation = AFFILIATION_MEMBER; + } else if (affiliation.equalsIgnoreCase("outcast")) { + this.affiliation = AFFILIATION_OUTCAST; + } else { + this.affiliation = AFFILIATION_NONE; + } + } + + public void setPgpKeyId(long id) { + this.pgpKeyId = id; + } + + public long getPgpKeyId() { + return this.pgpKeyId; + } + + public Contact getContact() { + return account.getRoster().getContactFromRoster(getJid()); + } + } + + private Account account; + private List users = new CopyOnWriteArrayList(); + private Conversation conversation; + private boolean isOnline = false; + private int error = ERROR_ROOM_NOT_FOUND; + private OnRenameListener renameListener = null; + private boolean aboutToRename = false; + private User self = new User(); + private String subject = null; + private String joinnick; + private String password = null; + + public MucOptions(Conversation conversation) { + this.account = conversation.getAccount(); + this.conversation = conversation; + } + + public void deleteUser(String name) { + for (int i = 0; i < users.size(); ++i) { + if (users.get(i).getName().equals(name)) { + users.remove(i); + return; + } + } + } + + public void addUser(User user) { + for (int i = 0; i < users.size(); ++i) { + if (users.get(i).getName().equals(user.getName())) { + users.set(i, user); + return; + } + } + users.add(user); + } + + public void processPacket(PresencePacket packet, PgpEngine pgp) { + String[] fromParts = packet.getFrom().split("/", 2); + if (fromParts.length >= 2) { + String name = fromParts[1]; + String type = packet.getAttribute("type"); + if (type == null) { + User user = new User(); + Element item = packet.findChild("x", + "http://jabber.org/protocol/muc#user") + .findChild("item"); + user.setName(name); + user.setAffiliation(item.getAttribute("affiliation")); + user.setRole(item.getAttribute("role")); + user.setJid(item.getAttribute("jid")); + user.setName(name); + if (name.equals(this.joinnick)) { + this.isOnline = true; + this.error = ERROR_NO_ERROR; + self = user; + if (aboutToRename) { + if (renameListener != null) { + renameListener.onRename(true); + } + aboutToRename = false; + } + } else { + addUser(user); + } + if (pgp != null) { + Element x = packet.findChild("x", "jabber:x:signed"); + if (x != null) { + Element status = packet.findChild("status"); + String msg; + if (status != null) { + msg = status.getContent(); + } else { + msg = ""; + } + user.setPgpKeyId(pgp.fetchKeyId(account, msg, + x.getContent())); + } + } + } else if (type.equals("unavailable") && name.equals(this.joinnick)) { + Element x = packet.findChild("x", + "http://jabber.org/protocol/muc#user"); + if (x != null) { + Element status = x.findChild("status"); + if (status != null) { + String code = status.getAttribute("code"); + if (STATUS_CODE_KICKED.equals(code)) { + this.isOnline = false; + this.error = KICKED_FROM_ROOM; + } else if (STATUS_CODE_BANNED.equals(code)) { + this.isOnline = false; + this.error = ERROR_BANNED; + } + } + } + } else if (type.equals("unavailable")) { + deleteUser(packet.getAttribute("from").split("/", 2)[1]); + } else if (type.equals("error")) { + Element error = packet.findChild("error"); + if (error != null && error.hasChild("conflict")) { + if (aboutToRename) { + if (renameListener != null) { + renameListener.onRename(false); + } + aboutToRename = false; + this.setJoinNick(getActualNick()); + } else { + this.error = ERROR_NICK_IN_USE; + } + } else if (error != null && error.hasChild("not-authorized")) { + this.error = ERROR_PASSWORD_REQUIRED; + } else if (error != null && error.hasChild("forbidden")) { + this.error = ERROR_BANNED; + } else if (error != null + && error.hasChild("registration-required")) { + this.error = ERROR_MEMBERS_ONLY; + } + } + } + } + + public List getUsers() { + return this.users; + } + + public String getProposedNick() { + String[] mucParts = conversation.getContactJid().split("/", 2); + if (conversation.getBookmark() != null + && conversation.getBookmark().getNick() != null) { + return conversation.getBookmark().getNick(); + } else { + if (mucParts.length == 2) { + return mucParts[1]; + } else { + return account.getUsername(); + } + } + } + + public String getActualNick() { + if (this.self.getName() != null) { + return this.self.getName(); + } else { + return this.getProposedNick(); + } + } + + public void setJoinNick(String nick) { + this.joinnick = nick; + } + + public boolean online() { + return this.isOnline; + } + + public int getError() { + return this.error; + } + + public void setOnRenameListener(OnRenameListener listener) { + this.renameListener = listener; + } + + public OnRenameListener getOnRenameListener() { + return this.renameListener; + } + + public void setOffline() { + this.users.clear(); + this.error = 0; + this.isOnline = false; + } + + public User getSelf() { + return self; + } + + public void setSubject(String content) { + this.subject = content; + } + + public String getSubject() { + return this.subject; + } + + public void flagAboutToRename() { + this.aboutToRename = true; + } + + public long[] getPgpKeyIds() { + List ids = new ArrayList(); + for (User user : getUsers()) { + if (user.getPgpKeyId() != 0) { + ids.add(user.getPgpKeyId()); + } + } + long[] primitivLongArray = new long[ids.size()]; + for (int i = 0; i < ids.size(); ++i) { + primitivLongArray[i] = ids.get(i); + } + return primitivLongArray; + } + + public boolean pgpKeysInUse() { + for (User user : getUsers()) { + if (user.getPgpKeyId() != 0) { + return true; + } + } + return false; + } + + public boolean everybodyHasKeys() { + for (User user : getUsers()) { + if (user.getPgpKeyId() == 0) { + return false; + } + } + return true; + } + + public String getJoinJid() { + return this.conversation.getContactJid().split("/", 2)[0] + "/" + + this.joinnick; + } + + public String getTrueCounterpart(String counterpart) { + for (User user : this.getUsers()) { + if (user.getName().equals(counterpart)) { + return user.getJid(); + } + } + return null; + } + + public String getPassword() { + this.password = conversation + .getAttribute(Conversation.ATTRIBUTE_MUC_PASSWORD); + if (this.password == null && conversation.getBookmark() != null + && conversation.getBookmark().getPassword() != null) { + return conversation.getBookmark().getPassword(); + } else { + return this.password; + } + } + + public void setPassword(String password) { + if (conversation.getBookmark() != null) { + conversation.getBookmark().setPassword(password); + } else { + this.password = password; + } + conversation + .setAttribute(Conversation.ATTRIBUTE_MUC_PASSWORD, password); + } + + public Conversation getConversation() { + return this.conversation; + } +} \ No newline at end of file diff --git a/conversations/src/main/java/eu/siacs/conversations/entities/Presences.java b/conversations/src/main/java/eu/siacs/conversations/entities/Presences.java new file mode 100644 index 00000000..b5899847 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/entities/Presences.java @@ -0,0 +1,76 @@ +package eu.siacs.conversations.entities; + +import java.util.Hashtable; +import java.util.Iterator; +import java.util.Map.Entry; + +import eu.siacs.conversations.xml.Element; + +public class Presences { + + public static final int CHAT = -1; + public static final int ONLINE = 0; + public static final int AWAY = 1; + public static final int XA = 2; + public static final int DND = 3; + public static final int OFFLINE = 4; + + private Hashtable presences = new Hashtable(); + + public Hashtable getPresences() { + return this.presences; + } + + public void updatePresence(String resource, int status) { + this.presences.put(resource, status); + } + + public void removePresence(String resource) { + this.presences.remove(resource); + } + + public void clearPresences() { + this.presences.clear(); + } + + public int getMostAvailableStatus() { + int status = OFFLINE; + Iterator> it = presences.entrySet().iterator(); + while (it.hasNext()) { + Entry entry = it.next(); + if (entry.getValue() < status) + status = entry.getValue(); + } + return status; + } + + public static int parseShow(Element show) { + if ((show == null) || (show.getContent() == null)) { + return Presences.ONLINE; + } else if (show.getContent().equals("away")) { + return Presences.AWAY; + } else if (show.getContent().equals("xa")) { + return Presences.XA; + } else if (show.getContent().equals("chat")) { + return Presences.CHAT; + } else if (show.getContent().equals("dnd")) { + return Presences.DND; + } else { + return Presences.OFFLINE; + } + } + + public int size() { + return presences.size(); + } + + public String[] asStringArray() { + final String[] presencesArray = new String[presences.size()]; + presences.keySet().toArray(presencesArray); + return presencesArray; + } + + public boolean has(String presence) { + return presences.containsKey(presence); + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/entities/Roster.java b/conversations/src/main/java/eu/siacs/conversations/entities/Roster.java new file mode 100644 index 00000000..3267b15a --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/entities/Roster.java @@ -0,0 +1,83 @@ +package eu.siacs.conversations.entities; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.ConcurrentHashMap; + +public class Roster { + Account account; + ConcurrentHashMap contacts = new ConcurrentHashMap(); + private String version = null; + + public Roster(Account account) { + this.account = account; + } + + public Contact getContactFromRoster(String jid) { + if (jid == null) { + return null; + } + String cleanJid = jid.split("/", 2)[0]; + Contact contact = contacts.get(cleanJid); + if (contact != null && contact.showInRoster()) { + return contact; + } else { + return null; + } + } + + public Contact getContact(String jid) { + String cleanJid = jid.split("/", 2)[0].toLowerCase(Locale.getDefault()); + if (contacts.containsKey(cleanJid)) { + return contacts.get(cleanJid); + } else { + Contact contact = new Contact(cleanJid); + contact.setAccount(account); + contacts.put(cleanJid, contact); + return contact; + } + } + + public void clearPresences() { + for (Contact contact : getContacts()) { + contact.clearPresences(); + } + } + + public void markAllAsNotInRoster() { + for (Contact contact : getContacts()) { + contact.resetOption(Contact.Options.IN_ROSTER); + } + } + + public void clearSystemAccounts() { + for (Contact contact : getContacts()) { + contact.setPhotoUri(null); + contact.setSystemName(null); + contact.setSystemAccount(null); + } + } + + public List getContacts() { + return new ArrayList(this.contacts.values()); + } + + public void initContact(Contact contact) { + contact.setAccount(account); + contact.setOption(Contact.Options.IN_ROSTER); + contacts.put(contact.getJid(), contact); + } + + public void setVersion(String version) { + this.version = version; + } + + public String getVersion() { + return this.version; + } + + public Account getAccount() { + return this.account; + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java b/conversations/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java new file mode 100644 index 00000000..c96d116d --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java @@ -0,0 +1,48 @@ +package eu.siacs.conversations.generator; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import eu.siacs.conversations.services.XmppConnectionService; + +import android.util.Base64; + +public abstract class AbstractGenerator { + public final String[] FEATURES = { "urn:xmpp:jingle:1", + "urn:xmpp:jingle:apps:file-transfer:3", + "urn:xmpp:jingle:transports:s5b:1", + "urn:xmpp:jingle:transports:ibb:1", "urn:xmpp:receipts", + "urn:xmpp:chat-markers:0", "http://jabber.org/protocol/muc", + "jabber:x:conference", "http://jabber.org/protocol/caps", + "http://jabber.org/protocol/disco#info", + "urn:xmpp:avatar:metadata+notify" }; + public final String IDENTITY_NAME = "Conversations 0.7"; + public final String IDENTITY_TYPE = "phone"; + + protected XmppConnectionService mXmppConnectionService; + + protected AbstractGenerator(XmppConnectionService service) { + this.mXmppConnectionService = service; + } + + public String getCapHash() { + StringBuilder s = new StringBuilder(); + s.append("client/" + IDENTITY_TYPE + "//" + IDENTITY_NAME + "<"); + MessageDigest md = null; + try { + md = MessageDigest.getInstance("SHA-1"); + } catch (NoSuchAlgorithmException e) { + return null; + } + List features = Arrays.asList(FEATURES); + Collections.sort(features); + for (String feature : features) { + s.append(feature + "<"); + } + byte[] sha1 = md.digest(s.toString().getBytes()); + return new String(Base64.encode(sha1, Base64.DEFAULT)).trim(); + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/conversations/src/main/java/eu/siacs/conversations/generator/IqGenerator.java new file mode 100644 index 00000000..d44bf0ca --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -0,0 +1,96 @@ +package eu.siacs.conversations.generator; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.pep.Avatar; +import eu.siacs.conversations.xmpp.stanzas.IqPacket; + +public class IqGenerator extends AbstractGenerator { + + public IqGenerator(XmppConnectionService service) { + super(service); + } + + public IqPacket discoResponse(IqPacket request) { + IqPacket packet = new IqPacket(IqPacket.TYPE_RESULT); + packet.setId(request.getId()); + packet.setTo(request.getFrom()); + Element query = packet.addChild("query", + "http://jabber.org/protocol/disco#info"); + query.setAttribute("node", request.query().getAttribute("node")); + Element identity = query.addChild("identity"); + identity.setAttribute("category", "client"); + identity.setAttribute("type", this.IDENTITY_TYPE); + identity.setAttribute("name", IDENTITY_NAME); + List features = Arrays.asList(FEATURES); + Collections.sort(features); + for (String feature : features) { + query.addChild("feature").setAttribute("var", feature); + } + return packet; + } + + protected IqPacket publish(String node, Element item) { + IqPacket packet = new IqPacket(IqPacket.TYPE_SET); + Element pubsub = packet.addChild("pubsub", + "http://jabber.org/protocol/pubsub"); + Element publish = pubsub.addChild("publish"); + publish.setAttribute("node", node); + publish.addChild(item); + return packet; + } + + protected IqPacket retrieve(String node, Element item) { + IqPacket packet = new IqPacket(IqPacket.TYPE_GET); + Element pubsub = packet.addChild("pubsub", + "http://jabber.org/protocol/pubsub"); + Element items = pubsub.addChild("items"); + items.setAttribute("node", node); + if (item != null) { + items.addChild(item); + } + return packet; + } + + public IqPacket publishAvatar(Avatar avatar) { + Element item = new Element("item"); + item.setAttribute("id", avatar.sha1sum); + Element data = item.addChild("data", "urn:xmpp:avatar:data"); + data.setContent(avatar.image); + return publish("urn:xmpp:avatar:data", item); + } + + public IqPacket publishAvatarMetadata(Avatar avatar) { + Element item = new Element("item"); + item.setAttribute("id", avatar.sha1sum); + Element metadata = item + .addChild("metadata", "urn:xmpp:avatar:metadata"); + Element info = metadata.addChild("info"); + info.setAttribute("bytes", avatar.size); + info.setAttribute("id", avatar.sha1sum); + info.setAttribute("height", avatar.height); + info.setAttribute("width", avatar.height); + info.setAttribute("type", avatar.type); + return publish("urn:xmpp:avatar:metadata", item); + } + + public IqPacket retrieveAvatar(Avatar avatar) { + Element item = new Element("item"); + item.setAttribute("id", avatar.sha1sum); + IqPacket packet = retrieve("urn:xmpp:avatar:data", item); + packet.setTo(avatar.owner); + return packet; + } + + public IqPacket retrieveAvatarMetaData(String to) { + IqPacket packet = retrieve("urn:xmpp:avatar:metadata", null); + if (to != null) { + packet.setTo(to); + } + return packet; + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/conversations/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java new file mode 100644 index 00000000..dd833e56 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java @@ -0,0 +1,178 @@ +package eu.siacs.conversations.generator; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + +import net.java.otr4j.OtrException; +import net.java.otr4j.session.Session; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.stanzas.MessagePacket; + +public class MessageGenerator extends AbstractGenerator { + public MessageGenerator(XmppConnectionService service) { + super(service); + } + + private MessagePacket preparePacket(Message message, boolean addDelay) { + Conversation conversation = message.getConversation(); + Account account = conversation.getAccount(); + MessagePacket packet = new MessagePacket(); + if (conversation.getMode() == Conversation.MODE_SINGLE) { + packet.setTo(message.getCounterpart()); + packet.setType(MessagePacket.TYPE_CHAT); + packet.addChild("markable", "urn:xmpp:chat-markers:0"); + if (this.mXmppConnectionService.indicateReceived()) { + packet.addChild("request", "urn:xmpp:receipts"); + } + } else if (message.getType() == Message.TYPE_PRIVATE) { + packet.setTo(message.getCounterpart()); + packet.setType(MessagePacket.TYPE_CHAT); + } else { + packet.setTo(message.getCounterpart().split("/", 2)[0]); + packet.setType(MessagePacket.TYPE_GROUPCHAT); + } + packet.setFrom(account.getFullJid()); + packet.setId(message.getUuid()); + if (addDelay) { + addDelay(packet, message.getTimeSent()); + } + return packet; + } + + private void addDelay(MessagePacket packet, long timestamp) { + final SimpleDateFormat mDateFormat = new SimpleDateFormat( + "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US); + mDateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + Element delay = packet.addChild("delay", "urn:xmpp:delay"); + Date date = new Date(timestamp); + delay.setAttribute("stamp", mDateFormat.format(date)); + } + + public MessagePacket generateOtrChat(Message message) { + return generateOtrChat(message, false); + } + + public MessagePacket generateOtrChat(Message message, boolean addDelay) { + Session otrSession = message.getConversation().getOtrSession(); + if (otrSession == null) { + return null; + } + MessagePacket packet = preparePacket(message, addDelay); + packet.addChild("private", "urn:xmpp:carbons:2"); + packet.addChild("no-copy", "urn:xmpp:hints"); + try { + packet.setBody(otrSession.transformSending(message.getBody())); + return packet; + } catch (OtrException e) { + return null; + } + } + + public MessagePacket generateChat(Message message) { + return generateChat(message, false); + } + + public MessagePacket generateChat(Message message, boolean addDelay) { + MessagePacket packet = preparePacket(message, addDelay); + packet.setBody(message.getBody()); + return packet; + } + + public MessagePacket generatePgpChat(Message message) { + return generatePgpChat(message, false); + } + + public MessagePacket generatePgpChat(Message message, boolean addDelay) { + MessagePacket packet = preparePacket(message, addDelay); + packet.setBody("This is an XEP-0027 encryted message"); + if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { + packet.addChild("x", "jabber:x:encrypted").setContent( + message.getEncryptedBody()); + } else if (message.getEncryption() == Message.ENCRYPTION_PGP) { + packet.addChild("x", "jabber:x:encrypted").setContent( + message.getBody()); + } + return packet; + } + + public MessagePacket generateNotAcceptable(MessagePacket origin) { + MessagePacket packet = generateError(origin); + Element error = packet.addChild("error"); + error.setAttribute("type", "modify"); + error.setAttribute("code", "406"); + error.addChild("not-acceptable"); + return packet; + } + + private MessagePacket generateError(MessagePacket origin) { + MessagePacket packet = new MessagePacket(); + packet.setId(origin.getId()); + packet.setTo(origin.getFrom()); + packet.setBody(origin.getBody()); + packet.setType(MessagePacket.TYPE_ERROR); + return packet; + } + + public MessagePacket confirm(Account account, String to, String id) { + MessagePacket packet = new MessagePacket(); + packet.setType(MessagePacket.TYPE_NORMAL); + packet.setTo(to); + packet.setFrom(account.getFullJid()); + Element received = packet.addChild("displayed", + "urn:xmpp:chat-markers:0"); + received.setAttribute("id", id); + return packet; + } + + public MessagePacket conferenceSubject(Conversation conversation, + String subject) { + MessagePacket packet = new MessagePacket(); + packet.setType(MessagePacket.TYPE_GROUPCHAT); + packet.setTo(conversation.getContactJid().split("/", 2)[0]); + Element subjectChild = new Element("subject"); + subjectChild.setContent(subject); + packet.addChild(subjectChild); + packet.setFrom(conversation.getAccount().getJid()); + return packet; + } + + public MessagePacket directInvite(Conversation conversation, String contact) { + MessagePacket packet = new MessagePacket(); + packet.setType(MessagePacket.TYPE_NORMAL); + packet.setTo(contact); + packet.setFrom(conversation.getAccount().getFullJid()); + Element x = packet.addChild("x", "jabber:x:conference"); + x.setAttribute("jid", conversation.getContactJid().split("/", 2)[0]); + return packet; + } + + public MessagePacket invite(Conversation conversation, String contact) { + MessagePacket packet = new MessagePacket(); + packet.setTo(conversation.getContactJid().split("/", 2)[0]); + packet.setFrom(conversation.getAccount().getFullJid()); + Element x = new Element("x"); + x.setAttribute("xmlns", "http://jabber.org/protocol/muc#user"); + Element invite = new Element("invite"); + invite.setAttribute("to", contact); + x.addChild(invite); + packet.addChild(x); + return packet; + } + + public MessagePacket received(Account account, + MessagePacket originalMessage, String namespace) { + MessagePacket receivedPacket = new MessagePacket(); + receivedPacket.setType(MessagePacket.TYPE_NORMAL); + receivedPacket.setTo(originalMessage.getFrom()); + receivedPacket.setFrom(account.getFullJid()); + Element received = receivedPacket.addChild("received", namespace); + received.setAttribute("id", originalMessage.getId()); + return receivedPacket; + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java b/conversations/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java new file mode 100644 index 00000000..d896dd00 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java @@ -0,0 +1,57 @@ +package eu.siacs.conversations.generator; + +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.stanzas.PresencePacket; + +public class PresenceGenerator extends AbstractGenerator { + + public PresenceGenerator(XmppConnectionService service) { + super(service); + } + + private PresencePacket subscription(String type, Contact contact) { + PresencePacket packet = new PresencePacket(); + packet.setAttribute("type", type); + packet.setAttribute("to", contact.getJid()); + packet.setAttribute("from", contact.getAccount().getJid()); + return packet; + } + + public PresencePacket requestPresenceUpdatesFrom(Contact contact) { + return subscription("subscribe", contact); + } + + public PresencePacket stopPresenceUpdatesFrom(Contact contact) { + return subscription("unsubscribe", contact); + } + + public PresencePacket stopPresenceUpdatesTo(Contact contact) { + return subscription("unsubscribed", contact); + } + + public PresencePacket sendPresenceUpdatesTo(Contact contact) { + return subscription("subscribed", contact); + } + + public PresencePacket sendPresence(Account account) { + PresencePacket packet = new PresencePacket(); + packet.setAttribute("from", account.getFullJid()); + String sig = account.getPgpSignature(); + if (sig != null) { + packet.addChild("status").setContent("online"); + packet.addChild("x", "jabber:x:signed").setContent(sig); + } + String capHash = getCapHash(); + if (capHash != null) { + Element cap = packet.addChild("c", + "http://jabber.org/protocol/caps"); + cap.setAttribute("hash", "sha-1"); + cap.setAttribute("node", "http://conversions.siacs.eu"); + cap.setAttribute("ver", capHash); + } + return packet; + } +} \ No newline at end of file diff --git a/conversations/src/main/java/eu/siacs/conversations/http/HttpConnection.java b/conversations/src/main/java/eu/siacs/conversations/http/HttpConnection.java new file mode 100644 index 00000000..407a13d9 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/http/HttpConnection.java @@ -0,0 +1,255 @@ +package eu.siacs.conversations.http; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.X509TrustManager; + +import org.apache.http.conn.ssl.StrictHostnameVerifier; + +import android.content.Intent; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.util.Log; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.entities.Downloadable; +import eu.siacs.conversations.entities.DownloadableFile; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.utils.CryptoHelper; + +public class HttpConnection implements Downloadable { + + private HttpConnectionManager mHttpConnectionManager; + private XmppConnectionService mXmppConnectionService; + + private URL mUrl; + private Message message; + private DownloadableFile file; + private int mStatus = Downloadable.STATUS_UNKNOWN; + + public HttpConnection(HttpConnectionManager manager) { + this.mHttpConnectionManager = manager; + this.mXmppConnectionService = manager.getXmppConnectionService(); + } + + @Override + public boolean start() { + if (mXmppConnectionService.hasInternetConnection()) { + if (this.mStatus == STATUS_OFFER_CHECK_FILESIZE) { + checkFileSize(true); + } else { + new Thread(new FileDownloader(true)).start(); + } + return true; + } else { + return false; + } + } + + public void init(Message message) { + this.message = message; + this.message.setDownloadable(this); + try { + mUrl = new URL(message.getBody()); + this.file = mXmppConnectionService.getFileBackend().getFile( + message, false); + String reference = mUrl.getRef(); + if (reference != null && reference.length() == 96) { + this.file.setKey(CryptoHelper.hexToBytes(reference)); + } + if (this.message.getEncryption() == Message.ENCRYPTION_OTR + && this.file.getKey() == null) { + this.message.setEncryption(Message.ENCRYPTION_NONE); + } + checkFileSize(false); + } catch (MalformedURLException e) { + this.cancel(); + } + } + + private void checkFileSize(boolean interactive) { + new Thread(new FileSizeChecker(interactive)).start(); + } + + public void cancel() { + mHttpConnectionManager.finishConnection(this); + message.setDownloadable(null); + mXmppConnectionService.updateConversationUi(); + } + + private void finish() { + Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); + intent.setData(Uri.fromFile(file)); + mXmppConnectionService.sendBroadcast(intent); + message.setDownloadable(null); + mHttpConnectionManager.finishConnection(this); + } + + private void changeStatus(int status) { + this.mStatus = status; + mXmppConnectionService.updateConversationUi(); + } + + private void setupTrustManager(HttpsURLConnection connection, + boolean interactive) { + X509TrustManager trustManager; + HostnameVerifier hostnameVerifier; + if (interactive) { + trustManager = mXmppConnectionService.getMemorizingTrustManager(); + hostnameVerifier = mXmppConnectionService + .getMemorizingTrustManager().wrapHostnameVerifier( + new StrictHostnameVerifier()); + } else { + trustManager = mXmppConnectionService.getMemorizingTrustManager() + .getNonInteractive(); + hostnameVerifier = mXmppConnectionService + .getMemorizingTrustManager() + .wrapHostnameVerifierNonInteractive( + new StrictHostnameVerifier()); + } + try { + SSLContext sc = SSLContext.getInstance("TLS"); + sc.init(null, new X509TrustManager[] { trustManager }, + mXmppConnectionService.getRNG()); + connection.setSSLSocketFactory(sc.getSocketFactory()); + connection.setHostnameVerifier(hostnameVerifier); + } catch (KeyManagementException e) { + return; + } catch (NoSuchAlgorithmException e) { + return; + } + } + + private class FileSizeChecker implements Runnable { + + private boolean interactive = false; + + public FileSizeChecker(boolean interactive) { + this.interactive = interactive; + } + + @Override + public void run() { + long size; + try { + size = retrieveFileSize(); + } catch (SSLHandshakeException e) { + changeStatus(STATUS_OFFER_CHECK_FILESIZE); + return; + } catch (IOException e) { + cancel(); + return; + } + file.setExpectedSize(size); + if (size <= mHttpConnectionManager.getAutoAcceptFileSize()) { + new Thread(new FileDownloader(interactive)).start(); + } else { + changeStatus(STATUS_OFFER); + } + } + + private long retrieveFileSize() throws IOException, + SSLHandshakeException { + changeStatus(STATUS_CHECKING); + HttpURLConnection connection = (HttpURLConnection) mUrl + .openConnection(); + connection.setRequestMethod("HEAD"); + if (connection instanceof HttpsURLConnection) { + setupTrustManager((HttpsURLConnection) connection, interactive); + } + connection.connect(); + String contentLength = connection.getHeaderField("Content-Length"); + if (contentLength == null) { + throw new IOException(); + } + try { + return Long.parseLong(contentLength, 10); + } catch (NumberFormatException e) { + throw new IOException(); + } + } + + } + + private class FileDownloader implements Runnable { + + private boolean interactive = false; + + public FileDownloader(boolean interactive) { + this.interactive = interactive; + } + + @Override + public void run() { + try { + changeStatus(STATUS_DOWNLOADING); + download(); + updateImageBounds(); + finish(); + } catch (SSLHandshakeException e) { + changeStatus(STATUS_OFFER); + } catch (IOException e) { + cancel(); + } + } + + private void download() throws SSLHandshakeException, IOException { + HttpURLConnection connection = (HttpURLConnection) mUrl + .openConnection(); + if (connection instanceof HttpsURLConnection) { + setupTrustManager((HttpsURLConnection) connection, interactive); + } + connection.connect(); + BufferedInputStream is = new BufferedInputStream( + connection.getInputStream()); + OutputStream os = file.createOutputStream(); + int count = -1; + byte[] buffer = new byte[1024]; + while ((count = is.read(buffer)) != -1) { + os.write(buffer, 0, count); + } + os.flush(); + os.close(); + is.close(); + } + + private void updateImageBounds() { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFile(file.getAbsolutePath(), options); + int imageHeight = options.outHeight; + int imageWidth = options.outWidth; + message.setBody(mUrl.toString() + "," + file.getSize() + ',' + + imageWidth + ',' + imageHeight); + message.setType(Message.TYPE_IMAGE); + mXmppConnectionService.updateMessage(message); + } + + } + + @Override + public int getStatus() { + return this.mStatus; + } + + @Override + public long getFileSize() { + if (this.file != null) { + return this.file.getExpectedSize(); + } else { + return 0; + } + } +} \ No newline at end of file diff --git a/conversations/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java b/conversations/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java new file mode 100644 index 00000000..9a2a2405 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java @@ -0,0 +1,28 @@ +package eu.siacs.conversations.http; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.services.AbstractConnectionManager; +import eu.siacs.conversations.services.XmppConnectionService; + +public class HttpConnectionManager extends AbstractConnectionManager { + + public HttpConnectionManager(XmppConnectionService service) { + super(service); + } + + private List connections = new CopyOnWriteArrayList(); + + public HttpConnection createNewConnection(Message message) { + HttpConnection connection = new HttpConnection(this); + connection.init(message); + this.connections.add(connection); + return connection; + } + + public void finishConnection(HttpConnection connection) { + this.connections.remove(connection); + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/parser/AbstractParser.java b/conversations/src/main/java/eu/siacs/conversations/parser/AbstractParser.java new file mode 100644 index 00000000..5541c1c6 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/parser/AbstractParser.java @@ -0,0 +1,92 @@ +package eu.siacs.conversations.parser; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.Locale; + +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xml.Element; + +public abstract class AbstractParser { + + protected XmppConnectionService mXmppConnectionService; + + protected AbstractParser(XmppConnectionService service) { + this.mXmppConnectionService = service; + } + + protected long getTimestamp(Element packet) { + long now = System.currentTimeMillis(); + ArrayList stamps = new ArrayList(); + for (Element child : packet.getChildren()) { + if (child.getName().equals("delay")) { + stamps.add(child.getAttribute("stamp").replace("Z", "+0000")); + } + } + Collections.sort(stamps); + if (stamps.size() >= 1) { + try { + String stamp = stamps.get(stamps.size() - 1); + if (stamp.contains(".")) { + Date date = new SimpleDateFormat( + "yyyy-MM-dd'T'HH:mm:ss.SSSZ", Locale.US) + .parse(stamp); + if (now < date.getTime()) { + return now; + } else { + return date.getTime(); + } + } else { + Date date = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", + Locale.US).parse(stamp); + if (now < date.getTime()) { + return now; + } else { + return date.getTime(); + } + } + } catch (ParseException e) { + return now; + } + } else { + return now; + } + } + + protected void updateLastseen(Element packet, Account account, + boolean presenceOverwrite) { + String[] fromParts = packet.getAttribute("from").split("/", 2); + String from = fromParts[0]; + String presence = null; + if (fromParts.length >= 2) { + presence = fromParts[1]; + } else { + presence = ""; + } + Contact contact = account.getRoster().getContact(from); + long timestamp = getTimestamp(packet); + if (timestamp >= contact.lastseen.time) { + contact.lastseen.time = timestamp; + if ((presence != null) && (presenceOverwrite)) { + contact.lastseen.presence = presence; + } + } + } + + protected String avatarData(Element items) { + Element item = items.findChild("item"); + if (item == null) { + return null; + } + Element data = item.findChild("data", "urn:xmpp:avatar:data"); + if (data == null) { + return null; + } + return data.getContent(); + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/parser/IqParser.java b/conversations/src/main/java/eu/siacs/conversations/parser/IqParser.java new file mode 100644 index 00000000..df6754f2 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/parser/IqParser.java @@ -0,0 +1,92 @@ +package eu.siacs.conversations.parser; + +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.OnIqPacketReceived; +import eu.siacs.conversations.xmpp.stanzas.IqPacket; + +public class IqParser extends AbstractParser implements OnIqPacketReceived { + + public IqParser(XmppConnectionService service) { + super(service); + } + + public void rosterItems(Account account, Element query) { + String version = query.getAttribute("ver"); + if (version != null) { + account.getRoster().setVersion(version); + } + for (Element item : query.getChildren()) { + if (item.getName().equals("item")) { + String jid = item.getAttribute("jid"); + String name = item.getAttribute("name"); + String subscription = item.getAttribute("subscription"); + Contact contact = account.getRoster().getContact(jid); + if (!contact.getOption(Contact.Options.DIRTY_PUSH)) { + contact.setServerName(name); + } + if (subscription != null) { + if (subscription.equals("remove")) { + contact.resetOption(Contact.Options.IN_ROSTER); + contact.resetOption(Contact.Options.DIRTY_DELETE); + contact.resetOption(Contact.Options.PREEMPTIVE_GRANT); + } else { + contact.setOption(Contact.Options.IN_ROSTER); + contact.resetOption(Contact.Options.DIRTY_PUSH); + contact.parseSubscriptionFromElement(item); + } + } + } + } + mXmppConnectionService.updateRosterUi(); + } + + public String avatarData(IqPacket packet) { + Element pubsub = packet.findChild("pubsub", + "http://jabber.org/protocol/pubsub"); + if (pubsub == null) { + return null; + } + Element items = pubsub.findChild("items"); + if (items == null) { + return null; + } + return super.avatarData(items); + } + + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (packet.hasChild("query", "jabber:iq:roster")) { + String from = packet.getFrom(); + if ((from == null) || (from.equals(account.getJid()))) { + Element query = packet.findChild("query"); + this.rosterItems(account, query); + } + } else if (packet.hasChild("open", "http://jabber.org/protocol/ibb") + || packet.hasChild("data", "http://jabber.org/protocol/ibb")) { + mXmppConnectionService.getJingleConnectionManager() + .deliverIbbPacket(account, packet); + } else if (packet.hasChild("query", + "http://jabber.org/protocol/disco#info")) { + IqPacket response = mXmppConnectionService.getIqGenerator() + .discoResponse(packet); + account.getXmppConnection().sendIqPacket(response, null); + } else if (packet.hasChild("ping", "urn:xmpp:ping")) { + IqPacket response = packet.generateRespone(IqPacket.TYPE_RESULT); + mXmppConnectionService.sendIqPacket(account, response, null); + } else { + if ((packet.getType() == IqPacket.TYPE_GET) + || (packet.getType() == IqPacket.TYPE_SET)) { + IqPacket response = packet.generateRespone(IqPacket.TYPE_ERROR); + Element error = response.addChild("error"); + error.setAttribute("type", "cancel"); + error.addChild("feature-not-implemented", + "urn:ietf:params:xml:ns:xmpp-stanzas"); + account.getXmppConnection().sendIqPacket(response, null); + } + } + } + +} diff --git a/conversations/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/conversations/src/main/java/eu/siacs/conversations/parser/MessageParser.java new file mode 100644 index 00000000..b5e14305 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -0,0 +1,517 @@ +package eu.siacs.conversations.parser; + +import net.java.otr4j.session.Session; +import net.java.otr4j.session.SessionStatus; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.services.NotificationService; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.utils.CryptoHelper; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.OnMessagePacketReceived; +import eu.siacs.conversations.xmpp.pep.Avatar; +import eu.siacs.conversations.xmpp.stanzas.MessagePacket; + +public class MessageParser extends AbstractParser implements + OnMessagePacketReceived { + public MessageParser(XmppConnectionService service) { + super(service); + } + + private Message parseChat(MessagePacket packet, Account account) { + String[] fromParts = packet.getFrom().split("/", 2); + Conversation conversation = mXmppConnectionService + .findOrCreateConversation(account, fromParts[0], false); + updateLastseen(packet, account, true); + String pgpBody = getPgpBody(packet); + Message finishedMessage; + if (pgpBody != null) { + finishedMessage = new Message(conversation, packet.getFrom(), + pgpBody, Message.ENCRYPTION_PGP, Message.STATUS_RECEIVED); + } else { + finishedMessage = new Message(conversation, packet.getFrom(), + packet.getBody(), Message.ENCRYPTION_NONE, + Message.STATUS_RECEIVED); + } + finishedMessage.setRemoteMsgId(packet.getId()); + finishedMessage.markable = isMarkable(packet); + if (conversation.getMode() == Conversation.MODE_MULTI + && fromParts.length >= 2) { + finishedMessage.setType(Message.TYPE_PRIVATE); + finishedMessage.setPresence(fromParts[1]); + finishedMessage.setTrueCounterpart(conversation.getMucOptions() + .getTrueCounterpart(fromParts[1])); + if (conversation.hasDuplicateMessage(finishedMessage)) { + return null; + } + + } + finishedMessage.setTime(getTimestamp(packet)); + return finishedMessage; + } + + private Message parseOtrChat(MessagePacket packet, Account account) { + boolean properlyAddressed = (packet.getTo().split("/", 2).length == 2) + || (account.countPresences() == 1); + String[] fromParts = packet.getFrom().split("/", 2); + Conversation conversation = mXmppConnectionService + .findOrCreateConversation(account, fromParts[0], false); + String presence; + if (fromParts.length >= 2) { + presence = fromParts[1]; + } else { + presence = ""; + } + updateLastseen(packet, account, true); + String body = packet.getBody(); + if (body.matches("^\\?OTRv\\d*\\?")) { + conversation.endOtrIfNeeded(); + } + if (!conversation.hasValidOtrSession()) { + if (properlyAddressed) { + conversation.startOtrSession(mXmppConnectionService, presence, + false); + } else { + return null; + } + } else { + String foreignPresence = conversation.getOtrSession() + .getSessionID().getUserID(); + if (!foreignPresence.equals(presence)) { + conversation.endOtrIfNeeded(); + if (properlyAddressed) { + conversation.startOtrSession(mXmppConnectionService, + presence, false); + } else { + return null; + } + } + } + try { + Session otrSession = conversation.getOtrSession(); + SessionStatus before = otrSession.getSessionStatus(); + body = otrSession.transformReceiving(body); + SessionStatus after = otrSession.getSessionStatus(); + if ((before != after) && (after == SessionStatus.ENCRYPTED)) { + mXmppConnectionService.onOtrSessionEstablished(conversation); + } else if ((before != after) && (after == SessionStatus.FINISHED)) { + conversation.resetOtrSession(); + mXmppConnectionService.updateConversationUi(); + } + if ((body == null) || (body.isEmpty())) { + return null; + } + if (body.startsWith(CryptoHelper.FILETRANSFER)) { + String key = body.substring(CryptoHelper.FILETRANSFER.length()); + conversation.setSymmetricKey(CryptoHelper.hexToBytes(key)); + return null; + } + Message finishedMessage = new Message(conversation, + packet.getFrom(), body, Message.ENCRYPTION_OTR, + Message.STATUS_RECEIVED); + finishedMessage.setTime(getTimestamp(packet)); + finishedMessage.setRemoteMsgId(packet.getId()); + finishedMessage.markable = isMarkable(packet); + return finishedMessage; + } catch (Exception e) { + String receivedId = packet.getId(); + if (receivedId != null) { + mXmppConnectionService.replyWithNotAcceptable(account, packet); + } + conversation.resetOtrSession(); + return null; + } + } + + private Message parseGroupchat(MessagePacket packet, Account account) { + int status; + String[] fromParts = packet.getFrom().split("/", 2); + if (mXmppConnectionService.find(account.pendingConferenceLeaves, + account, fromParts[0]) != null) { + return null; + } + Conversation conversation = mXmppConnectionService + .findOrCreateConversation(account, fromParts[0], true); + if (packet.hasChild("subject")) { + conversation.getMucOptions().setSubject( + packet.findChild("subject").getContent()); + mXmppConnectionService.updateConversationUi(); + return null; + } + if ((fromParts.length == 1)) { + return null; + } + String counterPart = fromParts[1]; + if (counterPart.equals(conversation.getMucOptions().getActualNick())) { + if (mXmppConnectionService.markMessage(conversation, + packet.getId(), Message.STATUS_SEND)) { + return null; + } else { + status = Message.STATUS_SEND; + } + } else { + status = Message.STATUS_RECEIVED; + } + String pgpBody = getPgpBody(packet); + Message finishedMessage; + if (pgpBody == null) { + finishedMessage = new Message(conversation, counterPart, + packet.getBody(), Message.ENCRYPTION_NONE, status); + } else { + finishedMessage = new Message(conversation, counterPart, pgpBody, + Message.ENCRYPTION_PGP, status); + } + finishedMessage.setRemoteMsgId(packet.getId()); + finishedMessage.markable = isMarkable(packet); + if (status == Message.STATUS_RECEIVED) { + finishedMessage.setTrueCounterpart(conversation.getMucOptions() + .getTrueCounterpart(counterPart)); + } + if (packet.hasChild("delay") + && conversation.hasDuplicateMessage(finishedMessage)) { + return null; + } + finishedMessage.setTime(getTimestamp(packet)); + return finishedMessage; + } + + private Message parseCarbonMessage(MessagePacket packet, Account account) { + int status; + String fullJid; + Element forwarded; + if (packet.hasChild("received", "urn:xmpp:carbons:2")) { + forwarded = packet.findChild("received", "urn:xmpp:carbons:2") + .findChild("forwarded", "urn:xmpp:forward:0"); + status = Message.STATUS_RECEIVED; + } else if (packet.hasChild("sent", "urn:xmpp:carbons:2")) { + forwarded = packet.findChild("sent", "urn:xmpp:carbons:2") + .findChild("forwarded", "urn:xmpp:forward:0"); + status = Message.STATUS_SEND; + } else { + return null; + } + if (forwarded == null) { + return null; + } + Element message = forwarded.findChild("message"); + if (message == null) { + return null; + } + if (!message.hasChild("body")) { + if (status == Message.STATUS_RECEIVED + && message.getAttribute("from") != null) { + parseNonMessage(message, account); + } else if (status == Message.STATUS_SEND + && message.hasChild("displayed", "urn:xmpp:chat-markers:0")) { + String to = message.getAttribute("to"); + if (to != null) { + Conversation conversation = mXmppConnectionService.find( + mXmppConnectionService.getConversations(), account, + to.split("/")[0]); + if (conversation != null) { + mXmppConnectionService.markRead(conversation, false); + } + } + } + return null; + } + if (status == Message.STATUS_RECEIVED) { + fullJid = message.getAttribute("from"); + if (fullJid == null) { + return null; + } else { + updateLastseen(message, account, true); + } + } else { + fullJid = message.getAttribute("to"); + if (fullJid == null) { + return null; + } + } + String[] parts = fullJid.split("/", 2); + Conversation conversation = mXmppConnectionService + .findOrCreateConversation(account, parts[0], false); + String pgpBody = getPgpBody(message); + Message finishedMessage; + if (pgpBody != null) { + finishedMessage = new Message(conversation, fullJid, pgpBody, + Message.ENCRYPTION_PGP, status); + } else { + String body = message.findChild("body").getContent(); + finishedMessage = new Message(conversation, fullJid, body, + Message.ENCRYPTION_NONE, status); + } + finishedMessage.setTime(getTimestamp(message)); + finishedMessage.setRemoteMsgId(message.getAttribute("id")); + finishedMessage.markable = isMarkable(message); + if (conversation.getMode() == Conversation.MODE_MULTI + && parts.length >= 2) { + finishedMessage.setType(Message.TYPE_PRIVATE); + finishedMessage.setPresence(parts[1]); + finishedMessage.setTrueCounterpart(conversation.getMucOptions() + .getTrueCounterpart(parts[1])); + if (conversation.hasDuplicateMessage(finishedMessage)) { + return null; + } + } + + return finishedMessage; + } + + private void parseError(MessagePacket packet, Account account) { + String[] fromParts = packet.getFrom().split("/", 2); + mXmppConnectionService.markMessage(account, fromParts[0], + packet.getId(), Message.STATUS_SEND_FAILED); + } + + private void parseNonMessage(Element packet, Account account) { + String from = packet.getAttribute("from"); + if (packet.hasChild("event", "http://jabber.org/protocol/pubsub#event")) { + Element event = packet.findChild("event", + "http://jabber.org/protocol/pubsub#event"); + parseEvent(event, packet.getAttribute("from"), account); + } else if (from != null + && packet.hasChild("displayed", "urn:xmpp:chat-markers:0")) { + String id = packet + .findChild("displayed", "urn:xmpp:chat-markers:0") + .getAttribute("id"); + updateLastseen(packet, account, true); + mXmppConnectionService.markMessage(account, from.split("/", 2)[0], + id, Message.STATUS_SEND_DISPLAYED); + } else if (from != null + && packet.hasChild("received", "urn:xmpp:chat-markers:0")) { + String id = packet.findChild("received", "urn:xmpp:chat-markers:0") + .getAttribute("id"); + updateLastseen(packet, account, false); + mXmppConnectionService.markMessage(account, from.split("/", 2)[0], + id, Message.STATUS_SEND_RECEIVED); + } else if (from != null + && packet.hasChild("received", "urn:xmpp:receipts")) { + String id = packet.findChild("received", "urn:xmpp:receipts") + .getAttribute("id"); + updateLastseen(packet, account, false); + mXmppConnectionService.markMessage(account, from.split("/", 2)[0], + id, Message.STATUS_SEND_RECEIVED); + } else if (packet.hasChild("x", "http://jabber.org/protocol/muc#user")) { + Element x = packet.findChild("x", + "http://jabber.org/protocol/muc#user"); + if (x.hasChild("invite")) { + Conversation conversation = mXmppConnectionService + .findOrCreateConversation(account, + packet.getAttribute("from"), true); + if (!conversation.getMucOptions().online()) { + if (x.hasChild("password")) { + Element password = x.findChild("password"); + conversation.getMucOptions().setPassword( + password.getContent()); + mXmppConnectionService.databaseBackend + .updateConversation(conversation); + } + mXmppConnectionService.joinMuc(conversation); + mXmppConnectionService.updateConversationUi(); + } + } + } else if (packet.hasChild("x", "jabber:x:conference")) { + Element x = packet.findChild("x", "jabber:x:conference"); + String jid = x.getAttribute("jid"); + String password = x.getAttribute("password"); + if (jid != null) { + Conversation conversation = mXmppConnectionService + .findOrCreateConversation(account, jid, true); + if (!conversation.getMucOptions().online()) { + if (password != null) { + conversation.getMucOptions().setPassword(password); + mXmppConnectionService.databaseBackend + .updateConversation(conversation); + } + mXmppConnectionService.joinMuc(conversation); + mXmppConnectionService.updateConversationUi(); + } + } + } + } + + private void parseEvent(Element event, String from, Account account) { + Element items = event.findChild("items"); + String node = items.getAttribute("node"); + if (node != null) { + if (node.equals("urn:xmpp:avatar:metadata")) { + Avatar avatar = Avatar.parseMetadata(items); + if (avatar != null) { + avatar.owner = from; + if (mXmppConnectionService.getFileBackend().isAvatarCached( + avatar)) { + if (account.getJid().equals(from)) { + if (account.setAvatar(avatar.getFilename())) { + mXmppConnectionService.databaseBackend + .updateAccount(account); + } + mXmppConnectionService.getAvatarService().clear( + account); + mXmppConnectionService.updateConversationUi(); + mXmppConnectionService.updateAccountUi(); + } else { + Contact contact = account.getRoster().getContact( + from); + contact.setAvatar(avatar.getFilename()); + mXmppConnectionService.getAvatarService().clear( + contact); + mXmppConnectionService.updateConversationUi(); + mXmppConnectionService.updateRosterUi(); + } + } else { + mXmppConnectionService.fetchAvatar(account, avatar); + } + } + } else if (node.equals("http://jabber.org/protocol/nick")) { + Element item = items.findChild("item"); + if (item != null) { + Element nick = item.findChild("nick", + "http://jabber.org/protocol/nick"); + if (nick != null) { + if (from != null) { + Contact contact = account.getRoster().getContact( + from); + contact.setPresenceName(nick.getContent()); + } + } + } + } + } + } + + private String getPgpBody(Element message) { + Element child = message.findChild("x", "jabber:x:encrypted"); + if (child == null) { + return null; + } else { + return child.getContent(); + } + } + + private boolean isMarkable(Element message) { + return message.hasChild("markable", "urn:xmpp:chat-markers:0"); + } + + @Override + public void onMessagePacketReceived(Account account, MessagePacket packet) { + Message message = null; + boolean notify = mXmppConnectionService.getPreferences().getBoolean( + "show_notification", true); + boolean alwaysNotifyInConference = notify + && mXmppConnectionService.getPreferences().getBoolean( + "always_notify_in_conference", false); + + this.parseNick(packet, account); + + if ((packet.getType() == MessagePacket.TYPE_CHAT || packet.getType() == MessagePacket.TYPE_NORMAL)) { + if ((packet.getBody() != null) + && (packet.getBody().startsWith("?OTR"))) { + message = this.parseOtrChat(packet, account); + if (message != null) { + message.markUnread(); + } + } else if (packet.hasChild("body") + && !(packet.hasChild("x", + "http://jabber.org/protocol/muc#user"))) { + message = this.parseChat(packet, account); + if (message != null) { + message.markUnread(); + } + } else if (packet.hasChild("received", "urn:xmpp:carbons:2") + || (packet.hasChild("sent", "urn:xmpp:carbons:2"))) { + message = this.parseCarbonMessage(packet, account); + if (message != null) { + if (message.getStatus() == Message.STATUS_SEND) { + account.activateGracePeriod(); + notify = false; + mXmppConnectionService.markRead( + message.getConversation(), false); + } else { + message.markUnread(); + } + } + } else { + parseNonMessage(packet, account); + } + } else if (packet.getType() == MessagePacket.TYPE_GROUPCHAT) { + message = this.parseGroupchat(packet, account); + if (message != null) { + if (message.getStatus() == Message.STATUS_RECEIVED) { + message.markUnread(); + notify = alwaysNotifyInConference + || NotificationService + .wasHighlightedOrPrivate(message); + } else { + mXmppConnectionService.markRead(message.getConversation(), + false); + account.activateGracePeriod(); + notify = false; + } + } + } else if (packet.getType() == MessagePacket.TYPE_ERROR) { + this.parseError(packet, account); + return; + } else if (packet.getType() == MessagePacket.TYPE_HEADLINE) { + this.parseHeadline(packet, account); + return; + } + if ((message == null) || (message.getBody() == null)) { + return; + } + if ((mXmppConnectionService.confirmMessages()) + && ((packet.getId() != null))) { + if (packet.hasChild("markable", "urn:xmpp:chat-markers:0")) { + MessagePacket receipt = mXmppConnectionService + .getMessageGenerator().received(account, packet, + "urn:xmpp:chat-markers:0"); + mXmppConnectionService.sendMessagePacket(account, receipt); + } + if (packet.hasChild("request", "urn:xmpp:receipts")) { + MessagePacket receipt = mXmppConnectionService + .getMessageGenerator().received(account, packet, + "urn:xmpp:receipts"); + mXmppConnectionService.sendMessagePacket(account, receipt); + } + } + Conversation conversation = message.getConversation(); + conversation.add(message); + if (packet.getType() != MessagePacket.TYPE_ERROR) { + if (message.getEncryption() == Message.ENCRYPTION_NONE + || mXmppConnectionService.saveEncryptedMessages()) { + mXmppConnectionService.databaseBackend.createMessage(message); + } + } + if (message.bodyContainsDownloadable()) { + this.mXmppConnectionService.getHttpConnectionManager() + .createNewConnection(message); + } + notify = notify && !conversation.isMuted(); + if (notify) { + mXmppConnectionService.getNotificationService().push(message); + } + mXmppConnectionService.updateConversationUi(); + } + + private void parseHeadline(MessagePacket packet, Account account) { + if (packet.hasChild("event", "http://jabber.org/protocol/pubsub#event")) { + Element event = packet.findChild("event", + "http://jabber.org/protocol/pubsub#event"); + parseEvent(event, packet.getFrom(), account); + } + } + + private void parseNick(MessagePacket packet, Account account) { + Element nick = packet.findChild("nick", + "http://jabber.org/protocol/nick"); + if (nick != null) { + if (packet.getFrom() != null) { + Contact contact = account.getRoster().getContact( + packet.getFrom()); + contact.setPresenceName(nick.getContent()); + } + } + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/parser/PresenceParser.java b/conversations/src/main/java/eu/siacs/conversations/parser/PresenceParser.java new file mode 100644 index 00000000..4e90cda8 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/parser/PresenceParser.java @@ -0,0 +1,133 @@ +package eu.siacs.conversations.parser; + +import eu.siacs.conversations.crypto.PgpEngine; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Presences; +import eu.siacs.conversations.generator.PresenceGenerator; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.OnPresencePacketReceived; +import eu.siacs.conversations.xmpp.stanzas.PresencePacket; + +public class PresenceParser extends AbstractParser implements + OnPresencePacketReceived { + + public PresenceParser(XmppConnectionService service) { + super(service); + } + + public void parseConferencePresence(PresencePacket packet, Account account) { + PgpEngine mPgpEngine = mXmppConnectionService.getPgpEngine(); + if (packet.hasChild("x", "http://jabber.org/protocol/muc#user")) { + Conversation muc = mXmppConnectionService.find(account, packet + .getAttribute("from").split("/", 2)[0]); + if (muc != null) { + boolean before = muc.getMucOptions().online(); + muc.getMucOptions().processPacket(packet, mPgpEngine); + if (before != muc.getMucOptions().online()) { + mXmppConnectionService.updateConversationUi(); + } + mXmppConnectionService.getAvatarService().clear(muc); + } + } else if (packet.hasChild("x", "http://jabber.org/protocol/muc")) { + Conversation muc = mXmppConnectionService.find(account, packet + .getAttribute("from").split("/", 2)[0]); + if (muc != null) { + boolean before = muc.getMucOptions().online(); + muc.getMucOptions().processPacket(packet, mPgpEngine); + if (before != muc.getMucOptions().online()) { + mXmppConnectionService.updateConversationUi(); + } + mXmppConnectionService.getAvatarService().clear(muc); + } + } + } + + public void parseContactPresence(PresencePacket packet, Account account) { + PresenceGenerator mPresenceGenerator = mXmppConnectionService + .getPresenceGenerator(); + if (packet.getFrom() == null) { + return; + } + String[] fromParts = packet.getFrom().split("/", 2); + String type = packet.getAttribute("type"); + if (fromParts[0].equals(account.getJid())) { + if (fromParts.length == 2) { + if (type == null) { + account.updatePresence(fromParts[1], + Presences.parseShow(packet.findChild("show"))); + } else if (type.equals("unavailable")) { + account.removePresence(fromParts[1]); + account.deactivateGracePeriod(); + } + } + } else { + Contact contact = account.getRoster().getContact(packet.getFrom()); + if (type == null) { + String presence; + if (fromParts.length >= 2) { + presence = fromParts[1]; + } else { + presence = ""; + } + int sizeBefore = contact.getPresences().size(); + contact.updatePresence(presence, + Presences.parseShow(packet.findChild("show"))); + PgpEngine pgp = mXmppConnectionService.getPgpEngine(); + if (pgp != null) { + Element x = packet.findChild("x", "jabber:x:signed"); + if (x != null) { + Element status = packet.findChild("status"); + String msg; + if (status != null) { + msg = status.getContent(); + } else { + msg = ""; + } + contact.setPgpKeyId(pgp.fetchKeyId(account, msg, + x.getContent())); + } + } + boolean online = sizeBefore < contact.getPresences().size(); + updateLastseen(packet, account, true); + mXmppConnectionService.onContactStatusChanged + .onContactStatusChanged(contact, online); + } else if (type.equals("unavailable")) { + if (fromParts.length != 2) { + contact.clearPresences(); + } else { + contact.removePresence(fromParts[1]); + } + mXmppConnectionService.onContactStatusChanged + .onContactStatusChanged(contact, false); + } else if (type.equals("subscribe")) { + if (contact.getOption(Contact.Options.PREEMPTIVE_GRANT)) { + mXmppConnectionService.sendPresencePacket(account, + mPresenceGenerator.sendPresenceUpdatesTo(contact)); + } else { + contact.setOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST); + } + } + Element nick = packet.findChild("nick", + "http://jabber.org/protocol/nick"); + if (nick != null) { + contact.setPresenceName(nick.getContent()); + } + } + mXmppConnectionService.updateRosterUi(); + } + + @Override + public void onPresencePacketReceived(Account account, PresencePacket packet) { + if (packet.hasChild("x", "http://jabber.org/protocol/muc#user")) { + this.parseConferencePresence(packet, account); + } else if (packet.hasChild("x", "http://jabber.org/protocol/muc")) { + this.parseConferencePresence(packet, account); + } else { + this.parseContactPresence(packet, account); + } + } + +} diff --git a/conversations/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/conversations/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java new file mode 100644 index 00000000..b49cf4e6 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -0,0 +1,335 @@ +package eu.siacs.conversations.persistance; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.entities.Roster; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteCantOpenDatabaseException; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; + +public class DatabaseBackend extends SQLiteOpenHelper { + + private static DatabaseBackend instance = null; + + private static final String DATABASE_NAME = "history"; + private static final int DATABASE_VERSION = 8; + + private static String CREATE_CONTATCS_STATEMENT = "create table " + + Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, " + + Contact.SERVERNAME + " TEXT, " + Contact.SYSTEMNAME + " TEXT," + + Contact.JID + " TEXT," + Contact.KEYS + " TEXT," + + Contact.PHOTOURI + " TEXT," + Contact.OPTIONS + " NUMBER," + + Contact.SYSTEMACCOUNT + " NUMBER, " + Contact.AVATAR + " TEXT, " + + "FOREIGN KEY(" + Contact.ACCOUNT + ") REFERENCES " + + Account.TABLENAME + "(" + Account.UUID + + ") ON DELETE CASCADE, UNIQUE(" + Contact.ACCOUNT + ", " + + Contact.JID + ") ON CONFLICT REPLACE);"; + + private DatabaseBackend(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL("PRAGMA foreign_keys=ON;"); + db.execSQL("create table " + Account.TABLENAME + "(" + Account.UUID + + " TEXT PRIMARY KEY," + Account.USERNAME + " TEXT," + + Account.SERVER + " TEXT," + Account.PASSWORD + " TEXT," + + Account.ROSTERVERSION + " TEXT," + Account.OPTIONS + + " NUMBER, " + Account.AVATAR + " TEXT, " + Account.KEYS + + " TEXT)"); + db.execSQL("create table " + Conversation.TABLENAME + " (" + + Conversation.UUID + " TEXT PRIMARY KEY, " + Conversation.NAME + + " TEXT, " + Conversation.CONTACT + " TEXT, " + + Conversation.ACCOUNT + " TEXT, " + Conversation.CONTACTJID + + " TEXT, " + Conversation.CREATED + " NUMBER, " + + Conversation.STATUS + " NUMBER, " + Conversation.MODE + + " NUMBER, " + Conversation.ATTRIBUTES + " TEXT, FOREIGN KEY(" + + Conversation.ACCOUNT + ") REFERENCES " + Account.TABLENAME + + "(" + Account.UUID + ") ON DELETE CASCADE);"); + db.execSQL("create table " + Message.TABLENAME + "( " + Message.UUID + + " TEXT PRIMARY KEY, " + Message.CONVERSATION + " TEXT, " + + Message.TIME_SENT + " NUMBER, " + Message.COUNTERPART + + " TEXT, " + Message.TRUE_COUNTERPART + " TEXT," + + Message.BODY + " TEXT, " + Message.ENCRYPTION + " NUMBER, " + + Message.STATUS + " NUMBER," + Message.TYPE + " NUMBER, " + + Message.REMOTE_MSG_ID + " TEXT, FOREIGN KEY(" + + Message.CONVERSATION + ") REFERENCES " + + Conversation.TABLENAME + "(" + Conversation.UUID + + ") ON DELETE CASCADE);"); + + db.execSQL(CREATE_CONTATCS_STATEMENT); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + if (oldVersion < 2 && newVersion >= 2) { + db.execSQL("update " + Account.TABLENAME + " set " + + Account.OPTIONS + " = " + Account.OPTIONS + " | 8"); + } + if (oldVersion < 3 && newVersion >= 3) { + db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + + Message.TYPE + " NUMBER"); + } + if (oldVersion < 5 && newVersion >= 5) { + db.execSQL("DROP TABLE " + Contact.TABLENAME); + db.execSQL(CREATE_CONTATCS_STATEMENT); + db.execSQL("UPDATE " + Account.TABLENAME + " SET " + + Account.ROSTERVERSION + " = NULL"); + } + if (oldVersion < 6 && newVersion >= 6) { + db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + + Message.TRUE_COUNTERPART + " TEXT"); + } + if (oldVersion < 7 && newVersion >= 7) { + db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + + Message.REMOTE_MSG_ID + " TEXT"); + db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " + + Contact.AVATAR + " TEXT"); + db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + + Account.AVATAR + " TEXT"); + } + if (oldVersion < 8 && newVersion >= 8) { + db.execSQL("ALTER TABLE " + Conversation.TABLENAME + " ADD COLUMN " + + Conversation.ATTRIBUTES + " TEXT"); + } + } + + public static synchronized DatabaseBackend getInstance(Context context) { + if (instance == null) { + instance = new DatabaseBackend(context); + } + return instance; + } + + public void createConversation(Conversation conversation) { + SQLiteDatabase db = this.getWritableDatabase(); + db.insert(Conversation.TABLENAME, null, conversation.getContentValues()); + } + + public void createMessage(Message message) { + SQLiteDatabase db = this.getWritableDatabase(); + db.insert(Message.TABLENAME, null, message.getContentValues()); + } + + public void createAccount(Account account) { + SQLiteDatabase db = this.getWritableDatabase(); + db.insert(Account.TABLENAME, null, account.getContentValues()); + } + + public void createContact(Contact contact) { + SQLiteDatabase db = this.getWritableDatabase(); + db.insert(Contact.TABLENAME, null, contact.getContentValues()); + } + + public int getConversationCount() { + SQLiteDatabase db = this.getReadableDatabase(); + Cursor cursor = db.rawQuery("select count(uuid) as count from " + + Conversation.TABLENAME + " where " + Conversation.STATUS + + "=" + Conversation.STATUS_AVAILABLE, null); + cursor.moveToFirst(); + return cursor.getInt(0); + } + + public CopyOnWriteArrayList getConversations(int status) { + CopyOnWriteArrayList list = new CopyOnWriteArrayList(); + SQLiteDatabase db = this.getReadableDatabase(); + String[] selectionArgs = { Integer.toString(status) }; + Cursor cursor = db.rawQuery("select * from " + Conversation.TABLENAME + + " where " + Conversation.STATUS + " = ? order by " + + Conversation.CREATED + " desc", selectionArgs); + while (cursor.moveToNext()) { + list.add(Conversation.fromCursor(cursor)); + } + return list; + } + + public ArrayList getMessages(Conversation conversations, int limit) { + return getMessages(conversations, limit, -1); + } + + public ArrayList getMessages(Conversation conversation, int limit, + long timestamp) { + ArrayList list = new ArrayList(); + SQLiteDatabase db = this.getReadableDatabase(); + Cursor cursor; + if (timestamp == -1) { + String[] selectionArgs = { conversation.getUuid() }; + cursor = db.query(Message.TABLENAME, null, Message.CONVERSATION + + "=?", selectionArgs, null, null, Message.TIME_SENT + + " DESC", String.valueOf(limit)); + } else { + String[] selectionArgs = { conversation.getUuid(), + Long.toString(timestamp) }; + cursor = db.query(Message.TABLENAME, null, Message.CONVERSATION + + "=? and " + Message.TIME_SENT + " 0) { + cursor.moveToLast(); + do { + Message message = Message.fromCursor(cursor); + message.setConversation(conversation); + list.add(message); + } while (cursor.moveToPrevious()); + } + return list; + } + + public Conversation findConversation(Account account, String contactJid) { + SQLiteDatabase db = this.getReadableDatabase(); + String[] selectionArgs = { account.getUuid(), contactJid + "%" }; + Cursor cursor = db.query(Conversation.TABLENAME, null, + Conversation.ACCOUNT + "=? AND " + Conversation.CONTACTJID + + " like ?", selectionArgs, null, null, null); + if (cursor.getCount() == 0) + return null; + cursor.moveToFirst(); + return Conversation.fromCursor(cursor); + } + + public void updateConversation(Conversation conversation) { + SQLiteDatabase db = this.getWritableDatabase(); + String[] args = { conversation.getUuid() }; + db.update(Conversation.TABLENAME, conversation.getContentValues(), + Conversation.UUID + "=?", args); + } + + public List getAccounts() { + List list = new ArrayList(); + SQLiteDatabase db = this.getReadableDatabase(); + Cursor cursor = db.query(Account.TABLENAME, null, null, null, null, + null, null); + while (cursor.moveToNext()) { + list.add(Account.fromCursor(cursor)); + } + cursor.close(); + return list; + } + + public void updateAccount(Account account) { + SQLiteDatabase db = this.getWritableDatabase(); + String[] args = { account.getUuid() }; + db.update(Account.TABLENAME, account.getContentValues(), Account.UUID + + "=?", args); + } + + public void deleteAccount(Account account) { + SQLiteDatabase db = this.getWritableDatabase(); + String[] args = { account.getUuid() }; + db.delete(Account.TABLENAME, Account.UUID + "=?", args); + } + + public boolean hasEnabledAccounts() { + SQLiteDatabase db = this.getReadableDatabase(); + Cursor cursor = db.rawQuery("select count(" + Account.UUID + ") from " + + Account.TABLENAME + " where not options & (1 <<1)", null); + try { + cursor.moveToFirst(); + int count = cursor.getInt(0); + cursor.close(); + return (count > 0); + } catch (SQLiteCantOpenDatabaseException e) { + return true; // better safe than sorry + } + } + + @Override + public SQLiteDatabase getWritableDatabase() { + SQLiteDatabase db = super.getWritableDatabase(); + db.execSQL("PRAGMA foreign_keys=ON;"); + return db; + } + + public void updateMessage(Message message) { + SQLiteDatabase db = this.getWritableDatabase(); + String[] args = { message.getUuid() }; + db.update(Message.TABLENAME, message.getContentValues(), Message.UUID + + "=?", args); + } + + public void readRoster(Roster roster) { + SQLiteDatabase db = this.getReadableDatabase(); + Cursor cursor; + String args[] = { roster.getAccount().getUuid() }; + cursor = db.query(Contact.TABLENAME, null, Contact.ACCOUNT + "=?", + args, null, null, null); + while (cursor.moveToNext()) { + roster.initContact(Contact.fromCursor(cursor)); + } + cursor.close(); + } + + public void writeRoster(Roster roster) { + Account account = roster.getAccount(); + SQLiteDatabase db = this.getWritableDatabase(); + for (Contact contact : roster.getContacts()) { + if (contact.getOption(Contact.Options.IN_ROSTER)) { + db.insert(Contact.TABLENAME, null, contact.getContentValues()); + } else { + String where = Contact.ACCOUNT + "=? AND " + Contact.JID + "=?"; + String[] whereArgs = { account.getUuid(), contact.getJid() }; + db.delete(Contact.TABLENAME, where, whereArgs); + } + } + account.setRosterVersion(roster.getVersion()); + updateAccount(account); + } + + public void deleteMessage(Message message) { + SQLiteDatabase db = this.getWritableDatabase(); + String[] args = { message.getUuid() }; + db.delete(Message.TABLENAME, Message.UUID + "=?", args); + } + + public void deleteMessagesInConversation(Conversation conversation) { + SQLiteDatabase db = this.getWritableDatabase(); + String[] args = { conversation.getUuid() }; + db.delete(Message.TABLENAME, Message.CONVERSATION + "=?", args); + } + + public Conversation findConversationByUuid(String conversationUuid) { + SQLiteDatabase db = this.getReadableDatabase(); + String[] selectionArgs = { conversationUuid }; + Cursor cursor = db.query(Conversation.TABLENAME, null, + Conversation.UUID + "=?", selectionArgs, null, null, null); + if (cursor.getCount() == 0) { + return null; + } + cursor.moveToFirst(); + return Conversation.fromCursor(cursor); + } + + public Message findMessageByUuid(String messageUuid) { + SQLiteDatabase db = this.getReadableDatabase(); + String[] selectionArgs = { messageUuid }; + Cursor cursor = db.query(Message.TABLENAME, null, Message.UUID + "=?", + selectionArgs, null, null, null); + if (cursor.getCount() == 0) { + return null; + } + cursor.moveToFirst(); + return Message.fromCursor(cursor); + } + + public Account findAccountByUuid(String accountUuid) { + SQLiteDatabase db = this.getReadableDatabase(); + String[] selectionArgs = { accountUuid }; + Cursor cursor = db.query(Account.TABLENAME, null, Account.UUID + "=?", + selectionArgs, null, null, null); + if (cursor.getCount() == 0) { + return null; + } + cursor.moveToFirst(); + return Account.fromCursor(cursor); + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/conversations/src/main/java/eu/siacs/conversations/persistance/FileBackend.java new file mode 100644 index 00000000..b891e9ef --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -0,0 +1,480 @@ +package eu.siacs.conversations.persistance; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.DigestOutputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.RectF; +import android.media.ExifInterface; +import android.net.Uri; +import android.os.Environment; +import android.provider.MediaStore; +import android.util.Base64; +import android.util.Base64OutputStream; +import android.util.Log; +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.DownloadableFile; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.utils.CryptoHelper; +import eu.siacs.conversations.xmpp.pep.Avatar; + +public class FileBackend { + + private static int IMAGE_SIZE = 1920; + + private SimpleDateFormat imageDateFormat = new SimpleDateFormat( + "yyyyMMdd_HHmmssSSS", Locale.US); + + private XmppConnectionService mXmppConnectionService; + + public FileBackend(XmppConnectionService service) { + this.mXmppConnectionService = service; + } + + public DownloadableFile getFile(Message message) { + return getFile(message, true); + } + + public DownloadableFile getFile(Message message, boolean decrypted) { + StringBuilder filename = new StringBuilder(); + filename.append(getConversationsDirectory()); + filename.append(message.getUuid()); + if ((decrypted) || (message.getEncryption() == Message.ENCRYPTION_NONE)) { + filename.append(".webp"); + } else { + if (message.getEncryption() == Message.ENCRYPTION_OTR) { + filename.append(".webp"); + } else { + filename.append(".webp.pgp"); + } + } + return new DownloadableFile(filename.toString()); + } + + public static String getConversationsDirectory() { + return Environment.getExternalStoragePublicDirectory( + Environment.DIRECTORY_PICTURES).getAbsolutePath() + + "/Conversations/"; + } + + public Bitmap resize(Bitmap originalBitmap, int size) { + int w = originalBitmap.getWidth(); + int h = originalBitmap.getHeight(); + if (Math.max(w, h) > size) { + int scalledW; + int scalledH; + if (w <= h) { + scalledW = (int) (w / ((double) h / size)); + scalledH = size; + } else { + scalledW = size; + scalledH = (int) (h / ((double) w / size)); + } + Bitmap scalledBitmap = Bitmap.createScaledBitmap(originalBitmap, + scalledW, scalledH, true); + return scalledBitmap; + } else { + return originalBitmap; + } + } + + public Bitmap rotate(Bitmap bitmap, int degree) { + int w = bitmap.getWidth(); + int h = bitmap.getHeight(); + Matrix mtx = new Matrix(); + mtx.postRotate(degree); + return Bitmap.createBitmap(bitmap, 0, 0, w, h, mtx, true); + } + + public DownloadableFile copyImageToPrivateStorage(Message message, Uri image) + throws ImageCopyException { + return this.copyImageToPrivateStorage(message, image, 0); + } + + private DownloadableFile copyImageToPrivateStorage(Message message, + Uri image, int sampleSize) throws ImageCopyException { + try { + InputStream is = mXmppConnectionService.getContentResolver() + .openInputStream(image); + DownloadableFile file = getFile(message); + file.getParentFile().mkdirs(); + file.createNewFile(); + Bitmap originalBitmap; + BitmapFactory.Options options = new BitmapFactory.Options(); + int inSampleSize = (int) Math.pow(2, sampleSize); + Log.d(Config.LOGTAG, "reading bitmap with sample size " + + inSampleSize); + options.inSampleSize = inSampleSize; + originalBitmap = BitmapFactory.decodeStream(is, null, options); + is.close(); + if (originalBitmap == null) { + throw new ImageCopyException(R.string.error_not_an_image_file); + } + Bitmap scalledBitmap = resize(originalBitmap, IMAGE_SIZE); + originalBitmap = null; + int rotation = getRotation(image); + if (rotation > 0) { + scalledBitmap = rotate(scalledBitmap, rotation); + } + OutputStream os = new FileOutputStream(file); + boolean success = scalledBitmap.compress( + Bitmap.CompressFormat.WEBP, 75, os); + if (!success) { + throw new ImageCopyException(R.string.error_compressing_image); + } + os.flush(); + os.close(); + long size = file.getSize(); + int width = scalledBitmap.getWidth(); + int height = scalledBitmap.getHeight(); + message.setBody(Long.toString(size) + ',' + width + ',' + height); + return file; + } catch (FileNotFoundException e) { + throw new ImageCopyException(R.string.error_file_not_found); + } catch (IOException e) { + throw new ImageCopyException(R.string.error_io_exception); + } catch (SecurityException e) { + throw new ImageCopyException( + R.string.error_security_exception_during_image_copy); + } catch (OutOfMemoryError e) { + ++sampleSize; + if (sampleSize <= 3) { + return copyImageToPrivateStorage(message, image, sampleSize); + } else { + throw new ImageCopyException(R.string.error_out_of_memory); + } + } + } + + private int getRotation(Uri image) { + if ("content".equals(image.getScheme())) { + try { + Cursor cursor = mXmppConnectionService + .getContentResolver() + .query(image, + new String[] { MediaStore.Images.ImageColumns.ORIENTATION }, + null, null, null); + if (cursor.getCount() != 1) { + return -1; + } + cursor.moveToFirst(); + return cursor.getInt(0); + } catch (IllegalArgumentException e) { + return -1; + } + } else { + ExifInterface exif; + try { + exif = new ExifInterface(image.toString()); + if (exif.getAttribute(ExifInterface.TAG_ORIENTATION) + .equalsIgnoreCase("6")) { + return 90; + } else if (exif.getAttribute(ExifInterface.TAG_ORIENTATION) + .equalsIgnoreCase("8")) { + return 270; + } else if (exif.getAttribute(ExifInterface.TAG_ORIENTATION) + .equalsIgnoreCase("3")) { + return 180; + } else { + return 0; + } + } catch (IOException e) { + return -1; + } + } + } + + public Bitmap getImageFromMessage(Message message) { + return BitmapFactory.decodeFile(getFile(message).getAbsolutePath()); + } + + public Bitmap getThumbnail(Message message, int size, boolean cacheOnly) + throws FileNotFoundException { + Bitmap thumbnail = mXmppConnectionService.getBitmapCache().get( + message.getUuid()); + if ((thumbnail == null) && (!cacheOnly)) { + File file = getFile(message); + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inSampleSize = calcSampleSize(file, size); + Bitmap fullsize = BitmapFactory.decodeFile(file.getAbsolutePath(), + options); + if (fullsize == null) { + throw new FileNotFoundException(); + } + thumbnail = resize(fullsize, size); + this.mXmppConnectionService.getBitmapCache().put(message.getUuid(), + thumbnail); + } + return thumbnail; + } + + public void removeFiles(Conversation conversation) { + String prefix = mXmppConnectionService.getFilesDir().getAbsolutePath(); + String path = prefix + "/" + conversation.getAccount().getJid() + "/" + + conversation.getContactJid(); + File file = new File(path); + try { + this.deleteFile(file); + } catch (IOException e) { + Log.d(Config.LOGTAG, + "error deleting file: " + file.getAbsolutePath()); + } + } + + private void deleteFile(File f) throws IOException { + if (f.isDirectory()) { + for (File c : f.listFiles()) + deleteFile(c); + } + f.delete(); + } + + public Uri getTakePhotoUri() { + StringBuilder pathBuilder = new StringBuilder(); + pathBuilder.append(Environment + .getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM)); + pathBuilder.append('/'); + pathBuilder.append("Camera"); + pathBuilder.append('/'); + pathBuilder.append("IMG_" + this.imageDateFormat.format(new Date()) + + ".jpg"); + Uri uri = Uri.parse("file://" + pathBuilder.toString()); + File file = new File(uri.toString()); + file.getParentFile().mkdirs(); + return uri; + } + + public Avatar getPepAvatar(Uri image, int size, Bitmap.CompressFormat format) { + try { + Avatar avatar = new Avatar(); + Bitmap bm = cropCenterSquare(image, size); + if (bm == null) { + return null; + } + ByteArrayOutputStream mByteArrayOutputStream = new ByteArrayOutputStream(); + Base64OutputStream mBase64OutputSttream = new Base64OutputStream( + mByteArrayOutputStream, Base64.DEFAULT); + MessageDigest digest = MessageDigest.getInstance("SHA-1"); + DigestOutputStream mDigestOutputStream = new DigestOutputStream( + mBase64OutputSttream, digest); + if (!bm.compress(format, 75, mDigestOutputStream)) { + return null; + } + mDigestOutputStream.flush(); + mDigestOutputStream.close(); + avatar.sha1sum = CryptoHelper.bytesToHex(digest.digest()); + avatar.image = new String(mByteArrayOutputStream.toByteArray()); + return avatar; + } catch (NoSuchAlgorithmException e) { + return null; + } catch (IOException e) { + return null; + } + } + + public boolean isAvatarCached(Avatar avatar) { + File file = new File(getAvatarPath(avatar.getFilename())); + return file.exists(); + } + + public boolean save(Avatar avatar) { + if (isAvatarCached(avatar)) { + return true; + } + String filename = getAvatarPath(avatar.getFilename()); + File file = new File(filename + ".tmp"); + file.getParentFile().mkdirs(); + try { + file.createNewFile(); + FileOutputStream mFileOutputStream = new FileOutputStream(file); + MessageDigest digest = MessageDigest.getInstance("SHA-1"); + digest.reset(); + DigestOutputStream mDigestOutputStream = new DigestOutputStream( + mFileOutputStream, digest); + mDigestOutputStream.write(avatar.getImageAsBytes()); + mDigestOutputStream.flush(); + mDigestOutputStream.close(); + avatar.size = file.length(); + String sha1sum = CryptoHelper.bytesToHex(digest.digest()); + if (sha1sum.equals(avatar.sha1sum)) { + file.renameTo(new File(filename)); + return true; + } else { + Log.d(Config.LOGTAG, "sha1sum mismatch for " + avatar.owner); + file.delete(); + return false; + } + } catch (FileNotFoundException e) { + return false; + } catch (IOException e) { + return false; + } catch (NoSuchAlgorithmException e) { + return false; + } + } + + public String getAvatarPath(String avatar) { + return mXmppConnectionService.getFilesDir().getAbsolutePath() + + "/avatars/" + avatar; + } + + public Uri getAvatarUri(String avatar) { + return Uri.parse("file:" + getAvatarPath(avatar)); + } + + public Bitmap cropCenterSquare(Uri image, int size) { + try { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inSampleSize = calcSampleSize(image, size); + InputStream is = mXmppConnectionService.getContentResolver() + .openInputStream(image); + Bitmap input = BitmapFactory.decodeStream(is, null, options); + if (input == null) { + return null; + } else { + int rotation = getRotation(image); + if (rotation > 0) { + input = rotate(input, rotation); + } + return cropCenterSquare(input, size); + } + } catch (FileNotFoundException e) { + return null; + } + } + + public Bitmap cropCenter(Uri image, int newHeight, int newWidth) { + try { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inSampleSize = calcSampleSize(image, + Math.max(newHeight, newWidth)); + InputStream is = mXmppConnectionService.getContentResolver() + .openInputStream(image); + Bitmap source = BitmapFactory.decodeStream(is, null, options); + + int sourceWidth = source.getWidth(); + int sourceHeight = source.getHeight(); + float xScale = (float) newWidth / sourceWidth; + float yScale = (float) newHeight / sourceHeight; + float scale = Math.max(xScale, yScale); + float scaledWidth = scale * sourceWidth; + float scaledHeight = scale * sourceHeight; + float left = (newWidth - scaledWidth) / 2; + float top = (newHeight - scaledHeight) / 2; + + RectF targetRect = new RectF(left, top, left + scaledWidth, top + + scaledHeight); + Bitmap dest = Bitmap.createBitmap(newWidth, newHeight, + source.getConfig()); + Canvas canvas = new Canvas(dest); + canvas.drawBitmap(source, null, targetRect, null); + + return dest; + } catch (FileNotFoundException e) { + return null; + } + + } + + public Bitmap cropCenterSquare(Bitmap input, int size) { + int w = input.getWidth(); + int h = input.getHeight(); + + float scale = Math.max((float) size / h, (float) size / w); + + float outWidth = scale * w; + float outHeight = scale * h; + float left = (size - outWidth) / 2; + float top = (size - outHeight) / 2; + RectF target = new RectF(left, top, left + outWidth, top + outHeight); + + Bitmap output = Bitmap.createBitmap(size, size, input.getConfig()); + Canvas canvas = new Canvas(output); + canvas.drawBitmap(input, null, target, null); + return output; + } + + private int calcSampleSize(Uri image, int size) + throws FileNotFoundException { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeStream(mXmppConnectionService.getContentResolver() + .openInputStream(image), null, options); + return calcSampleSize(options, size); + } + + private int calcSampleSize(File image, int size) { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFile(image.getAbsolutePath(), options); + return calcSampleSize(options, size); + } + + private int calcSampleSize(BitmapFactory.Options options, int size) { + int height = options.outHeight; + int width = options.outWidth; + int inSampleSize = 1; + + if (height > size || width > size) { + int halfHeight = height / 2; + int halfWidth = width / 2; + + while ((halfHeight / inSampleSize) > size + && (halfWidth / inSampleSize) > size) { + inSampleSize *= 2; + } + } + return inSampleSize; + } + + public Uri getJingleFileUri(Message message) { + File file = getFile(message); + return Uri.parse("file://" + file.getAbsolutePath()); + } + + public class ImageCopyException extends Exception { + private static final long serialVersionUID = -1010013599132881427L; + private int resId; + + public ImageCopyException(int resId) { + this.resId = resId; + } + + public int getResId() { + return resId; + } + } + + public Bitmap getAvatar(String avatar, int size) { + if (avatar == null) { + return null; + } + Bitmap bm = cropCenter(getAvatarUri(avatar), size, size); + if (bm == null) { + return null; + } + return bm; + } + + public boolean isFileAvailable(Message message) { + return getFile(message).exists(); + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/persistance/OnPhoneContactsMerged.java b/conversations/src/main/java/eu/siacs/conversations/persistance/OnPhoneContactsMerged.java new file mode 100644 index 00000000..6a457b17 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/persistance/OnPhoneContactsMerged.java @@ -0,0 +1,5 @@ +package eu.siacs.conversations.persistance; + +public interface OnPhoneContactsMerged { + public void phoneContactsMerged(); +} diff --git a/conversations/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java b/conversations/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java new file mode 100644 index 00000000..676a09c9 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java @@ -0,0 +1,23 @@ +package eu.siacs.conversations.services; + +public class AbstractConnectionManager { + protected XmppConnectionService mXmppConnectionService; + + public AbstractConnectionManager(XmppConnectionService service) { + this.mXmppConnectionService = service; + } + + public XmppConnectionService getXmppConnectionService() { + return this.mXmppConnectionService; + } + + public long getAutoAcceptFileSize() { + String config = this.mXmppConnectionService.getPreferences().getString( + "auto_accept_file_size", "524288"); + try { + return Long.parseLong(config); + } catch (NumberFormatException e) { + return 524288; + } + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/services/AvatarService.java b/conversations/src/main/java/eu/siacs/conversations/services/AvatarService.java new file mode 100644 index 00000000..c0668a19 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/services/AvatarService.java @@ -0,0 +1,298 @@ +package eu.siacs.conversations.services; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Bookmark; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.ListItem; +import eu.siacs.conversations.entities.MucOptions; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.net.Uri; +import android.util.Log; + +public class AvatarService { + + private static final int FG_COLOR = 0xFFFAFAFA; + private static final int TRANSPARENT = 0x00000000; + + private static final String PREFIX_CONTACT = "contact"; + private static final String PREFIX_CONVERSATION = "conversation"; + private static final String PREFIX_ACCOUNT = "account"; + private static final String PREFIX_GENERIC = "generic"; + + private ArrayList sizes = new ArrayList(); + + protected XmppConnectionService mXmppConnectionService = null; + + public AvatarService(XmppConnectionService service) { + this.mXmppConnectionService = service; + } + + public Bitmap get(Contact contact, int size) { + final String KEY = key(contact, size); + Bitmap avatar = this.mXmppConnectionService.getBitmapCache().get(KEY); + if (avatar != null) { + return avatar; + } + Log.d(Config.LOGTAG, "no cache hit for " + KEY); + avatar = mXmppConnectionService.getFileBackend().getAvatar( + contact.getAvatar(), size); + if (avatar == null) { + if (contact.getProfilePhoto() != null) { + avatar = mXmppConnectionService.getFileBackend() + .cropCenterSquare(Uri.parse(contact.getProfilePhoto()), + size); + if (avatar == null) { + avatar = get(contact.getDisplayName(), size); + } + } else { + avatar = get(contact.getDisplayName(), size); + } + } + this.mXmppConnectionService.getBitmapCache().put(KEY, avatar); + return avatar; + } + + public void clear(Contact contact) { + for (Integer size : sizes) { + this.mXmppConnectionService.getBitmapCache().remove( + key(contact, size)); + } + } + + private String key(Contact contact, int size) { + synchronized (this.sizes) { + if (!this.sizes.contains(size)) { + this.sizes.add(size); + } + } + return PREFIX_CONTACT + "_" + contact.getAccount().getJid() + "_" + + contact.getJid() + "_" + String.valueOf(size); + } + + public Bitmap get(ListItem item, int size) { + if (item instanceof Contact) { + return get((Contact) item, size); + } else if (item instanceof Bookmark) { + Bookmark bookmark = (Bookmark) item; + if (bookmark.getConversation() != null) { + return get(bookmark.getConversation(), size); + } else { + return get(bookmark.getDisplayName(), size); + } + } else { + return get(item.getDisplayName(), size); + } + } + + public Bitmap get(Conversation conversation, int size) { + if (conversation.getMode() == Conversation.MODE_SINGLE) { + return get(conversation.getContact(), size); + } else { + return get(conversation.getMucOptions(), size); + } + } + + public void clear(Conversation conversation) { + if (conversation.getMode() == Conversation.MODE_SINGLE) { + clear(conversation.getContact()); + } else { + clear(conversation.getMucOptions()); + } + } + + public Bitmap get(MucOptions mucOptions, int size) { + final String KEY = key(mucOptions, size); + Bitmap bitmap = this.mXmppConnectionService.getBitmapCache().get(KEY); + if (bitmap != null) { + return bitmap; + } + Log.d(Config.LOGTAG, "no cache hit for " + KEY); + List users = mucOptions.getUsers(); + int count = users.size(); + bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + bitmap.eraseColor(TRANSPARENT); + + if (count == 0) { + String name = mucOptions.getConversation().getName(); + String letter = name.substring(0, 1); + int color = this.getColorForName(name); + drawTile(canvas, letter, color, 0, 0, size, size); + } else if (count == 1) { + drawTile(canvas, users.get(0), 0, 0, size, size); + } else if (count == 2) { + drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size); + drawTile(canvas, users.get(1), size / 2 + 1, 0, size, size); + } else if (count == 3) { + drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size); + drawTile(canvas, users.get(1), size / 2 + 1, 0, size, size / 2 - 1); + drawTile(canvas, users.get(2), size / 2 + 1, size / 2 + 1, size, + size); + } else if (count == 4) { + drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size / 2 - 1); + drawTile(canvas, users.get(1), 0, size / 2 + 1, size / 2 - 1, size); + drawTile(canvas, users.get(2), size / 2 + 1, 0, size, size / 2 - 1); + drawTile(canvas, users.get(3), size / 2 + 1, size / 2 + 1, size, + size); + } else { + drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size / 2 - 1); + drawTile(canvas, users.get(1), 0, size / 2 + 1, size / 2 - 1, size); + drawTile(canvas, users.get(2), size / 2 + 1, 0, size, size / 2 - 1); + drawTile(canvas, "\u2026", 0xFF202020, size / 2 + 1, size / 2 + 1, + size, size); + } + this.mXmppConnectionService.getBitmapCache().put(KEY, bitmap); + return bitmap; + } + + public void clear(MucOptions options) { + for (Integer size : sizes) { + this.mXmppConnectionService.getBitmapCache().remove( + key(options, size)); + } + } + + private String key(MucOptions options, int size) { + synchronized (this.sizes) { + if (!this.sizes.contains(size)) { + this.sizes.add(size); + } + } + return PREFIX_CONVERSATION + "_" + options.getConversation().getUuid() + + "_" + String.valueOf(size); + } + + public Bitmap get(Account account, int size) { + final String KEY = key(account, size); + Bitmap avatar = mXmppConnectionService.getBitmapCache().get(KEY); + if (avatar != null) { + return avatar; + } + Log.d(Config.LOGTAG, "no cache hit for " + KEY); + avatar = mXmppConnectionService.getFileBackend().getAvatar( + account.getAvatar(), size); + if (avatar == null) { + avatar = get(account.getJid(), size); + } + mXmppConnectionService.getBitmapCache().put(KEY, avatar); + return avatar; + } + + public void clear(Account account) { + for (Integer size : sizes) { + this.mXmppConnectionService.getBitmapCache().remove( + key(account, size)); + } + } + + private String key(Account account, int size) { + synchronized (this.sizes) { + if (!this.sizes.contains(size)) { + this.sizes.add(size); + } + } + return PREFIX_ACCOUNT + "_" + account.getUuid() + "_" + + String.valueOf(size); + } + + public Bitmap get(String name, int size) { + final String KEY = key(name, size); + Bitmap bitmap = mXmppConnectionService.getBitmapCache().get(KEY); + if (bitmap != null) { + return bitmap; + } + Log.d(Config.LOGTAG, "no cache hit for " + KEY); + bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + String letter = name.substring(0, 1); + int color = this.getColorForName(name); + drawTile(canvas, letter, color, 0, 0, size, size); + mXmppConnectionService.getBitmapCache().put(KEY, bitmap); + return bitmap; + } + + private String key(String name, int size) { + synchronized (this.sizes) { + if (!this.sizes.contains(size)) { + this.sizes.add(size); + } + } + return PREFIX_GENERIC + "_" + name + "_" + String.valueOf(size); + } + + private void drawTile(Canvas canvas, String letter, int tileColor, + int left, int top, int right, int bottom) { + letter = letter.toUpperCase(Locale.getDefault()); + Paint tilePaint = new Paint(), textPaint = new Paint(); + tilePaint.setColor(tileColor); + textPaint.setFlags(Paint.ANTI_ALIAS_FLAG); + textPaint.setColor(FG_COLOR); + textPaint.setTypeface(Typeface.create("sans-serif-light", + Typeface.NORMAL)); + textPaint.setTextSize((float) ((right - left) * 0.8)); + Rect rect = new Rect(); + + canvas.drawRect(new Rect(left, top, right, bottom), tilePaint); + textPaint.getTextBounds(letter, 0, 1, rect); + float width = textPaint.measureText(letter); + canvas.drawText(letter, (right + left) / 2 - width / 2, (top + bottom) + / 2 + rect.height() / 2, textPaint); + } + + private void drawTile(Canvas canvas, MucOptions.User user, int left, + int top, int right, int bottom) { + Contact contact = user.getContact(); + if (contact != null) { + Uri uri = null; + if (contact.getAvatar() != null) { + uri = mXmppConnectionService.getFileBackend().getAvatarUri( + contact.getAvatar()); + } else if (contact.getProfilePhoto() != null) { + uri = Uri.parse(contact.getProfilePhoto()); + } + if (uri != null) { + Bitmap bitmap = mXmppConnectionService.getFileBackend() + .cropCenter(uri, bottom - top, right - left); + if (bitmap != null) { + drawTile(canvas, bitmap, left, top, right, bottom); + } else { + String letter = user.getName().substring(0, 1); + int color = this.getColorForName(user.getName()); + drawTile(canvas, letter, color, left, top, right, bottom); + } + } else { + String letter = user.getName().substring(0, 1); + int color = this.getColorForName(user.getName()); + drawTile(canvas, letter, color, left, top, right, bottom); + } + } else { + String letter = user.getName().substring(0, 1); + int color = this.getColorForName(user.getName()); + drawTile(canvas, letter, color, left, top, right, bottom); + } + } + + private void drawTile(Canvas canvas, Bitmap bm, int dstleft, int dsttop, + int dstright, int dstbottom) { + Rect dst = new Rect(dstleft, dsttop, dstright, dstbottom); + canvas.drawBitmap(bm, null, dst, null); + } + + private int getColorForName(String name) { + int holoColors[] = { 0xFFe91e63, 0xFF9c27b0, 0xFF673ab7, 0xFF3f51b5, + 0xFF5677fc, 0xFF03a9f4, 0xFF00bcd4, 0xFF009688, 0xFFff5722, + 0xFF795548, 0xFF607d8b }; + return holoColors[(int) ((name.hashCode() & 0xffffffffl) % holoColors.length)]; + } + +} diff --git a/conversations/src/main/java/eu/siacs/conversations/services/EventReceiver.java b/conversations/src/main/java/eu/siacs/conversations/services/EventReceiver.java new file mode 100644 index 00000000..dfbe9db7 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/services/EventReceiver.java @@ -0,0 +1,24 @@ +package eu.siacs.conversations.services; + +import eu.siacs.conversations.persistance.DatabaseBackend; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +public class EventReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + Intent mIntentForService = new Intent(context, + XmppConnectionService.class); + if (intent.getAction() != null) { + mIntentForService.setAction(intent.getAction()); + } else { + mIntentForService.setAction("other"); + } + if (intent.getAction().equals("ui") + || DatabaseBackend.getInstance(context).hasEnabledAccounts()) { + context.startService(mIntentForService); + } + } + +} diff --git a/conversations/src/main/java/eu/siacs/conversations/services/NotificationService.java b/conversations/src/main/java/eu/siacs/conversations/services/NotificationService.java new file mode 100644 index 00000000..00765deb --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/services/NotificationService.java @@ -0,0 +1,237 @@ +package eu.siacs.conversations.services; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.Uri; +import android.os.PowerManager; +import android.support.v4.app.NotificationCompat; +import android.support.v4.app.TaskStackBuilder; +import android.text.Html; +import android.util.DisplayMetrics; + +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.ui.ConversationActivity; + +public class NotificationService { + + private XmppConnectionService mXmppConnectionService; + + private LinkedHashMap> notifications = new LinkedHashMap>(); + + public int NOTIFICATION_ID = 0x2342; + private Conversation mOpenConversation; + private boolean mIsInForeground; + + public NotificationService(XmppConnectionService service) { + this.mXmppConnectionService = service; + } + + public void push(Message message) { + PowerManager pm = (PowerManager) mXmppConnectionService + .getSystemService(Context.POWER_SERVICE); + boolean isScreenOn = pm.isScreenOn(); + + if (this.mIsInForeground && isScreenOn + && this.mOpenConversation == message.getConversation()) { + return; + } + synchronized (notifications) { + String conversationUuid = message.getConversationUuid(); + if (notifications.containsKey(conversationUuid)) { + notifications.get(conversationUuid).add(message); + } else { + ArrayList mList = new ArrayList(); + mList.add(message); + notifications.put(conversationUuid, mList); + } + Account account = message.getConversation().getAccount(); + updateNotification((!(this.mIsInForeground && this.mOpenConversation == null) || !isScreenOn) + && !account.inGracePeriod()); + } + + } + + public void clear() { + synchronized (notifications) { + notifications.clear(); + updateNotification(false); + } + } + + public void clear(Conversation conversation) { + synchronized (notifications) { + notifications.remove(conversation.getUuid()); + updateNotification(false); + } + } + + private void updateNotification(boolean notify) { + NotificationManager notificationManager = (NotificationManager) mXmppConnectionService + .getSystemService(Context.NOTIFICATION_SERVICE); + SharedPreferences preferences = mXmppConnectionService.getPreferences(); + + String ringtone = preferences.getString("notification_ringtone", null); + boolean vibrate = preferences.getBoolean("vibrate_on_notification", + true); + + if (notifications.size() == 0) { + notificationManager.cancel(NOTIFICATION_ID); + } else { + NotificationCompat.Builder mBuilder = new NotificationCompat.Builder( + mXmppConnectionService); + mBuilder.setSmallIcon(R.drawable.ic_notification); + if (notifications.size() == 1) { + ArrayList messages = notifications.values().iterator() + .next(); + if (messages.size() >= 1) { + Conversation conversation = messages.get(0) + .getConversation(); + mBuilder.setLargeIcon(mXmppConnectionService + .getAvatarService().get(conversation, getPixel(64))); + mBuilder.setContentTitle(conversation.getName()); + StringBuilder text = new StringBuilder(); + for (int i = 0; i < messages.size(); ++i) { + text.append(messages.get(i).getReadableBody( + mXmppConnectionService)); + if (i != messages.size() - 1) { + text.append("\n"); + } + } + mBuilder.setStyle(new NotificationCompat.BigTextStyle() + .bigText(text.toString())); + mBuilder.setContentText(messages.get(0).getReadableBody( + mXmppConnectionService)); + if (notify) { + mBuilder.setTicker(messages.get(messages.size() - 1) + .getReadableBody(mXmppConnectionService)); + } + mBuilder.setContentIntent(createContentIntent(conversation + .getUuid())); + } else { + notificationManager.cancel(NOTIFICATION_ID); + return; + } + } else { + NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle(); + style.setBigContentTitle(notifications.size() + + " " + + mXmppConnectionService + .getString(R.string.unread_conversations)); + StringBuilder names = new StringBuilder(); + Conversation conversation = null; + for (ArrayList messages : notifications.values()) { + if (messages.size() > 0) { + conversation = messages.get(0).getConversation(); + String name = conversation.getName(); + style.addLine(Html.fromHtml("" + + name + + " " + + messages.get(0).getReadableBody( + mXmppConnectionService))); + names.append(name); + names.append(", "); + } + } + if (names.length() >= 2) { + names.delete(names.length() - 2, names.length()); + } + mBuilder.setContentTitle(notifications.size() + + " " + + mXmppConnectionService + .getString(R.string.unread_conversations)); + mBuilder.setContentText(names.toString()); + mBuilder.setStyle(style); + if (conversation != null) { + mBuilder.setContentIntent(createContentIntent(conversation + .getUuid())); + } + } + if (notify) { + if (vibrate) { + int dat = 70; + long[] pattern = { 0, 3 * dat, dat, dat }; + mBuilder.setVibrate(pattern); + } + if (ringtone != null) { + mBuilder.setSound(Uri.parse(ringtone)); + } + } + mBuilder.setDeleteIntent(createDeleteIntent()); + mBuilder.setLights(0xffffffff, 2000, 4000); + Notification notification = mBuilder.build(); + notificationManager.notify(NOTIFICATION_ID, notification); + } + } + + private PendingIntent createContentIntent(String conversationUuid) { + TaskStackBuilder stackBuilder = TaskStackBuilder + .create(mXmppConnectionService); + stackBuilder.addParentStack(ConversationActivity.class); + + Intent viewConversationIntent = new Intent(mXmppConnectionService, + ConversationActivity.class); + viewConversationIntent.setAction(Intent.ACTION_VIEW); + viewConversationIntent.putExtra(ConversationActivity.CONVERSATION, + conversationUuid); + viewConversationIntent.setType(ConversationActivity.VIEW_CONVERSATION); + + stackBuilder.addNextIntent(viewConversationIntent); + + PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0, + PendingIntent.FLAG_UPDATE_CURRENT); + return resultPendingIntent; + } + + private PendingIntent createDeleteIntent() { + Intent intent = new Intent(mXmppConnectionService, + XmppConnectionService.class); + intent.setAction("clear_notification"); + return PendingIntent.getService(mXmppConnectionService, 0, intent, 0); + } + + public static boolean wasHighlightedOrPrivate(Message message) { + String nick = message.getConversation().getMucOptions().getActualNick(); + Pattern highlight = generateNickHighlightPattern(nick); + if (message.getBody() == null || nick == null) { + return false; + } + Matcher m = highlight.matcher(message.getBody()); + return (m.find() || message.getType() == Message.TYPE_PRIVATE); + } + + private static Pattern generateNickHighlightPattern(String nick) { + // We expect a word boundary, i.e. space or start of string, followed by + // the + // nick (matched in case-insensitive manner), followed by optional + // punctuation (for example "bob: i disagree" or "how are you alice?"), + // followed by another word boundary. + return Pattern.compile("\\b" + nick + "\\p{Punct}?\\b", + Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE); + } + + public void setOpenConversation(Conversation conversation) { + this.mOpenConversation = conversation; + } + + public void setIsInForeground(boolean foreground) { + this.mIsInForeground = foreground; + } + + private int getPixel(int dp) { + DisplayMetrics metrics = mXmppConnectionService.getResources() + .getDisplayMetrics(); + return ((int) (dp * metrics.density)); + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/conversations/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java new file mode 100644 index 00000000..37e334eb --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -0,0 +1,1927 @@ +package eu.siacs.conversations.services; + +import java.security.SecureRandom; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.Hashtable; +import java.util.List; +import java.util.Locale; +import java.util.TimeZone; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.openintents.openpgp.util.OpenPgpApi; +import org.openintents.openpgp.util.OpenPgpServiceConnection; + +import de.duenndns.ssl.MemorizingTrustManager; + +import net.java.otr4j.OtrException; +import net.java.otr4j.session.Session; +import net.java.otr4j.session.SessionStatus; +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.crypto.PgpEngine; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Bookmark; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Downloadable; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.entities.MucOptions; +import eu.siacs.conversations.entities.MucOptions.OnRenameListener; +import eu.siacs.conversations.entities.Presences; +import eu.siacs.conversations.generator.IqGenerator; +import eu.siacs.conversations.generator.MessageGenerator; +import eu.siacs.conversations.generator.PresenceGenerator; +import eu.siacs.conversations.http.HttpConnectionManager; +import eu.siacs.conversations.parser.IqParser; +import eu.siacs.conversations.parser.MessageParser; +import eu.siacs.conversations.parser.PresenceParser; +import eu.siacs.conversations.persistance.DatabaseBackend; +import eu.siacs.conversations.persistance.FileBackend; +import eu.siacs.conversations.ui.UiCallback; +import eu.siacs.conversations.utils.CryptoHelper; +import eu.siacs.conversations.utils.ExceptionHelper; +import eu.siacs.conversations.utils.OnPhoneContactsLoadedListener; +import eu.siacs.conversations.utils.PRNGFixes; +import eu.siacs.conversations.utils.PhoneHelper; +import eu.siacs.conversations.utils.UIHelper; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.OnBindListener; +import eu.siacs.conversations.xmpp.OnContactStatusChanged; +import eu.siacs.conversations.xmpp.OnIqPacketReceived; +import eu.siacs.conversations.xmpp.OnMessageAcknowledged; +import eu.siacs.conversations.xmpp.OnStatusChanged; +import eu.siacs.conversations.xmpp.XmppConnection; +import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager; +import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived; +import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; +import eu.siacs.conversations.xmpp.pep.Avatar; +import eu.siacs.conversations.xmpp.stanzas.IqPacket; +import eu.siacs.conversations.xmpp.stanzas.MessagePacket; +import eu.siacs.conversations.xmpp.stanzas.PresencePacket; +import android.annotation.SuppressLint; +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.database.ContentObserver; +import android.graphics.Bitmap; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.net.Uri; +import android.os.Binder; +import android.os.Bundle; +import android.os.FileObserver; +import android.os.IBinder; +import android.os.PowerManager; +import android.os.PowerManager.WakeLock; +import android.os.SystemClock; +import android.preference.PreferenceManager; +import android.provider.ContactsContract; +import android.util.Log; +import android.util.LruCache; + +public class XmppConnectionService extends Service { + + public DatabaseBackend databaseBackend; + private FileBackend fileBackend = new FileBackend(this); + + public long startDate; + + private static String ACTION_MERGE_PHONE_CONTACTS = "merge_phone_contacts"; + public static String ACTION_CLEAR_NOTIFICATION = "clear_notification"; + + private MemorizingTrustManager mMemorizingTrustManager; + + private NotificationService mNotificationService = new NotificationService( + this); + + private MessageParser mMessageParser = new MessageParser(this); + private PresenceParser mPresenceParser = new PresenceParser(this); + private IqParser mIqParser = new IqParser(this); + private MessageGenerator mMessageGenerator = new MessageGenerator(this); + private PresenceGenerator mPresenceGenerator = new PresenceGenerator(this); + + private List accounts; + private CopyOnWriteArrayList conversations = null; + private JingleConnectionManager mJingleConnectionManager = new JingleConnectionManager( + this); + private HttpConnectionManager mHttpConnectionManager = new HttpConnectionManager( + this); + private AvatarService mAvatarService = new AvatarService(this); + + private OnConversationUpdate mOnConversationUpdate = null; + private Integer convChangedListenerCount = 0; + private OnAccountUpdate mOnAccountUpdate = null; + private Integer accountChangedListenerCount = 0; + private OnRosterUpdate mOnRosterUpdate = null; + private Integer rosterChangedListenerCount = 0; + public OnContactStatusChanged onContactStatusChanged = new OnContactStatusChanged() { + + @Override + public void onContactStatusChanged(Contact contact, boolean online) { + Conversation conversation = find(getConversations(), contact); + if (conversation != null) { + conversation.endOtrIfNeeded(); + if (online && (contact.getPresences().size() == 1)) { + sendUnsendMessages(conversation); + } + } + } + }; + + private SecureRandom mRandom; + + private ContentObserver contactObserver = new ContentObserver(null) { + @Override + public void onChange(boolean selfChange) { + super.onChange(selfChange); + Intent intent = new Intent(getApplicationContext(), + XmppConnectionService.class); + intent.setAction(ACTION_MERGE_PHONE_CONTACTS); + startService(intent); + } + }; + + private FileObserver fileObserver = new FileObserver( + FileBackend.getConversationsDirectory()) { + + @Override + public void onEvent(int event, String path) { + if (event == FileObserver.DELETE) { + markFileDeleted(path.split("\\.")[0]); + } + } + }; + + private final IBinder mBinder = new XmppConnectionBinder(); + private OnStatusChanged statusListener = new OnStatusChanged() { + + @Override + public void onStatusChanged(Account account) { + XmppConnection connection = account.getXmppConnection(); + if (mOnAccountUpdate != null) { + mOnAccountUpdate.onAccountUpdate(); + ; + } + if (account.getStatus() == Account.STATUS_ONLINE) { + for (Conversation conversation : account.pendingConferenceLeaves) { + leaveMuc(conversation); + } + for (Conversation conversation : account.pendingConferenceJoins) { + joinMuc(conversation); + } + mJingleConnectionManager.cancelInTransmission(); + List conversations = getConversations(); + for (int i = 0; i < conversations.size(); ++i) { + if (conversations.get(i).getAccount() == account) { + conversations.get(i).startOtrIfNeeded(); + sendUnsendMessages(conversations.get(i)); + } + } + if (connection != null && connection.getFeatures().csi()) { + if (checkListeners()) { + Log.d(Config.LOGTAG, account.getJid() + + " sending csi//inactive"); + connection.sendInactive(); + } else { + Log.d(Config.LOGTAG, account.getJid() + + " sending csi//active"); + connection.sendActive(); + } + } + syncDirtyContacts(account); + scheduleWakeupCall(Config.PING_MAX_INTERVAL, true); + } else if (account.getStatus() == Account.STATUS_OFFLINE) { + resetSendingToWaiting(account); + if (!account.isOptionSet(Account.OPTION_DISABLED)) { + int timeToReconnect = mRandom.nextInt(50) + 10; + scheduleWakeupCall(timeToReconnect, false); + } + } else if (account.getStatus() == Account.STATUS_REGISTRATION_SUCCESSFULL) { + databaseBackend.updateAccount(account); + reconnectAccount(account, true); + } else if ((account.getStatus() != Account.STATUS_CONNECTING) + && (account.getStatus() != Account.STATUS_NO_INTERNET)) { + if (connection != null) { + int next = connection.getTimeToNextAttempt(); + Log.d(Config.LOGTAG, account.getJid() + + ": error connecting account. try again in " + + next + "s for the " + + (connection.getAttempt() + 1) + " time"); + scheduleWakeupCall((int) (next * 1.2), false); + } + } + UIHelper.showErrorNotification(getApplicationContext(), + getAccounts()); + } + }; + + private OnJinglePacketReceived jingleListener = new OnJinglePacketReceived() { + + @Override + public void onJinglePacketReceived(Account account, JinglePacket packet) { + mJingleConnectionManager.deliverPacket(account, packet); + } + }; + + private OpenPgpServiceConnection pgpServiceConnection; + private PgpEngine mPgpEngine = null; + private Intent pingIntent; + private PendingIntent pendingPingIntent = null; + private WakeLock wakeLock; + private PowerManager pm; + private OnBindListener mOnBindListener = new OnBindListener() { + + @Override + public void onBind(final Account account) { + account.getRoster().clearPresences(); + account.clearPresences(); // self presences + account.pendingConferenceJoins.clear(); + account.pendingConferenceLeaves.clear(); + fetchRosterFromServer(account); + fetchBookmarks(account); + sendPresencePacket(account, + mPresenceGenerator.sendPresence(account)); + connectMultiModeConversations(account); + updateConversationUi(); + } + }; + + private OnMessageAcknowledged mOnMessageAcknowledgedListener = new OnMessageAcknowledged() { + + @Override + public void onMessageAcknowledged(Account account, String uuid) { + for (Conversation conversation : getConversations()) { + if (conversation.getAccount() == account) { + for (Message message : conversation.getMessages()) { + if ((message.getStatus() == Message.STATUS_UNSEND || message + .getStatus() == Message.STATUS_WAITING) + && message.getUuid().equals(uuid)) { + markMessage(message, Message.STATUS_SEND); + return; + } + } + } + } + } + }; + private LruCache mBitmapCache; + + public PgpEngine getPgpEngine() { + if (pgpServiceConnection.isBound()) { + if (this.mPgpEngine == null) { + this.mPgpEngine = new PgpEngine(new OpenPgpApi( + getApplicationContext(), + pgpServiceConnection.getService()), this); + } + return mPgpEngine; + } else { + return null; + } + + } + + public FileBackend getFileBackend() { + return this.fileBackend; + } + + public AvatarService getAvatarService() { + return this.mAvatarService; + } + + public Message attachImageToConversation(final Conversation conversation, + final Uri uri, final UiCallback callback) { + final Message message; + if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) { + message = new Message(conversation, "", + Message.ENCRYPTION_DECRYPTED); + } else { + message = new Message(conversation, "", + conversation.getNextEncryption(forceEncryption())); + } + message.setPresence(conversation.getNextPresence()); + message.setType(Message.TYPE_IMAGE); + message.setStatus(Message.STATUS_OFFERED); + new Thread(new Runnable() { + + @Override + public void run() { + try { + getFileBackend().copyImageToPrivateStorage(message, uri); + if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) { + getPgpEngine().encrypt(message, callback); + } else { + callback.success(message); + } + } catch (FileBackend.ImageCopyException e) { + callback.error(e.getResId(), message); + } + } + }).start(); + return message; + } + + public Conversation find(Bookmark bookmark) { + return find(bookmark.getAccount(), bookmark.getJid()); + } + + public Conversation find(Account account, String jid) { + return find(getConversations(), account, jid); + } + + public class XmppConnectionBinder extends Binder { + public XmppConnectionService getService() { + return XmppConnectionService.this; + } + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (intent != null && intent.getAction() != null) { + if (intent.getAction().equals(ACTION_MERGE_PHONE_CONTACTS)) { + mergePhoneContactsWithRoster(); + return START_STICKY; + } else if (intent.getAction().equals(Intent.ACTION_SHUTDOWN)) { + logoutAndSave(); + return START_NOT_STICKY; + } else if (intent.getAction().equals(ACTION_CLEAR_NOTIFICATION)) { + mNotificationService.clear(); + } + } + this.wakeLock.acquire(); + + for (Account account : accounts) { + if (!account.isOptionSet(Account.OPTION_DISABLED)) { + if (!hasInternetConnection()) { + account.setStatus(Account.STATUS_NO_INTERNET); + if (statusListener != null) { + statusListener.onStatusChanged(account); + } + } else { + if (account.getStatus() == Account.STATUS_NO_INTERNET) { + account.setStatus(Account.STATUS_OFFLINE); + if (statusListener != null) { + statusListener.onStatusChanged(account); + } + } + if (account.getStatus() == Account.STATUS_ONLINE) { + long lastReceived = account.getXmppConnection() + .getLastPacketReceived(); + long lastSent = account.getXmppConnection() + .getLastPingSent(); + if (lastSent - lastReceived >= Config.PING_TIMEOUT * 1000) { + Log.d(Config.LOGTAG, account.getJid() + + ": ping timeout"); + this.reconnectAccount(account, true); + } else if (SystemClock.elapsedRealtime() - lastReceived >= Config.PING_MIN_INTERVAL * 1000) { + account.getXmppConnection().sendPing(); + this.scheduleWakeupCall(2, false); + } + } else if (account.getStatus() == Account.STATUS_OFFLINE) { + if (account.getXmppConnection() == null) { + account.setXmppConnection(this + .createConnection(account)); + } + new Thread(account.getXmppConnection()).start(); + } else if ((account.getStatus() == Account.STATUS_CONNECTING) + && ((SystemClock.elapsedRealtime() - account + .getXmppConnection().getLastConnect()) / 1000 >= Config.CONNECT_TIMEOUT)) { + Log.d(Config.LOGTAG, account.getJid() + + ": time out during connect reconnecting"); + reconnectAccount(account, true); + } else { + if (account.getXmppConnection().getTimeToNextAttempt() <= 0) { + reconnectAccount(account, true); + } + } + // in any case. reschedule wakup call + this.scheduleWakeupCall(Config.PING_MAX_INTERVAL, true); + } + if (mOnAccountUpdate != null) { + mOnAccountUpdate.onAccountUpdate(); + } + } + } + if (wakeLock.isHeld()) { + try { + wakeLock.release(); + } catch (RuntimeException re) { + } + } + return START_STICKY; + } + + public boolean hasInternetConnection() { + ConnectivityManager cm = (ConnectivityManager) getApplicationContext() + .getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo activeNetwork = cm.getActiveNetworkInfo(); + return activeNetwork != null && activeNetwork.isConnected(); + } + + @SuppressLint("TrulyRandom") + @Override + public void onCreate() { + ExceptionHelper.init(getApplicationContext()); + PRNGFixes.apply(); + this.mRandom = new SecureRandom(); + this.mMemorizingTrustManager = new MemorizingTrustManager( + getApplicationContext()); + + int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); + int cacheSize = maxMemory / 8; + this.mBitmapCache = new LruCache(cacheSize) { + @Override + protected int sizeOf(String key, Bitmap bitmap) { + return bitmap.getByteCount() / 1024; + } + }; + + this.databaseBackend = DatabaseBackend + .getInstance(getApplicationContext()); + this.accounts = databaseBackend.getAccounts(); + + for (Account account : this.accounts) { + this.databaseBackend.readRoster(account.getRoster()); + } + this.mergePhoneContactsWithRoster(); + this.getConversations(); + + getContentResolver().registerContentObserver( + ContactsContract.Contacts.CONTENT_URI, true, contactObserver); + this.fileObserver.startWatching(); + this.pgpServiceConnection = new OpenPgpServiceConnection( + getApplicationContext(), "org.sufficientlysecure.keychain"); + this.pgpServiceConnection.bindToService(); + + this.pm = (PowerManager) getSystemService(Context.POWER_SERVICE); + this.wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, + "XmppConnectionService"); + } + + @Override + public void onDestroy() { + super.onDestroy(); + this.logoutAndSave(); + } + + @Override + public void onTaskRemoved(Intent rootIntent) { + super.onTaskRemoved(rootIntent); + this.logoutAndSave(); + } + + private void logoutAndSave() { + for (Account account : accounts) { + databaseBackend.writeRoster(account.getRoster()); + if (account.getXmppConnection() != null) { + disconnect(account, false); + } + } + Context context = getApplicationContext(); + AlarmManager alarmManager = (AlarmManager) context + .getSystemService(Context.ALARM_SERVICE); + Intent intent = new Intent(context, EventReceiver.class); + alarmManager.cancel(PendingIntent.getBroadcast(context, 0, intent, 0)); + Log.d(Config.LOGTAG, "good bye"); + stopSelf(); + } + + protected void scheduleWakeupCall(int seconds, boolean ping) { + long timeToWake = SystemClock.elapsedRealtime() + seconds * 1000; + Context context = getApplicationContext(); + AlarmManager alarmManager = (AlarmManager) context + .getSystemService(Context.ALARM_SERVICE); + + if (ping) { + if (this.pingIntent == null) { + this.pingIntent = new Intent(context, EventReceiver.class); + this.pingIntent.setAction("ping"); + this.pingIntent.putExtra("time", timeToWake); + this.pendingPingIntent = PendingIntent.getBroadcast(context, 0, + this.pingIntent, 0); + alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, + timeToWake, pendingPingIntent); + } else { + long scheduledTime = this.pingIntent.getLongExtra("time", 0); + if (scheduledTime < SystemClock.elapsedRealtime() + || (scheduledTime > timeToWake)) { + this.pingIntent.putExtra("time", timeToWake); + alarmManager.cancel(this.pendingPingIntent); + this.pendingPingIntent = PendingIntent.getBroadcast( + context, 0, this.pingIntent, 0); + alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, + timeToWake, pendingPingIntent); + } + } + } else { + Intent intent = new Intent(context, EventReceiver.class); + intent.setAction("ping_check"); + PendingIntent alarmIntent = PendingIntent.getBroadcast(context, 0, + intent, 0); + alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake, + alarmIntent); + } + + } + + public XmppConnection createConnection(Account account) { + SharedPreferences sharedPref = getPreferences(); + account.setResource(sharedPref.getString("resource", "mobile") + .toLowerCase(Locale.getDefault())); + XmppConnection connection = new XmppConnection(account, this); + connection.setOnMessagePacketReceivedListener(this.mMessageParser); + connection.setOnStatusChangedListener(this.statusListener); + connection.setOnPresencePacketReceivedListener(this.mPresenceParser); + connection.setOnUnregisteredIqPacketReceivedListener(this.mIqParser); + connection.setOnJinglePacketReceivedListener(this.jingleListener); + connection.setOnBindListener(this.mOnBindListener); + connection + .setOnMessageAcknowledgeListener(this.mOnMessageAcknowledgedListener); + return connection; + } + + public void sendMessage(Message message) { + Account account = message.getConversation().getAccount(); + account.deactivateGracePeriod(); + Conversation conv = message.getConversation(); + MessagePacket packet = null; + boolean saveInDb = true; + boolean send = false; + if (account.getStatus() == Account.STATUS_ONLINE + && account.getXmppConnection() != null) { + if (message.getType() == Message.TYPE_IMAGE) { + if (message.getPresence() != null) { + if (message.getEncryption() == Message.ENCRYPTION_OTR) { + if (!conv.hasValidOtrSession() + && (message.getPresence() != null)) { + conv.startOtrSession(this, message.getPresence(), + true); + message.setStatus(Message.STATUS_WAITING); + } else if (conv.hasValidOtrSession() + && conv.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) { + mJingleConnectionManager + .createNewConnection(message); + } else if (message.getPresence() == null) { + message.setStatus(Message.STATUS_WAITING); + } + } else { + mJingleConnectionManager.createNewConnection(message); + } + } else { + message.setStatus(Message.STATUS_WAITING); + } + } else { + if (message.getEncryption() == Message.ENCRYPTION_OTR) { + if (!conv.hasValidOtrSession() + && (message.getPresence() != null)) { + conv.startOtrSession(this, message.getPresence(), true); + message.setStatus(Message.STATUS_WAITING); + } else if (conv.hasValidOtrSession() + && conv.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) { + message.setPresence(conv.getOtrSession().getSessionID() + .getUserID()); + packet = mMessageGenerator.generateOtrChat(message); + send = true; + + } else if (message.getPresence() == null) { + message.setStatus(Message.STATUS_WAITING); + } + } else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { + message.getConversation().endOtrIfNeeded(); + failWaitingOtrMessages(message.getConversation()); + packet = mMessageGenerator.generatePgpChat(message); + send = true; + } else { + message.getConversation().endOtrIfNeeded(); + failWaitingOtrMessages(message.getConversation()); + packet = mMessageGenerator.generateChat(message); + send = true; + } + } + if (!account.getXmppConnection().getFeatures().sm() + && conv.getMode() != Conversation.MODE_MULTI) { + message.setStatus(Message.STATUS_SEND); + } + } else { + message.setStatus(Message.STATUS_WAITING); + if (message.getType() == Message.TYPE_TEXT) { + if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { + String pgpBody = message.getEncryptedBody(); + String decryptedBody = message.getBody(); + message.setBody(pgpBody); + message.setEncryption(Message.ENCRYPTION_PGP); + databaseBackend.createMessage(message); + saveInDb = false; + message.setBody(decryptedBody); + message.setEncryption(Message.ENCRYPTION_DECRYPTED); + } else if (message.getEncryption() == Message.ENCRYPTION_OTR) { + if (conv.hasValidOtrSession()) { + message.setPresence(conv.getOtrSession().getSessionID() + .getUserID()); + } else if (!conv.hasValidOtrSession() + && message.getPresence() != null) { + conv.startOtrSession(this, message.getPresence(), false); + } + } + } + + } + conv.add(message); + if (saveInDb) { + if (message.getEncryption() == Message.ENCRYPTION_NONE + || saveEncryptedMessages()) { + databaseBackend.createMessage(message); + } + } + if ((send) && (packet != null)) { + sendMessagePacket(account, packet); + } + updateConversationUi(); + } + + private void sendUnsendMessages(Conversation conversation) { + for (int i = 0; i < conversation.getMessages().size(); ++i) { + int status = conversation.getMessages().get(i).getStatus(); + if (status == Message.STATUS_WAITING) { + resendMessage(conversation.getMessages().get(i)); + } + } + } + + private void resendMessage(Message message) { + Account account = message.getConversation().getAccount(); + MessagePacket packet = null; + if (message.getEncryption() == Message.ENCRYPTION_OTR) { + Presences presences = message.getConversation().getContact() + .getPresences(); + if (!message.getConversation().hasValidOtrSession()) { + if ((message.getPresence() != null) + && (presences.has(message.getPresence()))) { + message.getConversation().startOtrSession(this, + message.getPresence(), true); + } else { + if (presences.size() == 1) { + String presence = presences.asStringArray()[0]; + message.getConversation().startOtrSession(this, + presence, true); + } + } + } else { + if (message.getConversation().getOtrSession() + .getSessionStatus() == SessionStatus.ENCRYPTED) { + if (message.getType() == Message.TYPE_TEXT) { + packet = mMessageGenerator.generateOtrChat(message, + true); + } else if (message.getType() == Message.TYPE_IMAGE) { + mJingleConnectionManager.createNewConnection(message); + } + } + } + } else if (message.getType() == Message.TYPE_TEXT) { + if (message.getEncryption() == Message.ENCRYPTION_NONE) { + packet = mMessageGenerator.generateChat(message, true); + } else if ((message.getEncryption() == Message.ENCRYPTION_DECRYPTED) + || (message.getEncryption() == Message.ENCRYPTION_PGP)) { + packet = mMessageGenerator.generatePgpChat(message, true); + } + } else if (message.getType() == Message.TYPE_IMAGE) { + Presences presences = message.getConversation().getContact() + .getPresences(); + if ((message.getPresence() != null) + && (presences.has(message.getPresence()))) { + markMessage(message, Message.STATUS_OFFERED); + mJingleConnectionManager.createNewConnection(message); + } else { + if (presences.size() == 1) { + String presence = presences.asStringArray()[0]; + message.setPresence(presence); + markMessage(message, Message.STATUS_OFFERED); + mJingleConnectionManager.createNewConnection(message); + } + } + } + if (packet != null) { + if (!account.getXmppConnection().getFeatures().sm() + && message.getConversation().getMode() != Conversation.MODE_MULTI) { + markMessage(message, Message.STATUS_SEND); + } else { + markMessage(message, Message.STATUS_UNSEND); + } + sendMessagePacket(account, packet); + } + } + + public void fetchRosterFromServer(Account account) { + IqPacket iqPacket = new IqPacket(IqPacket.TYPE_GET); + if (!"".equals(account.getRosterVersion())) { + Log.d(Config.LOGTAG, account.getJid() + + ": fetching roster version " + account.getRosterVersion()); + } else { + Log.d(Config.LOGTAG, account.getJid() + ": fetching roster"); + } + iqPacket.query("jabber:iq:roster").setAttribute("ver", + account.getRosterVersion()); + account.getXmppConnection().sendIqPacket(iqPacket, + new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(final Account account, + IqPacket packet) { + Element query = packet.findChild("query"); + if (query != null) { + account.getRoster().markAllAsNotInRoster(); + mIqParser.rosterItems(account, query); + } + } + }); + } + + public void fetchBookmarks(Account account) { + IqPacket iqPacket = new IqPacket(IqPacket.TYPE_GET); + Element query = iqPacket.query("jabber:iq:private"); + query.addChild("storage", "storage:bookmarks"); + OnIqPacketReceived callback = new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + Element query = packet.query(); + List bookmarks = new CopyOnWriteArrayList(); + Element storage = query.findChild("storage", + "storage:bookmarks"); + if (storage != null) { + for (Element item : storage.getChildren()) { + if (item.getName().equals("conference")) { + Bookmark bookmark = Bookmark.parse(item, account); + bookmarks.add(bookmark); + Conversation conversation = find(bookmark); + if (conversation != null) { + conversation.setBookmark(bookmark); + } else { + if (bookmark.autojoin()) { + conversation = findOrCreateConversation( + account, bookmark.getJid(), true); + conversation.setBookmark(bookmark); + joinMuc(conversation); + } + } + } + } + } + account.setBookmarks(bookmarks); + } + }; + sendIqPacket(account, iqPacket, callback); + + } + + public void pushBookmarks(Account account) { + IqPacket iqPacket = new IqPacket(IqPacket.TYPE_SET); + Element query = iqPacket.query("jabber:iq:private"); + Element storage = query.addChild("storage", "storage:bookmarks"); + for (Bookmark bookmark : account.getBookmarks()) { + storage.addChild(bookmark); + } + sendIqPacket(account, iqPacket, null); + } + + private void mergePhoneContactsWithRoster() { + PhoneHelper.loadPhoneContacts(getApplicationContext(), + new OnPhoneContactsLoadedListener() { + @Override + public void onPhoneContactsLoaded(List phoneContacts) { + for (Account account : accounts) { + account.getRoster().clearSystemAccounts(); + } + for (Bundle phoneContact : phoneContacts) { + for (Account account : accounts) { + String jid = phoneContact.getString("jid"); + Contact contact = account.getRoster() + .getContact(jid); + String systemAccount = phoneContact + .getInt("phoneid") + + "#" + + phoneContact.getString("lookup"); + contact.setSystemAccount(systemAccount); + contact.setPhotoUri(phoneContact + .getString("photouri")); + contact.setSystemName(phoneContact + .getString("displayname")); + getAvatarService().clear(contact); + } + } + } + }); + } + + public List getConversations() { + if (this.conversations == null) { + Hashtable accountLookupTable = new Hashtable(); + for (Account account : this.accounts) { + accountLookupTable.put(account.getUuid(), account); + } + this.conversations = databaseBackend + .getConversations(Conversation.STATUS_AVAILABLE); + for (Conversation conv : this.conversations) { + Account account = accountLookupTable.get(conv.getAccountUuid()); + conv.setAccount(account); + conv.setMessages(databaseBackend.getMessages(conv, 50)); + checkDeletedFiles(conv); + } + } + return this.conversations; + } + + private void checkDeletedFiles(Conversation conversation) { + for (Message message : conversation.getMessages()) { + if (message.getType() == Message.TYPE_IMAGE + && message.getEncryption() != Message.ENCRYPTION_PGP) { + if (!getFileBackend().isFileAvailable(message)) { + message.setDownloadable(new DeletedDownloadable()); + } + } + } + } + + private void markFileDeleted(String uuid) { + for (Conversation conversation : getConversations()) { + for (Message message : conversation.getMessages()) { + if (message.getType() == Message.TYPE_IMAGE + && message.getEncryption() != Message.ENCRYPTION_PGP + && message.getUuid().equals(uuid)) { + if (!getFileBackend().isFileAvailable(message)) { + message.setDownloadable(new DeletedDownloadable()); + updateConversationUi(); + } + return; + } + } + } + } + + public void populateWithOrderedConversations(List list) { + populateWithOrderedConversations(list, true); + } + + public void populateWithOrderedConversations(List list, + boolean includeConferences) { + list.clear(); + if (includeConferences) { + list.addAll(getConversations()); + } else { + for (Conversation conversation : getConversations()) { + if (conversation.getMode() == Conversation.MODE_SINGLE) { + list.add(conversation); + } + } + } + Collections.sort(list, new Comparator() { + @Override + public int compare(Conversation lhs, Conversation rhs) { + Message left = lhs.getLatestMessage(); + Message right = rhs.getLatestMessage(); + if (left.getTimeSent() > right.getTimeSent()) { + return -1; + } else if (left.getTimeSent() < right.getTimeSent()) { + return 1; + } else { + return 0; + } + } + }); + } + + public int loadMoreMessages(Conversation conversation, long timestamp) { + List messages = databaseBackend.getMessages(conversation, 50, + timestamp); + for (Message message : messages) { + message.setConversation(conversation); + } + conversation.addAll(0, messages); + return messages.size(); + } + + public List getAccounts() { + return this.accounts; + } + + public Conversation find(List haystack, Contact contact) { + for (Conversation conversation : haystack) { + if (conversation.getContact() == contact) { + return conversation; + } + } + return null; + } + + public Conversation find(List haystack, Account account, + String jid) { + for (Conversation conversation : haystack) { + if ((account == null || conversation.getAccount().equals(account)) + && (conversation.getContactJid().split("/", 2)[0] + .equals(jid))) { + return conversation; + } + } + return null; + } + + public Conversation findOrCreateConversation(Account account, String jid, + boolean muc) { + Conversation conversation = find(account, jid); + if (conversation != null) { + return conversation; + } + conversation = databaseBackend.findConversation(account, jid); + if (conversation != null) { + conversation.setStatus(Conversation.STATUS_AVAILABLE); + conversation.setAccount(account); + if (muc) { + conversation.setMode(Conversation.MODE_MULTI); + } else { + conversation.setMode(Conversation.MODE_SINGLE); + } + conversation.setMessages(databaseBackend.getMessages(conversation, + 50)); + this.databaseBackend.updateConversation(conversation); + } else { + String conversationName; + Contact contact = account.getRoster().getContact(jid); + if (contact != null) { + conversationName = contact.getDisplayName(); + } else { + conversationName = jid.split("@")[0]; + } + if (muc) { + conversation = new Conversation(conversationName, account, jid, + Conversation.MODE_MULTI); + } else { + conversation = new Conversation(conversationName, account, jid, + Conversation.MODE_SINGLE); + } + this.databaseBackend.createConversation(conversation); + } + this.conversations.add(conversation); + updateConversationUi(); + return conversation; + } + + public void archiveConversation(Conversation conversation) { + if (conversation.getMode() == Conversation.MODE_MULTI) { + if (conversation.getAccount().getStatus() == Account.STATUS_ONLINE) { + Bookmark bookmark = conversation.getBookmark(); + if (bookmark != null && bookmark.autojoin()) { + bookmark.setAutojoin(false); + pushBookmarks(bookmark.getAccount()); + } + } + leaveMuc(conversation); + } else { + conversation.endOtrIfNeeded(); + } + this.databaseBackend.updateConversation(conversation); + this.conversations.remove(conversation); + updateConversationUi(); + } + + public void clearConversationHistory(Conversation conversation) { + this.databaseBackend.deleteMessagesInConversation(conversation); + this.fileBackend.removeFiles(conversation); + conversation.getMessages().clear(); + updateConversationUi(); + } + + public int getConversationCount() { + return this.databaseBackend.getConversationCount(); + } + + public void createAccount(Account account) { + databaseBackend.createAccount(account); + this.accounts.add(account); + this.reconnectAccount(account, false); + updateAccountUi(); + } + + public void updateAccount(Account account) { + this.statusListener.onStatusChanged(account); + databaseBackend.updateAccount(account); + reconnectAccount(account, false); + updateAccountUi(); + UIHelper.showErrorNotification(getApplicationContext(), getAccounts()); + } + + public void deleteAccount(Account account) { + for (Conversation conversation : conversations) { + if (conversation.getAccount() == account) { + if (conversation.getMode() == Conversation.MODE_MULTI) { + leaveMuc(conversation); + } else if (conversation.getMode() == Conversation.MODE_SINGLE) { + conversation.endOtrIfNeeded(); + } + conversations.remove(conversation); + } + } + if (account.getXmppConnection() != null) { + this.disconnect(account, true); + } + databaseBackend.deleteAccount(account); + this.accounts.remove(account); + updateAccountUi(); + UIHelper.showErrorNotification(getApplicationContext(), getAccounts()); + } + + public void setOnConversationListChangedListener( + OnConversationUpdate listener) { + if (!isScreenOn()) { + Log.d(Config.LOGTAG, + "ignoring setOnConversationListChangedListener"); + return; + } + synchronized (this.convChangedListenerCount) { + if (checkListeners()) { + switchToForeground(); + } + this.mOnConversationUpdate = listener; + this.mNotificationService.setIsInForeground(true); + this.convChangedListenerCount++; + } + } + + public void removeOnConversationListChangedListener() { + synchronized (this.convChangedListenerCount) { + this.convChangedListenerCount--; + if (this.convChangedListenerCount <= 0) { + this.convChangedListenerCount = 0; + this.mOnConversationUpdate = null; + this.mNotificationService.setIsInForeground(false); + if (checkListeners()) { + switchToBackground(); + } + } + } + } + + public void setOnAccountListChangedListener(OnAccountUpdate listener) { + if (!isScreenOn()) { + Log.d(Config.LOGTAG, "ignoring setOnAccountListChangedListener"); + return; + } + synchronized (this.accountChangedListenerCount) { + if (checkListeners()) { + switchToForeground(); + } + this.mOnAccountUpdate = listener; + this.accountChangedListenerCount++; + } + } + + public void removeOnAccountListChangedListener() { + synchronized (this.accountChangedListenerCount) { + this.accountChangedListenerCount--; + if (this.accountChangedListenerCount <= 0) { + this.mOnAccountUpdate = null; + this.accountChangedListenerCount = 0; + if (checkListeners()) { + switchToBackground(); + } + } + } + } + + public void setOnRosterUpdateListener(OnRosterUpdate listener) { + if (!isScreenOn()) { + Log.d(Config.LOGTAG, "ignoring setOnRosterUpdateListener"); + return; + } + synchronized (this.rosterChangedListenerCount) { + if (checkListeners()) { + switchToForeground(); + } + this.mOnRosterUpdate = listener; + this.rosterChangedListenerCount++; + } + } + + public void removeOnRosterUpdateListener() { + synchronized (this.rosterChangedListenerCount) { + this.rosterChangedListenerCount--; + if (this.rosterChangedListenerCount <= 0) { + this.rosterChangedListenerCount = 0; + this.mOnRosterUpdate = null; + if (checkListeners()) { + switchToBackground(); + } + } + } + } + + private boolean checkListeners() { + return (this.mOnAccountUpdate == null + && this.mOnConversationUpdate == null && this.mOnRosterUpdate == null); + } + + private void switchToForeground() { + for (Account account : getAccounts()) { + if (account.getStatus() == Account.STATUS_ONLINE) { + XmppConnection connection = account.getXmppConnection(); + if (connection != null && connection.getFeatures().csi()) { + connection.sendActive(); + } + } + } + Log.d(Config.LOGTAG, "app switched into foreground"); + } + + private void switchToBackground() { + for (Account account : getAccounts()) { + if (account.getStatus() == Account.STATUS_ONLINE) { + XmppConnection connection = account.getXmppConnection(); + if (connection != null && connection.getFeatures().csi()) { + connection.sendInactive(); + } + } + } + this.mNotificationService.setIsInForeground(false); + Log.d(Config.LOGTAG, "app switched into background"); + } + + private boolean isScreenOn() { + PowerManager pm = (PowerManager) this + .getSystemService(Context.POWER_SERVICE); + return pm.isScreenOn(); + } + + public void connectMultiModeConversations(Account account) { + List conversations = getConversations(); + for (int i = 0; i < conversations.size(); i++) { + Conversation conversation = conversations.get(i); + if ((conversation.getMode() == Conversation.MODE_MULTI) + && (conversation.getAccount() == account)) { + joinMuc(conversation); + } + } + } + + public void joinMuc(Conversation conversation) { + Account account = conversation.getAccount(); + account.pendingConferenceJoins.remove(conversation); + account.pendingConferenceLeaves.remove(conversation); + if (account.getStatus() == Account.STATUS_ONLINE) { + Log.d(Config.LOGTAG, + "joining conversation " + conversation.getContactJid()); + String nick = conversation.getMucOptions().getProposedNick(); + conversation.getMucOptions().setJoinNick(nick); + PresencePacket packet = new PresencePacket(); + String joinJid = conversation.getMucOptions().getJoinJid(); + packet.setAttribute("to", conversation.getMucOptions().getJoinJid()); + Element x = new Element("x"); + x.setAttribute("xmlns", "http://jabber.org/protocol/muc"); + if (conversation.getMucOptions().getPassword() != null) { + Element password = x.addChild("password"); + password.setContent(conversation.getMucOptions().getPassword()); + } + String sig = account.getPgpSignature(); + if (sig != null) { + packet.addChild("status").setContent("online"); + packet.addChild("x", "jabber:x:signed").setContent(sig); + } + if (conversation.getMessages().size() != 0) { + final SimpleDateFormat mDateFormat = new SimpleDateFormat( + "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US); + mDateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + Date date = new Date(conversation.getLatestMessage() + .getTimeSent() + 1000); + x.addChild("history").setAttribute("since", + mDateFormat.format(date)); + } + packet.addChild(x); + sendPresencePacket(account, packet); + if (!joinJid.equals(conversation.getContactJid())) { + conversation.setContactJid(joinJid); + databaseBackend.updateConversation(conversation); + } + } else { + account.pendingConferenceJoins.add(conversation); + } + } + + private OnRenameListener renameListener = null; + private IqGenerator mIqGenerator = new IqGenerator(this); + + public void setOnRenameListener(OnRenameListener listener) { + this.renameListener = listener; + } + + public void providePasswordForMuc(Conversation conversation, String password) { + if (conversation.getMode() == Conversation.MODE_MULTI) { + conversation.getMucOptions().setPassword(password); + if (conversation.getBookmark() != null) { + conversation.getBookmark().setAutojoin(true); + pushBookmarks(conversation.getAccount()); + } + databaseBackend.updateConversation(conversation); + joinMuc(conversation); + } + } + + public void renameInMuc(final Conversation conversation, final String nick) { + final MucOptions options = conversation.getMucOptions(); + options.setJoinNick(nick); + if (options.online()) { + Account account = conversation.getAccount(); + options.setOnRenameListener(new OnRenameListener() { + + @Override + public void onRename(boolean success) { + if (renameListener != null) { + renameListener.onRename(success); + } + if (success) { + conversation.setContactJid(conversation.getMucOptions() + .getJoinJid()); + databaseBackend.updateConversation(conversation); + Bookmark bookmark = conversation.getBookmark(); + if (bookmark != null) { + bookmark.setNick(nick); + pushBookmarks(bookmark.getAccount()); + } + } + } + }); + options.flagAboutToRename(); + PresencePacket packet = new PresencePacket(); + packet.setAttribute("to", options.getJoinJid()); + packet.setAttribute("from", conversation.getAccount().getFullJid()); + + String sig = account.getPgpSignature(); + if (sig != null) { + packet.addChild("status").setContent("online"); + packet.addChild("x", "jabber:x:signed").setContent(sig); + } + sendPresencePacket(account, packet); + } else { + conversation.setContactJid(options.getJoinJid()); + databaseBackend.updateConversation(conversation); + if (conversation.getAccount().getStatus() == Account.STATUS_ONLINE) { + Bookmark bookmark = conversation.getBookmark(); + if (bookmark != null) { + bookmark.setNick(nick); + pushBookmarks(bookmark.getAccount()); + } + joinMuc(conversation); + } + } + } + + public void leaveMuc(Conversation conversation) { + Account account = conversation.getAccount(); + account.pendingConferenceJoins.remove(conversation); + account.pendingConferenceLeaves.remove(conversation); + if (account.getStatus() == Account.STATUS_ONLINE) { + PresencePacket packet = new PresencePacket(); + packet.setAttribute("to", conversation.getMucOptions().getJoinJid()); + packet.setAttribute("from", conversation.getAccount().getFullJid()); + packet.setAttribute("type", "unavailable"); + sendPresencePacket(conversation.getAccount(), packet); + conversation.getMucOptions().setOffline(); + conversation.deregisterWithBookmark(); + Log.d(Config.LOGTAG, conversation.getAccount().getJid() + + ": leaving muc " + conversation.getContactJid()); + } else { + account.pendingConferenceLeaves.add(conversation); + } + } + + public void disconnect(Account account, boolean force) { + if ((account.getStatus() == Account.STATUS_ONLINE) + || (account.getStatus() == Account.STATUS_DISABLED)) { + if (!force) { + List conversations = getConversations(); + for (int i = 0; i < conversations.size(); i++) { + Conversation conversation = conversations.get(i); + if (conversation.getAccount() == account) { + if (conversation.getMode() == Conversation.MODE_MULTI) { + leaveMuc(conversation); + } else { + if (conversation.endOtrIfNeeded()) { + Log.d(Config.LOGTAG, account.getJid() + + ": ended otr session with " + + conversation.getContactJid()); + } + } + } + } + } + account.getXmppConnection().disconnect(force); + } + } + + @Override + public IBinder onBind(Intent intent) { + return mBinder; + } + + public void updateMessage(Message message) { + databaseBackend.updateMessage(message); + updateConversationUi(); + } + + protected void syncDirtyContacts(Account account) { + for (Contact contact : account.getRoster().getContacts()) { + if (contact.getOption(Contact.Options.DIRTY_PUSH)) { + pushContactToServer(contact); + } + if (contact.getOption(Contact.Options.DIRTY_DELETE)) { + deleteContactOnServer(contact); + } + } + } + + public void createContact(Contact contact) { + SharedPreferences sharedPref = getPreferences(); + boolean autoGrant = sharedPref.getBoolean("grant_new_contacts", true); + if (autoGrant) { + contact.setOption(Contact.Options.PREEMPTIVE_GRANT); + contact.setOption(Contact.Options.ASKING); + } + pushContactToServer(contact); + } + + public void onOtrSessionEstablished(Conversation conversation) { + Account account = conversation.getAccount(); + List messages = conversation.getMessages(); + Session otrSession = conversation.getOtrSession(); + Log.d(Config.LOGTAG, + account.getJid() + " otr session established with " + + conversation.getContactJid() + "/" + + otrSession.getSessionID().getUserID()); + for (int i = 0; i < messages.size(); ++i) { + Message msg = messages.get(i); + if ((msg.getStatus() == Message.STATUS_UNSEND || msg.getStatus() == Message.STATUS_WAITING) + && (msg.getEncryption() == Message.ENCRYPTION_OTR)) { + msg.setPresence(otrSession.getSessionID().getUserID()); + if (msg.getType() == Message.TYPE_TEXT) { + MessagePacket outPacket = mMessageGenerator + .generateOtrChat(msg, true); + if (outPacket != null) { + msg.setStatus(Message.STATUS_SEND); + databaseBackend.updateMessage(msg); + sendMessagePacket(account, outPacket); + } + } else if (msg.getType() == Message.TYPE_IMAGE) { + mJingleConnectionManager.createNewConnection(msg); + } + } + } + updateConversationUi(); + } + + public boolean renewSymmetricKey(Conversation conversation) { + Account account = conversation.getAccount(); + byte[] symmetricKey = new byte[32]; + this.mRandom.nextBytes(symmetricKey); + Session otrSession = conversation.getOtrSession(); + if (otrSession != null) { + MessagePacket packet = new MessagePacket(); + packet.setType(MessagePacket.TYPE_CHAT); + packet.setFrom(account.getFullJid()); + packet.addChild("private", "urn:xmpp:carbons:2"); + packet.addChild("no-copy", "urn:xmpp:hints"); + packet.setTo(otrSession.getSessionID().getAccountID() + "/" + + otrSession.getSessionID().getUserID()); + try { + packet.setBody(otrSession + .transformSending(CryptoHelper.FILETRANSFER + + CryptoHelper.bytesToHex(symmetricKey))); + sendMessagePacket(account, packet); + conversation.setSymmetricKey(symmetricKey); + return true; + } catch (OtrException e) { + return false; + } + } + return false; + } + + public void pushContactToServer(Contact contact) { + contact.resetOption(Contact.Options.DIRTY_DELETE); + contact.setOption(Contact.Options.DIRTY_PUSH); + Account account = contact.getAccount(); + if (account.getStatus() == Account.STATUS_ONLINE) { + boolean ask = contact.getOption(Contact.Options.ASKING); + boolean sendUpdates = contact + .getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST) + && contact.getOption(Contact.Options.PREEMPTIVE_GRANT); + IqPacket iq = new IqPacket(IqPacket.TYPE_SET); + iq.query("jabber:iq:roster").addChild(contact.asElement()); + account.getXmppConnection().sendIqPacket(iq, null); + if (sendUpdates) { + sendPresencePacket(account, + mPresenceGenerator.sendPresenceUpdatesTo(contact)); + } + if (ask) { + sendPresencePacket(account, + mPresenceGenerator.requestPresenceUpdatesFrom(contact)); + } + } + } + + public void publishAvatar(Account account, Uri image, + final UiCallback callback) { + final Bitmap.CompressFormat format = Config.AVATAR_FORMAT; + final int size = Config.AVATAR_SIZE; + final Avatar avatar = getFileBackend() + .getPepAvatar(image, size, format); + if (avatar != null) { + avatar.height = size; + avatar.width = size; + if (format.equals(Bitmap.CompressFormat.WEBP)) { + avatar.type = "image/webp"; + } else if (format.equals(Bitmap.CompressFormat.JPEG)) { + avatar.type = "image/jpeg"; + } else if (format.equals(Bitmap.CompressFormat.PNG)) { + avatar.type = "image/png"; + } + if (!getFileBackend().save(avatar)) { + callback.error(R.string.error_saving_avatar, avatar); + return; + } + IqPacket packet = this.mIqGenerator.publishAvatar(avatar); + this.sendIqPacket(account, packet, new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, IqPacket result) { + if (result.getType() == IqPacket.TYPE_RESULT) { + IqPacket packet = XmppConnectionService.this.mIqGenerator + .publishAvatarMetadata(avatar); + sendIqPacket(account, packet, new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, + IqPacket result) { + if (result.getType() == IqPacket.TYPE_RESULT) { + if (account.setAvatar(avatar.getFilename())) { + databaseBackend.updateAccount(account); + } + callback.success(avatar); + } else { + callback.error( + R.string.error_publish_avatar_server_reject, + avatar); + } + } + }); + } else { + callback.error( + R.string.error_publish_avatar_server_reject, + avatar); + } + } + }); + } else { + callback.error(R.string.error_publish_avatar_converting, null); + } + } + + public void fetchAvatar(Account account, Avatar avatar) { + fetchAvatar(account, avatar, null); + } + + public void fetchAvatar(Account account, final Avatar avatar, + final UiCallback callback) { + IqPacket packet = this.mIqGenerator.retrieveAvatar(avatar); + sendIqPacket(account, packet, new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, IqPacket result) { + final String ERROR = account.getJid() + + ": fetching avatar for " + avatar.owner + " failed "; + if (result.getType() == IqPacket.TYPE_RESULT) { + avatar.image = mIqParser.avatarData(result); + if (avatar.image != null) { + if (getFileBackend().save(avatar)) { + if (account.getJid().equals(avatar.owner)) { + if (account.setAvatar(avatar.getFilename())) { + databaseBackend.updateAccount(account); + } + getAvatarService().clear(account); + updateConversationUi(); + updateAccountUi(); + } else { + Contact contact = account.getRoster() + .getContact(avatar.owner); + contact.setAvatar(avatar.getFilename()); + getAvatarService().clear(contact); + updateConversationUi(); + updateRosterUi(); + } + if (callback != null) { + callback.success(avatar); + } + Log.d(Config.LOGTAG, account.getJid() + + ": succesfully fetched avatar for " + + avatar.owner); + return; + } + } else { + + Log.d(Config.LOGTAG, ERROR + "(parsing error)"); + } + } else { + Element error = result.findChild("error"); + if (error == null) { + Log.d(Config.LOGTAG, ERROR + "(server error)"); + } else { + Log.d(Config.LOGTAG, ERROR + error.toString()); + } + } + if (callback != null) { + callback.error(0, null); + } + + } + }); + } + + public void checkForAvatar(Account account, + final UiCallback callback) { + IqPacket packet = this.mIqGenerator.retrieveAvatarMetaData(null); + this.sendIqPacket(account, packet, new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (packet.getType() == IqPacket.TYPE_RESULT) { + Element pubsub = packet.findChild("pubsub", + "http://jabber.org/protocol/pubsub"); + if (pubsub != null) { + Element items = pubsub.findChild("items"); + if (items != null) { + Avatar avatar = Avatar.parseMetadata(items); + if (avatar != null) { + avatar.owner = account.getJid(); + if (fileBackend.isAvatarCached(avatar)) { + if (account.setAvatar(avatar.getFilename())) { + databaseBackend.updateAccount(account); + } + getAvatarService().clear(account); + callback.success(avatar); + } else { + fetchAvatar(account, avatar, callback); + } + return; + } + } + } + } + callback.error(0, null); + } + }); + } + + public void deleteContactOnServer(Contact contact) { + contact.resetOption(Contact.Options.PREEMPTIVE_GRANT); + contact.resetOption(Contact.Options.DIRTY_PUSH); + contact.setOption(Contact.Options.DIRTY_DELETE); + Account account = contact.getAccount(); + if (account.getStatus() == Account.STATUS_ONLINE) { + IqPacket iq = new IqPacket(IqPacket.TYPE_SET); + Element item = iq.query("jabber:iq:roster").addChild("item"); + item.setAttribute("jid", contact.getJid()); + item.setAttribute("subscription", "remove"); + account.getXmppConnection().sendIqPacket(iq, null); + } + } + + public void updateConversation(Conversation conversation) { + this.databaseBackend.updateConversation(conversation); + } + + public void reconnectAccount(final Account account, final boolean force) { + new Thread(new Runnable() { + + @Override + public void run() { + if (account.getXmppConnection() != null) { + disconnect(account, force); + } + if (!account.isOptionSet(Account.OPTION_DISABLED)) { + if (account.getXmppConnection() == null) { + account.setXmppConnection(createConnection(account)); + } + Thread thread = new Thread(account.getXmppConnection()); + thread.start(); + scheduleWakeupCall((int) (Config.CONNECT_TIMEOUT * 1.2), + false); + } else { + account.getRoster().clearPresences(); + account.setXmppConnection(null); + } + } + }).start(); + } + + public void invite(Conversation conversation, String contact) { + MessagePacket packet = mMessageGenerator.invite(conversation, contact); + sendMessagePacket(conversation.getAccount(), packet); + } + + public void resetSendingToWaiting(Account account) { + for (Conversation conversation : getConversations()) { + if (conversation.getAccount() == account) { + for (Message message : conversation.getMessages()) { + if (message.getType() != Message.TYPE_IMAGE + && message.getStatus() == Message.STATUS_UNSEND) { + markMessage(message, Message.STATUS_WAITING); + } + } + } + } + } + + public boolean markMessage(Account account, String recipient, String uuid, + int status) { + if (uuid == null) { + return false; + } else { + for (Conversation conversation : getConversations()) { + if (conversation.getContactJid().equals(recipient) + && conversation.getAccount().equals(account)) { + return markMessage(conversation, uuid, status); + } + } + return false; + } + } + + public boolean markMessage(Conversation conversation, String uuid, + int status) { + if (uuid == null) { + return false; + } else { + for (Message message : conversation.getMessages()) { + if (uuid.equals(message.getUuid()) + || (message.getStatus() >= Message.STATUS_SEND && uuid + .equals(message.getRemoteMsgId()))) { + markMessage(message, status); + return true; + } + } + return false; + } + } + + public void markMessage(Message message, int status) { + if (status == Message.STATUS_SEND_FAILED + && (message.getStatus() == Message.STATUS_SEND_RECEIVED || message + .getStatus() == Message.STATUS_SEND_DISPLAYED)) { + return; + } + message.setStatus(status); + databaseBackend.updateMessage(message); + updateConversationUi(); + } + + public SharedPreferences getPreferences() { + return PreferenceManager + .getDefaultSharedPreferences(getApplicationContext()); + } + + public boolean forceEncryption() { + return getPreferences().getBoolean("force_encryption", false); + } + + public boolean confirmMessages() { + return getPreferences().getBoolean("confirm_messages", true); + } + + public boolean saveEncryptedMessages() { + return !getPreferences().getBoolean("dont_save_encrypted", false); + } + + public boolean indicateReceived() { + return getPreferences().getBoolean("indicate_received", false); + } + + public void updateConversationUi() { + if (mOnConversationUpdate != null) { + mOnConversationUpdate.onConversationUpdate(); + } + } + + public void updateAccountUi() { + if (mOnAccountUpdate != null) { + mOnAccountUpdate.onAccountUpdate(); + } + } + + public void updateRosterUi() { + if (mOnRosterUpdate != null) { + mOnRosterUpdate.onRosterUpdate(); + } + } + + public Account findAccountByJid(String accountJid) { + for (Account account : this.accounts) { + if (account.getJid().equals(accountJid)) { + return account; + } + } + return null; + } + + public Conversation findConversationByUuid(String uuid) { + for (Conversation conversation : getConversations()) { + if (conversation.getUuid().equals(uuid)) { + return conversation; + } + } + return null; + } + + public void markRead(Conversation conversation, boolean calledByUi) { + mNotificationService.clear(conversation); + String id = conversation.getLatestMarkableMessageId(); + conversation.markRead(); + if (confirmMessages() && id != null && calledByUi) { + Log.d(Config.LOGTAG, conversation.getAccount().getJid() + + ": sending read marker for " + conversation.getName()); + Account account = conversation.getAccount(); + String to = conversation.getContactJid(); + this.sendMessagePacket(conversation.getAccount(), + mMessageGenerator.confirm(account, to, id)); + } + if (!calledByUi) { + updateConversationUi(); + } + } + + public void failWaitingOtrMessages(Conversation conversation) { + for (Message message : conversation.getMessages()) { + if (message.getEncryption() == Message.ENCRYPTION_OTR + && message.getStatus() == Message.STATUS_WAITING) { + markMessage(message, Message.STATUS_SEND_FAILED); + } + } + } + + public SecureRandom getRNG() { + return this.mRandom; + } + + public MemorizingTrustManager getMemorizingTrustManager() { + return this.mMemorizingTrustManager; + } + + public PowerManager getPowerManager() { + return this.pm; + } + + public LruCache getBitmapCache() { + return this.mBitmapCache; + } + + public void replyWithNotAcceptable(Account account, MessagePacket packet) { + if (account.getStatus() == Account.STATUS_ONLINE) { + MessagePacket error = this.mMessageGenerator + .generateNotAcceptable(packet); + sendMessagePacket(account, error); + } + } + + public void syncRosterToDisk(final Account account) { + new Thread(new Runnable() { + + @Override + public void run() { + databaseBackend.writeRoster(account.getRoster()); + } + }).start(); + + } + + public List getKnownHosts() { + List hosts = new ArrayList(); + for (Account account : getAccounts()) { + if (!hosts.contains(account.getServer())) { + hosts.add(account.getServer()); + } + for (Contact contact : account.getRoster().getContacts()) { + if (contact.showInRoster()) { + String server = contact.getServer(); + if (server != null && !hosts.contains(server)) { + hosts.add(server); + } + } + } + } + return hosts; + } + + public List getKnownConferenceHosts() { + ArrayList mucServers = new ArrayList(); + for (Account account : accounts) { + if (account.getXmppConnection() != null) { + String server = account.getXmppConnection().getMucServer(); + if (server != null && !mucServers.contains(server)) { + mucServers.add(server); + } + } + } + return mucServers; + } + + public void sendMessagePacket(Account account, MessagePacket packet) { + XmppConnection connection = account.getXmppConnection(); + if (connection != null) { + connection.sendMessagePacket(packet); + } + } + + public void sendPresencePacket(Account account, PresencePacket packet) { + XmppConnection connection = account.getXmppConnection(); + if (connection != null) { + connection.sendPresencePacket(packet); + } + } + + public void sendIqPacket(Account account, IqPacket packet, + OnIqPacketReceived callback) { + XmppConnection connection = account.getXmppConnection(); + if (connection != null) { + connection.sendIqPacket(packet, callback); + } + } + + public MessageGenerator getMessageGenerator() { + return this.mMessageGenerator; + } + + public PresenceGenerator getPresenceGenerator() { + return this.mPresenceGenerator; + } + + public IqGenerator getIqGenerator() { + return this.mIqGenerator; + } + + public JingleConnectionManager getJingleConnectionManager() { + return this.mJingleConnectionManager; + } + + public interface OnConversationUpdate { + public void onConversationUpdate(); + } + + public interface OnAccountUpdate { + public void onAccountUpdate(); + } + + public interface OnRosterUpdate { + public void onRosterUpdate(); + } + + public List findContacts(String jid) { + ArrayList contacts = new ArrayList(); + for (Account account : getAccounts()) { + if (!account.isOptionSet(Account.OPTION_DISABLED)) { + Contact contact = account.getRoster().getContactFromRoster(jid); + if (contact != null) { + contacts.add(contact); + } + } + } + return contacts; + } + + public NotificationService getNotificationService() { + return this.mNotificationService; + } + + public HttpConnectionManager getHttpConnectionManager() { + return this.mHttpConnectionManager; + } + + private class DeletedDownloadable implements Downloadable { + + @Override + public boolean start() { + return false; + } + + @Override + public int getStatus() { + return Downloadable.STATUS_DELETED; + } + + @Override + public long getFileSize() { + return 0; + } + + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java b/conversations/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java new file mode 100644 index 00000000..62a2cbe1 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java @@ -0,0 +1,145 @@ +package eu.siacs.conversations.ui; + +import java.util.ArrayList; +import java.util.Collections; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.EditText; +import android.widget.ListView; +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.ListItem; +import eu.siacs.conversations.ui.adapter.ListItemAdapter; + +public class ChooseContactActivity extends XmppActivity { + + private ListView mListView; + private ArrayList contacts = new ArrayList(); + private ArrayAdapter mContactsAdapter; + + private EditText mSearchEditText; + + private TextWatcher mSearchTextWatcher = new TextWatcher() { + + @Override + public void afterTextChanged(Editable editable) { + filterContacts(editable.toString()); + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, + int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, + int count) { + } + }; + + private MenuItem.OnActionExpandListener mOnActionExpandListener = new MenuItem.OnActionExpandListener() { + + @Override + public boolean onMenuItemActionExpand(MenuItem item) { + mSearchEditText.post(new Runnable() { + + @Override + public void run() { + mSearchEditText.requestFocus(); + InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + imm.showSoftInput(mSearchEditText, + InputMethodManager.SHOW_IMPLICIT); + } + }); + + return true; + } + + @Override + public boolean onMenuItemActionCollapse(MenuItem item) { + InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(mSearchEditText.getWindowToken(), + InputMethodManager.HIDE_IMPLICIT_ONLY); + mSearchEditText.setText(""); + filterContacts(null); + return true; + } + }; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_choose_contact); + mListView = (ListView) findViewById(R.id.choose_contact_list); + mListView.setFastScrollEnabled(true); + mContactsAdapter = new ListItemAdapter(this, contacts); + mListView.setAdapter(mContactsAdapter); + mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + + @Override + public void onItemClick(AdapterView arg0, View arg1, + int position, long arg3) { + InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(mSearchEditText.getWindowToken(), + InputMethodManager.HIDE_IMPLICIT_ONLY); + Intent request = getIntent(); + Intent data = new Intent(); + ListItem mListItem = contacts.get(position); + data.putExtra("contact", mListItem.getJid()); + String account = request.getStringExtra("account"); + if (account == null && mListItem instanceof Contact) { + account = ((Contact) mListItem).getAccount().getJid(); + } + data.putExtra("account", account); + data.putExtra("conversation", + request.getStringExtra("conversation")); + setResult(RESULT_OK, data); + finish(); + } + }); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.choose_contact, menu); + MenuItem menuSearchView = (MenuItem) menu.findItem(R.id.action_search); + View mSearchView = menuSearchView.getActionView(); + mSearchEditText = (EditText) mSearchView + .findViewById(R.id.search_field); + mSearchEditText.addTextChangedListener(mSearchTextWatcher); + menuSearchView.setOnActionExpandListener(mOnActionExpandListener); + return true; + } + + @Override + void onBackendConnected() { + filterContacts(null); + } + + protected void filterContacts(String needle) { + this.contacts.clear(); + for (Account account : xmppConnectionService.getAccounts()) { + if (account.getStatus() != Account.STATUS_DISABLED) { + for (Contact contact : account.getRoster().getContacts()) { + if (contact.showInRoster() && contact.match(needle)) { + this.contacts.add(contact); + } + } + } + } + Collections.sort(this.contacts); + mContactsAdapter.notifyDataSetChanged(); + } + +} diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java b/conversations/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java new file mode 100644 index 00000000..6b4642cb --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java @@ -0,0 +1,280 @@ +package eu.siacs.conversations.ui; + +import java.util.ArrayList; +import java.util.List; + +import org.openintents.openpgp.util.OpenPgpUtils; + +import eu.siacs.conversations.R; +import eu.siacs.conversations.crypto.PgpEngine; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.MucOptions; +import eu.siacs.conversations.entities.MucOptions.OnRenameListener; +import eu.siacs.conversations.entities.MucOptions.User; +import eu.siacs.conversations.services.XmppConnectionService.OnConversationUpdate; +import eu.siacs.conversations.xmpp.stanzas.MessagePacket; +import android.app.PendingIntent; +import android.content.Context; +import android.content.IntentSender.SendIntentException; +import android.graphics.Bitmap; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; + +public class ConferenceDetailsActivity extends XmppActivity { + public static final String ACTION_VIEW_MUC = "view_muc"; + private Conversation conversation; + private TextView mYourNick; + private ImageView mYourPhoto; + private ImageButton mEditNickButton; + private TextView mRoleAffiliaton; + private TextView mFullJid; + private TextView mAccountJid; + private LinearLayout membersView; + private LinearLayout mMoreDetails; + private Button mInviteButton; + private String uuid = null; + + private OnClickListener inviteListener = new OnClickListener() { + + @Override + public void onClick(View v) { + inviteToConversation(conversation); + } + }; + + private List users = new ArrayList(); + private OnConversationUpdate onConvChanged = new OnConversationUpdate() { + + @Override + public void onConversationUpdate() { + runOnUiThread(new Runnable() { + + @Override + public void run() { + populateView(); + } + }); + } + }; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_muc_details); + mYourNick = (TextView) findViewById(R.id.muc_your_nick); + mYourPhoto = (ImageView) findViewById(R.id.your_photo); + mEditNickButton = (ImageButton) findViewById(R.id.edit_nick_button); + mFullJid = (TextView) findViewById(R.id.muc_jabberid); + membersView = (LinearLayout) findViewById(R.id.muc_members); + mAccountJid = (TextView) findViewById(R.id.details_account); + mMoreDetails = (LinearLayout) findViewById(R.id.muc_more_details); + mMoreDetails.setVisibility(View.GONE); + mInviteButton = (Button) findViewById(R.id.invite); + mInviteButton.setOnClickListener(inviteListener); + getActionBar().setHomeButtonEnabled(true); + getActionBar().setDisplayHomeAsUpEnabled(true); + mEditNickButton.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + quickEdit(conversation.getMucOptions().getActualNick(), + new OnValueEdited() { + + @Override + public void onValueEdited(String value) { + xmppConnectionService.renameInMuc(conversation, + value); + } + }); + } + }); + } + + @Override + public boolean onOptionsItemSelected(MenuItem menuItem) { + switch (menuItem.getItemId()) { + case android.R.id.home: + finish(); + break; + case R.id.action_edit_subject: + if (conversation != null) { + quickEdit(conversation.getName(), new OnValueEdited() { + + @Override + public void onValueEdited(String value) { + MessagePacket packet = xmppConnectionService + .getMessageGenerator().conferenceSubject( + conversation, value); + xmppConnectionService.sendMessagePacket( + conversation.getAccount(), packet); + } + }); + } + break; + } + return super.onOptionsItemSelected(menuItem); + } + + public String getReadableRole(int role) { + switch (role) { + case User.ROLE_MODERATOR: + return getString(R.string.moderator); + case User.ROLE_PARTICIPANT: + return getString(R.string.participant); + case User.ROLE_VISITOR: + return getString(R.string.visitor); + default: + return ""; + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.muc_details, menu); + return true; + } + + @Override + void onBackendConnected() { + registerListener(); + if (getIntent().getAction().equals(ACTION_VIEW_MUC)) { + this.uuid = getIntent().getExtras().getString("uuid"); + } + if (uuid != null) { + this.conversation = xmppConnectionService + .findConversationByUuid(uuid); + if (this.conversation != null) { + populateView(); + } + } + } + + @Override + protected void onStop() { + if (xmppConnectionServiceBound) { + xmppConnectionService.removeOnConversationListChangedListener(); + } + super.onStop(); + } + + protected void registerListener() { + xmppConnectionService + .setOnConversationListChangedListener(this.onConvChanged); + xmppConnectionService.setOnRenameListener(new OnRenameListener() { + + @Override + public void onRename(final boolean success) { + runOnUiThread(new Runnable() { + + @Override + public void run() { + populateView(); + if (success) { + Toast.makeText( + ConferenceDetailsActivity.this, + getString(R.string.your_nick_has_been_changed), + Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(ConferenceDetailsActivity.this, + getString(R.string.nick_in_use), + Toast.LENGTH_SHORT).show(); + } + } + }); + } + }); + } + + private void populateView() { + mAccountJid.setText(getString(R.string.using_account, conversation + .getAccount().getJid())); + mYourPhoto.setImageBitmap(avatarService().get( + conversation.getAccount(), getPixel(48))); + setTitle(conversation.getName()); + mFullJid.setText(conversation.getContactJid().split("/", 2)[0]); + mYourNick.setText(conversation.getMucOptions().getActualNick()); + mRoleAffiliaton = (TextView) findViewById(R.id.muc_role); + if (conversation.getMucOptions().online()) { + mMoreDetails.setVisibility(View.VISIBLE); + User self = conversation.getMucOptions().getSelf(); + switch (self.getAffiliation()) { + case User.AFFILIATION_ADMIN: + mRoleAffiliaton.setText(getReadableRole(self.getRole()) + " (" + + getString(R.string.admin) + ")"); + break; + case User.AFFILIATION_OWNER: + mRoleAffiliaton.setText(getReadableRole(self.getRole()) + " (" + + getString(R.string.owner) + ")"); + break; + default: + mRoleAffiliaton.setText(getReadableRole(self.getRole())); + break; + } + } + this.users.clear(); + this.users.addAll(conversation.getMucOptions().getUsers()); + LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); + membersView.removeAllViews(); + for (final User user : conversation.getMucOptions().getUsers()) { + View view = (View) inflater.inflate(R.layout.contact, membersView, + false); + TextView name = (TextView) view + .findViewById(R.id.contact_display_name); + TextView key = (TextView) view.findViewById(R.id.key); + TextView role = (TextView) view.findViewById(R.id.contact_jid); + if (user.getPgpKeyId() != 0) { + key.setVisibility(View.VISIBLE); + key.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + viewPgpKey(user); + } + }); + key.setText(OpenPgpUtils.convertKeyIdToHex(user.getPgpKeyId())); + } + Bitmap bm; + Contact contact = user.getContact(); + if (contact != null) { + bm = avatarService().get(contact, getPixel(48)); + name.setText(contact.getDisplayName()); + role.setText(user.getName() + " \u2022 " + + getReadableRole(user.getRole())); + } else { + bm = avatarService().get(user.getName(), getPixel(48)); + name.setText(user.getName()); + role.setText(getReadableRole(user.getRole())); + } + ImageView iv = (ImageView) view.findViewById(R.id.contact_photo); + iv.setImageBitmap(bm); + membersView.addView(view); + } + } + + private void viewPgpKey(User user) { + PgpEngine pgp = xmppConnectionService.getPgpEngine(); + if (pgp != null) { + PendingIntent intent = pgp.getIntentForKey( + conversation.getAccount(), user.getPgpKeyId()); + if (intent != null) { + try { + startIntentSenderForResult(intent.getIntentSender(), 0, + null, 0, 0, 0); + } catch (SendIntentException e) { + + } + } + } + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/conversations/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java new file mode 100644 index 00000000..ae26466e --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java @@ -0,0 +1,436 @@ +package eu.siacs.conversations.ui; + +import java.util.Iterator; + +import org.openintents.openpgp.util.OpenPgpUtils; + +import android.app.AlertDialog; +import android.app.PendingIntent; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentSender.SendIntentException; +import android.net.Uri; +import android.os.Bundle; +import android.provider.ContactsContract.CommonDataKinds; +import android.provider.ContactsContract.Contacts; +import android.provider.ContactsContract.Intents; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.CheckBox; +import android.widget.CompoundButton.OnCheckedChangeListener; +import android.widget.CompoundButton; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import android.widget.QuickContactBadge; +import android.widget.TextView; +import eu.siacs.conversations.R; +import eu.siacs.conversations.crypto.PgpEngine; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Presences; +import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate; +import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate; +import eu.siacs.conversations.utils.UIHelper; + +public class ContactDetailsActivity extends XmppActivity { + public static final String ACTION_VIEW_CONTACT = "view_contact"; + + private Contact contact; + + private String accountJid; + private String contactJid; + + private TextView contactJidTv; + private TextView accountJidTv; + private TextView status; + private TextView lastseen; + private CheckBox send; + private CheckBox receive; + private QuickContactBadge badge; + + private DialogInterface.OnClickListener removeFromRoster = new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + ContactDetailsActivity.this.xmppConnectionService + .deleteContactOnServer(contact); + ContactDetailsActivity.this.finish(); + } + }; + + private DialogInterface.OnClickListener addToPhonebook = new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT); + intent.setType(Contacts.CONTENT_ITEM_TYPE); + intent.putExtra(Intents.Insert.IM_HANDLE, contact.getJid()); + intent.putExtra(Intents.Insert.IM_PROTOCOL, + CommonDataKinds.Im.PROTOCOL_JABBER); + intent.putExtra("finishActivityOnSaveCompleted", true); + ContactDetailsActivity.this.startActivityForResult(intent, 0); + } + }; + private OnClickListener onBadgeClick = new OnClickListener() { + + @Override + public void onClick(View v) { + AlertDialog.Builder builder = new AlertDialog.Builder( + ContactDetailsActivity.this); + builder.setTitle(getString(R.string.action_add_phone_book)); + builder.setMessage(getString(R.string.add_phone_book_text, + contact.getJid())); + builder.setNegativeButton(getString(R.string.cancel), null); + builder.setPositiveButton(getString(R.string.add), addToPhonebook); + builder.create().show(); + } + }; + + private LinearLayout keys; + + private OnRosterUpdate rosterUpdate = new OnRosterUpdate() { + + @Override + public void onRosterUpdate() { + runOnUiThread(new Runnable() { + + @Override + public void run() { + populateView(); + } + }); + } + }; + + private OnCheckedChangeListener mOnSendCheckedChange = new OnCheckedChangeListener() { + + @Override + public void onCheckedChanged(CompoundButton buttonView, + boolean isChecked) { + if (isChecked) { + if (contact + .getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) { + xmppConnectionService.sendPresencePacket(contact + .getAccount(), + xmppConnectionService.getPresenceGenerator() + .sendPresenceUpdatesTo(contact)); + } else { + contact.setOption(Contact.Options.PREEMPTIVE_GRANT); + } + } else { + contact.resetOption(Contact.Options.PREEMPTIVE_GRANT); + xmppConnectionService.sendPresencePacket(contact.getAccount(), + xmppConnectionService.getPresenceGenerator() + .stopPresenceUpdatesTo(contact)); + } + } + }; + + private OnCheckedChangeListener mOnReceiveCheckedChange = new OnCheckedChangeListener() { + + @Override + public void onCheckedChanged(CompoundButton buttonView, + boolean isChecked) { + if (isChecked) { + xmppConnectionService.sendPresencePacket(contact.getAccount(), + xmppConnectionService.getPresenceGenerator() + .requestPresenceUpdatesFrom(contact)); + } else { + xmppConnectionService.sendPresencePacket(contact.getAccount(), + xmppConnectionService.getPresenceGenerator() + .stopPresenceUpdatesFrom(contact)); + } + } + }; + + private OnAccountUpdate accountUpdate = new OnAccountUpdate() { + + @Override + public void onAccountUpdate() { + runOnUiThread(new Runnable() { + + @Override + public void run() { + populateView(); + } + }); + } + }; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (getIntent().getAction().equals(ACTION_VIEW_CONTACT)) { + this.accountJid = getIntent().getExtras().getString("account"); + this.contactJid = getIntent().getExtras().getString("contact"); + } + setContentView(R.layout.activity_contact_details); + + contactJidTv = (TextView) findViewById(R.id.details_contactjid); + accountJidTv = (TextView) findViewById(R.id.details_account); + status = (TextView) findViewById(R.id.details_contactstatus); + lastseen = (TextView) findViewById(R.id.details_lastseen); + send = (CheckBox) findViewById(R.id.details_send_presence); + receive = (CheckBox) findViewById(R.id.details_receive_presence); + badge = (QuickContactBadge) findViewById(R.id.details_contact_badge); + keys = (LinearLayout) findViewById(R.id.details_contact_keys); + getActionBar().setHomeButtonEnabled(true); + getActionBar().setDisplayHomeAsUpEnabled(true); + + } + + @Override + public boolean onOptionsItemSelected(MenuItem menuItem) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setNegativeButton(getString(R.string.cancel), null); + switch (menuItem.getItemId()) { + case android.R.id.home: + finish(); + break; + case R.id.action_delete_contact: + builder.setTitle(getString(R.string.action_delete_contact)) + .setMessage( + getString(R.string.remove_contact_text, + contact.getJid())) + .setPositiveButton(getString(R.string.delete), + removeFromRoster).create().show(); + break; + case R.id.action_edit_contact: + if (contact.getSystemAccount() == null) { + quickEdit(contact.getDisplayName(), new OnValueEdited() { + + @Override + public void onValueEdited(String value) { + contact.setServerName(value); + ContactDetailsActivity.this.xmppConnectionService + .pushContactToServer(contact); + populateView(); + } + }); + } else { + Intent intent = new Intent(Intent.ACTION_EDIT); + String[] systemAccount = contact.getSystemAccount().split("#"); + long id = Long.parseLong(systemAccount[0]); + Uri uri = Contacts.getLookupUri(id, systemAccount[1]); + intent.setDataAndType(uri, Contacts.CONTENT_ITEM_TYPE); + intent.putExtra("finishActivityOnSaveCompleted", true); + startActivity(intent); + } + break; + } + return super.onOptionsItemSelected(menuItem); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.contact_details, menu); + return true; + } + + private void populateView() { + send.setOnCheckedChangeListener(null); + receive.setOnCheckedChangeListener(null); + setTitle(contact.getDisplayName()); + if (contact.getOption(Contact.Options.FROM)) { + send.setText(R.string.send_presence_updates); + send.setChecked(true); + } else if (contact + .getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) { + send.setChecked(false); + send.setText(R.string.send_presence_updates); + } else { + send.setText(R.string.preemptively_grant); + if (contact.getOption(Contact.Options.PREEMPTIVE_GRANT)) { + send.setChecked(true); + } else { + send.setChecked(false); + } + } + if (contact.getOption(Contact.Options.TO)) { + receive.setText(R.string.receive_presence_updates); + receive.setChecked(true); + } else { + receive.setText(R.string.ask_for_presence_updates); + if (contact.getOption(Contact.Options.ASKING)) { + receive.setChecked(true); + } else { + receive.setChecked(false); + } + } + if (contact.getAccount().getStatus() == Account.STATUS_ONLINE) { + receive.setEnabled(true); + send.setEnabled(true); + } else { + receive.setEnabled(false); + send.setEnabled(false); + } + + send.setOnCheckedChangeListener(this.mOnSendCheckedChange); + receive.setOnCheckedChangeListener(this.mOnReceiveCheckedChange); + + lastseen.setText(UIHelper.lastseen(getApplicationContext(), + contact.lastseen.time)); + + switch (contact.getMostAvailableStatus()) { + case Presences.CHAT: + status.setText(R.string.contact_status_free_to_chat); + status.setTextColor(mColorGreen); + break; + case Presences.ONLINE: + status.setText(R.string.contact_status_online); + status.setTextColor(mColorGreen); + break; + case Presences.AWAY: + status.setText(R.string.contact_status_away); + status.setTextColor(mColorOrange); + break; + case Presences.XA: + status.setText(R.string.contact_status_extended_away); + status.setTextColor(mColorOrange); + break; + case Presences.DND: + status.setText(R.string.contact_status_do_not_disturb); + status.setTextColor(mColorRed); + break; + case Presences.OFFLINE: + status.setText(R.string.contact_status_offline); + status.setTextColor(mSecondaryTextColor); + break; + default: + status.setText(R.string.contact_status_offline); + status.setTextColor(mSecondaryTextColor); + break; + } + if (contact.getPresences().size() > 1) { + contactJidTv.setText(contact.getJid() + " (" + + contact.getPresences().size() + ")"); + } else { + contactJidTv.setText(contact.getJid()); + } + accountJidTv.setText(getString(R.string.using_account, contact + .getAccount().getJid())); + prepareContactBadge(badge, contact); + if (contact.getSystemAccount() == null) { + badge.setOnClickListener(onBadgeClick); + } + + keys.removeAllViews(); + boolean hasKeys = false; + LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); + for (Iterator iterator = contact.getOtrFingerprints() + .iterator(); iterator.hasNext();) { + hasKeys = true; + final String otrFingerprint = iterator.next(); + View view = (View) inflater.inflate(R.layout.contact_key, keys, + false); + TextView key = (TextView) view.findViewById(R.id.key); + TextView keyType = (TextView) view.findViewById(R.id.key_type); + ImageButton remove = (ImageButton) view + .findViewById(R.id.button_remove); + remove.setVisibility(View.VISIBLE); + keyType.setText("OTR Fingerprint"); + key.setText(otrFingerprint); + keys.addView(view); + remove.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + confirmToDeleteFingerprint(otrFingerprint); + } + }); + } + if (contact.getPgpKeyId() != 0) { + hasKeys = true; + View view = (View) inflater.inflate(R.layout.contact_key, keys, + false); + TextView key = (TextView) view.findViewById(R.id.key); + TextView keyType = (TextView) view.findViewById(R.id.key_type); + keyType.setText("PGP Key ID"); + key.setText(OpenPgpUtils.convertKeyIdToHex(contact.getPgpKeyId())); + view.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + PgpEngine pgp = ContactDetailsActivity.this.xmppConnectionService + .getPgpEngine(); + if (pgp != null) { + PendingIntent intent = pgp.getIntentForKey(contact); + if (intent != null) { + try { + startIntentSenderForResult( + intent.getIntentSender(), 0, null, 0, + 0, 0); + } catch (SendIntentException e) { + + } + } + } + } + }); + keys.addView(view); + } + if (hasKeys) { + keys.setVisibility(View.VISIBLE); + } else { + keys.setVisibility(View.GONE); + } + } + + private void prepareContactBadge(QuickContactBadge badge, Contact contact) { + if (contact.getSystemAccount() != null) { + String[] systemAccount = contact.getSystemAccount().split("#"); + long id = Long.parseLong(systemAccount[0]); + badge.assignContactUri(Contacts.getLookupUri(id, systemAccount[1])); + } + badge.setImageBitmap(avatarService().get(contact, getPixel(72))); + } + + protected void confirmToDeleteFingerprint(final String fingerprint) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(R.string.delete_fingerprint); + builder.setMessage(R.string.sure_delete_fingerprint); + builder.setNegativeButton(R.string.cancel, null); + builder.setPositiveButton(R.string.delete, + new android.content.DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + if (contact.deleteOtrFingerprint(fingerprint)) { + populateView(); + xmppConnectionService.syncRosterToDisk(contact + .getAccount()); + } + } + + }); + builder.create().show(); + } + + @Override + public void onBackendConnected() { + xmppConnectionService.setOnRosterUpdateListener(this.rosterUpdate); + xmppConnectionService + .setOnAccountListChangedListener(this.accountUpdate); + if ((accountJid != null) && (contactJid != null)) { + Account account = xmppConnectionService + .findAccountByJid(accountJid); + if (account == null) { + return; + } + this.contact = account.getRoster().getContact(contactJid); + populateView(); + } + } + + @Override + protected void onStop() { + super.onStop(); + xmppConnectionService.removeOnRosterUpdateListener(); + xmppConnectionService.removeOnAccountListChangedListener(); + } + +} diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java b/conversations/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java new file mode 100644 index 00000000..91e1c81f --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java @@ -0,0 +1,947 @@ +package eu.siacs.conversations.ui; + +import java.util.ArrayList; +import java.util.List; + +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate; +import eu.siacs.conversations.services.XmppConnectionService.OnConversationUpdate; +import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate; +import eu.siacs.conversations.ui.adapter.ConversationAdapter; +import eu.siacs.conversations.utils.ExceptionHelper; +import android.net.Uri; +import android.os.Bundle; +import android.os.SystemClock; +import android.provider.MediaStore; +import android.annotation.SuppressLint; +import android.app.ActionBar; +import android.app.AlertDialog; +import android.app.FragmentTransaction; +import android.app.PendingIntent; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.IntentSender.SendIntentException; +import android.content.Intent; +import android.support.v4.widget.SlidingPaneLayout; +import android.support.v4.widget.SlidingPaneLayout.PanelSlideListener; +import android.view.KeyEvent; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ArrayAdapter; +import android.widget.CheckBox; +import android.widget.ListView; +import android.widget.PopupMenu; +import android.widget.PopupMenu.OnMenuItemClickListener; +import android.widget.Toast; + +public class ConversationActivity extends XmppActivity implements + OnAccountUpdate, OnConversationUpdate, OnRosterUpdate { + + public static final String VIEW_CONVERSATION = "viewConversation"; + public static final String CONVERSATION = "conversationUuid"; + public static final String TEXT = "text"; + public static final String PRESENCE = "eu.siacs.conversations.presence"; + + public static final int REQUEST_SEND_MESSAGE = 0x0201; + public static final int REQUEST_DECRYPT_PGP = 0x0202; + private static final int REQUEST_ATTACH_FILE_DIALOG = 0x0203; + private static final int REQUEST_IMAGE_CAPTURE = 0x0204; + private static final int REQUEST_RECORD_AUDIO = 0x0205; + private static final int REQUEST_SEND_PGP_IMAGE = 0x0206; + public static final int REQUEST_ENCRYPT_MESSAGE = 0x0207; + + private static final int ATTACHMENT_CHOICE_CHOOSE_IMAGE = 0x0301; + private static final int ATTACHMENT_CHOICE_TAKE_PHOTO = 0x0302; + private static final int ATTACHMENT_CHOICE_RECORD_VOICE = 0x0303; + private static final String STATE_OPEN_CONVERSATION = "state_open_conversation"; + private static final String STATE_PANEL_OPEN = "state_panel_open"; + + private String mOpenConverstaion = null; + private boolean mPanelOpen = true; + + private View mContentView; + + private List conversationList = new ArrayList(); + private Conversation selectedConversation = null; + private ListView listView; + + private boolean paneShouldBeOpen = true; + private ArrayAdapter listAdapter; + + private Toast prepareImageToast; + + private Uri pendingImageUri = null; + + public List getConversationList() { + return this.conversationList; + } + + public Conversation getSelectedConversation() { + return this.selectedConversation; + } + + public void setSelectedConversation(Conversation conversation) { + this.selectedConversation = conversation; + } + + public ListView getConversationListView() { + return this.listView; + } + + public boolean shouldPaneBeOpen() { + return paneShouldBeOpen; + } + + public void showConversationsOverview() { + if (mContentView instanceof SlidingPaneLayout) { + SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView; + mSlidingPaneLayout.openPane(); + } + } + + public void hideConversationsOverview() { + if (mContentView instanceof SlidingPaneLayout) { + SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView; + mSlidingPaneLayout.closePane(); + } + } + + public boolean isConversationsOverviewHideable() { + if (mContentView instanceof SlidingPaneLayout) { + SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView; + return mSlidingPaneLayout.isSlideable(); + } else { + return false; + } + } + + public boolean isConversationsOverviewVisable() { + if (mContentView instanceof SlidingPaneLayout) { + SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView; + return mSlidingPaneLayout.isOpen(); + } else { + return true; + } + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (savedInstanceState != null) { + mOpenConverstaion = savedInstanceState.getString( + STATE_OPEN_CONVERSATION, null); + mPanelOpen = savedInstanceState.getBoolean(STATE_PANEL_OPEN, true); + } + + setContentView(R.layout.fragment_conversations_overview); + + listView = (ListView) findViewById(R.id.list); + + getActionBar().setDisplayHomeAsUpEnabled(false); + getActionBar().setHomeButtonEnabled(false); + + this.listAdapter = new ConversationAdapter(this, conversationList); + listView.setAdapter(this.listAdapter); + + listView.setOnItemClickListener(new OnItemClickListener() { + + @Override + public void onItemClick(AdapterView arg0, View clickedView, + int position, long arg3) { + paneShouldBeOpen = false; + if (getSelectedConversation() != conversationList.get(position)) { + setSelectedConversation(conversationList.get(position)); + swapConversationFragment(); + } else { + hideConversationsOverview(); + } + } + }); + mContentView = findViewById(R.id.content_view_spl); + if (mContentView == null) { + mContentView = findViewById(R.id.content_view_ll); + } + if (mContentView instanceof SlidingPaneLayout) { + SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView; + mSlidingPaneLayout.setParallaxDistance(150); + mSlidingPaneLayout + .setShadowResource(R.drawable.es_slidingpane_shadow); + mSlidingPaneLayout.setSliderFadeColor(0); + mSlidingPaneLayout.setPanelSlideListener(new PanelSlideListener() { + + @Override + public void onPanelOpened(View arg0) { + paneShouldBeOpen = true; + ActionBar ab = getActionBar(); + if (ab != null) { + ab.setDisplayHomeAsUpEnabled(false); + ab.setHomeButtonEnabled(false); + ab.setTitle(R.string.app_name); + } + invalidateOptionsMenu(); + hideKeyboard(); + if (xmppConnectionServiceBound) { + xmppConnectionService.getNotificationService() + .setOpenConversation(null); + } + } + + @Override + public void onPanelClosed(View arg0) { + paneShouldBeOpen = false; + if ((conversationList.size() > 0) + && (getSelectedConversation() != null)) { + openConversation(getSelectedConversation()); + if (!getSelectedConversation().isRead()) { + xmppConnectionService.markRead( + getSelectedConversation(), true); + listView.invalidateViews(); + } + } + } + + @Override + public void onPanelSlide(View arg0, float arg1) { + // TODO Auto-generated method stub + + } + }); + } + } + + public void openConversation(Conversation conversation) { + ActionBar ab = getActionBar(); + if (ab != null) { + ab.setDisplayHomeAsUpEnabled(true); + ab.setHomeButtonEnabled(true); + if (getSelectedConversation().getMode() == Conversation.MODE_SINGLE + || ConversationActivity.this + .useSubjectToIdentifyConference()) { + ab.setTitle(getSelectedConversation().getName()); + } else { + ab.setTitle(getSelectedConversation().getContactJid() + .split("/")[0]); + } + } + invalidateOptionsMenu(); + if (xmppConnectionServiceBound) { + xmppConnectionService.getNotificationService().setOpenConversation( + conversation); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.conversations, menu); + MenuItem menuSecure = (MenuItem) menu.findItem(R.id.action_security); + MenuItem menuArchive = (MenuItem) menu.findItem(R.id.action_archive); + MenuItem menuMucDetails = (MenuItem) menu + .findItem(R.id.action_muc_details); + MenuItem menuContactDetails = (MenuItem) menu + .findItem(R.id.action_contact_details); + MenuItem menuAttach = (MenuItem) menu.findItem(R.id.action_attach_file); + MenuItem menuClearHistory = (MenuItem) menu + .findItem(R.id.action_clear_history); + MenuItem menuAdd = (MenuItem) menu.findItem(R.id.action_add); + MenuItem menuInviteContact = (MenuItem) menu + .findItem(R.id.action_invite); + MenuItem menuMute = (MenuItem) menu.findItem(R.id.action_mute); + + if (isConversationsOverviewVisable() + && isConversationsOverviewHideable()) { + menuArchive.setVisible(false); + menuMucDetails.setVisible(false); + menuContactDetails.setVisible(false); + menuSecure.setVisible(false); + menuInviteContact.setVisible(false); + menuAttach.setVisible(false); + menuClearHistory.setVisible(false); + menuMute.setVisible(false); + } else { + menuAdd.setVisible(!isConversationsOverviewHideable()); + if (this.getSelectedConversation() != null) { + if (this.getSelectedConversation().getLatestMessage() + .getEncryption() != Message.ENCRYPTION_NONE) { + menuSecure.setIcon(R.drawable.ic_action_secure); + } + if (this.getSelectedConversation().getMode() == Conversation.MODE_MULTI) { + menuContactDetails.setVisible(false); + menuAttach.setVisible(false); + } else { + menuMucDetails.setVisible(false); + menuInviteContact.setVisible(false); + } + } + } + return true; + } + + private void selectPresenceToAttachFile(final int attachmentChoice) { + selectPresence(getSelectedConversation(), new OnPresenceSelected() { + + @Override + public void onPresenceSelected() { + if (attachmentChoice == ATTACHMENT_CHOICE_TAKE_PHOTO) { + pendingImageUri = xmppConnectionService.getFileBackend() + .getTakePhotoUri(); + Intent takePictureIntent = new Intent( + MediaStore.ACTION_IMAGE_CAPTURE); + takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, + pendingImageUri); + if (takePictureIntent.resolveActivity(getPackageManager()) != null) { + startActivityForResult(takePictureIntent, + REQUEST_IMAGE_CAPTURE); + } + } else if (attachmentChoice == ATTACHMENT_CHOICE_CHOOSE_IMAGE) { + Intent attachFileIntent = new Intent(); + attachFileIntent.setType("image/*"); + attachFileIntent.setAction(Intent.ACTION_GET_CONTENT); + Intent chooser = Intent.createChooser(attachFileIntent, + getString(R.string.attach_file)); + startActivityForResult(chooser, REQUEST_ATTACH_FILE_DIALOG); + } else if (attachmentChoice == ATTACHMENT_CHOICE_RECORD_VOICE) { + Intent intent = new Intent( + MediaStore.Audio.Media.RECORD_SOUND_ACTION); + startActivityForResult(intent, REQUEST_RECORD_AUDIO); + } + } + }); + } + + private void attachFile(final int attachmentChoice) { + final Conversation conversation = getSelectedConversation(); + if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) { + if (hasPgp()) { + if (conversation.getContact().getPgpKeyId() != 0) { + xmppConnectionService.getPgpEngine().hasKey( + conversation.getContact(), + new UiCallback() { + + @Override + public void userInputRequried(PendingIntent pi, + Contact contact) { + ConversationActivity.this.runIntent(pi, + attachmentChoice); + } + + @Override + public void success(Contact contact) { + selectPresenceToAttachFile(attachmentChoice); + } + + @Override + public void error(int error, Contact contact) { + displayErrorDialog(error); + } + }); + } else { + final ConversationFragment fragment = (ConversationFragment) getFragmentManager() + .findFragmentByTag("conversation"); + if (fragment != null) { + fragment.showNoPGPKeyDialog(false, + new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, + int which) { + conversation + .setNextEncryption(Message.ENCRYPTION_NONE); + xmppConnectionService.databaseBackend + .updateConversation(conversation); + selectPresenceToAttachFile(attachmentChoice); + } + }); + } + } + } else { + showInstallPgpDialog(); + } + } else if (getSelectedConversation().getNextEncryption( + forceEncryption()) == Message.ENCRYPTION_NONE) { + selectPresenceToAttachFile(attachmentChoice); + } else { + selectPresenceToAttachFile(attachmentChoice); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + showConversationsOverview(); + return true; + } else if (item.getItemId() == R.id.action_add) { + startActivity(new Intent(this, StartConversationActivity.class)); + return true; + } else if (getSelectedConversation() != null) { + switch (item.getItemId()) { + case R.id.action_attach_file: + attachFileDialog(); + break; + case R.id.action_archive: + this.endConversation(getSelectedConversation()); + break; + case R.id.action_contact_details: + Contact contact = this.getSelectedConversation().getContact(); + if (contact.showInRoster()) { + switchToContactDetails(contact); + } else { + showAddToRosterDialog(getSelectedConversation()); + } + break; + case R.id.action_muc_details: + Intent intent = new Intent(this, + ConferenceDetailsActivity.class); + intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC); + intent.putExtra("uuid", getSelectedConversation().getUuid()); + startActivity(intent); + break; + case R.id.action_invite: + inviteToConversation(getSelectedConversation()); + break; + case R.id.action_security: + selectEncryptionDialog(getSelectedConversation()); + break; + case R.id.action_clear_history: + clearHistoryDialog(getSelectedConversation()); + break; + case R.id.action_mute: + muteConversationDialog(getSelectedConversation()); + break; + default: + break; + } + return super.onOptionsItemSelected(item); + } else { + return super.onOptionsItemSelected(item); + } + } + + public void endConversation(Conversation conversation) { + conversation.setStatus(Conversation.STATUS_ARCHIVED); + paneShouldBeOpen = true; + showConversationsOverview(); + xmppConnectionService.archiveConversation(conversation); + if (conversationList.size() > 0) { + setSelectedConversation(conversationList.get(0)); + } else { + setSelectedConversation(null); + } + } + + @SuppressLint("InflateParams") + protected void clearHistoryDialog(final Conversation conversation) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(getString(R.string.clear_conversation_history)); + View dialogView = getLayoutInflater().inflate( + R.layout.dialog_clear_history, null); + final CheckBox endConversationCheckBox = (CheckBox) dialogView + .findViewById(R.id.end_conversation_checkbox); + builder.setView(dialogView); + builder.setNegativeButton(getString(R.string.cancel), null); + builder.setPositiveButton(getString(R.string.delete_messages), + new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + ConversationActivity.this.xmppConnectionService + .clearConversationHistory(conversation); + if (endConversationCheckBox.isChecked()) { + endConversation(conversation); + } + } + }); + builder.create().show(); + } + + protected void attachFileDialog() { + View menuAttachFile = findViewById(R.id.action_attach_file); + if (menuAttachFile == null) { + return; + } + PopupMenu attachFilePopup = new PopupMenu(this, menuAttachFile); + attachFilePopup.inflate(R.menu.attachment_choices); + attachFilePopup + .setOnMenuItemClickListener(new OnMenuItemClickListener() { + + @Override + public boolean onMenuItemClick(MenuItem item) { + switch (item.getItemId()) { + case R.id.attach_choose_picture: + attachFile(ATTACHMENT_CHOICE_CHOOSE_IMAGE); + break; + case R.id.attach_take_picture: + attachFile(ATTACHMENT_CHOICE_TAKE_PHOTO); + break; + case R.id.attach_record_voice: + attachFile(ATTACHMENT_CHOICE_RECORD_VOICE); + break; + } + return false; + } + }); + attachFilePopup.show(); + } + + protected void selectEncryptionDialog(final Conversation conversation) { + View menuItemView = findViewById(R.id.action_security); + if (menuItemView == null) { + return; + } + PopupMenu popup = new PopupMenu(this, menuItemView); + final ConversationFragment fragment = (ConversationFragment) getFragmentManager() + .findFragmentByTag("conversation"); + if (fragment != null) { + popup.setOnMenuItemClickListener(new OnMenuItemClickListener() { + + @Override + public boolean onMenuItemClick(MenuItem item) { + switch (item.getItemId()) { + case R.id.encryption_choice_none: + conversation.setNextEncryption(Message.ENCRYPTION_NONE); + item.setChecked(true); + break; + case R.id.encryption_choice_otr: + conversation.setNextEncryption(Message.ENCRYPTION_OTR); + item.setChecked(true); + break; + case R.id.encryption_choice_pgp: + if (hasPgp()) { + if (conversation.getAccount().getKeys() + .has("pgp_signature")) { + conversation + .setNextEncryption(Message.ENCRYPTION_PGP); + item.setChecked(true); + } else { + announcePgp(conversation.getAccount(), + conversation); + } + } else { + showInstallPgpDialog(); + } + break; + default: + conversation.setNextEncryption(Message.ENCRYPTION_NONE); + break; + } + xmppConnectionService.databaseBackend + .updateConversation(conversation); + fragment.updateChatMsgHint(); + return true; + } + }); + popup.inflate(R.menu.encryption_choices); + MenuItem otr = popup.getMenu().findItem(R.id.encryption_choice_otr); + MenuItem none = popup.getMenu().findItem( + R.id.encryption_choice_none); + if (conversation.getMode() == Conversation.MODE_MULTI) { + otr.setEnabled(false); + } else { + if (forceEncryption()) { + none.setVisible(false); + } + } + switch (conversation.getNextEncryption(forceEncryption())) { + case Message.ENCRYPTION_NONE: + none.setChecked(true); + break; + case Message.ENCRYPTION_OTR: + otr.setChecked(true); + break; + case Message.ENCRYPTION_PGP: + popup.getMenu().findItem(R.id.encryption_choice_pgp) + .setChecked(true); + break; + default: + popup.getMenu().findItem(R.id.encryption_choice_none) + .setChecked(true); + break; + } + popup.show(); + } + } + + protected void muteConversationDialog(final Conversation conversation) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(R.string.disable_notifications_for_this_conversation); + final int[] durations = getResources().getIntArray( + R.array.mute_options_durations); + builder.setItems(R.array.mute_options_descriptions, + new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + long till; + if (durations[which] == -1) { + till = Long.MAX_VALUE; + } else { + till = SystemClock.elapsedRealtime() + + (durations[which] * 1000); + } + conversation.setMutedTill(till); + ConversationActivity.this.xmppConnectionService.databaseBackend + .updateConversation(conversation); + ConversationFragment selectedFragment = (ConversationFragment) getFragmentManager() + .findFragmentByTag("conversation"); + if (selectedFragment != null) { + selectedFragment.updateMessages(); + } + } + }); + builder.create().show(); + } + + protected ConversationFragment swapConversationFragment() { + ConversationFragment selectedFragment = new ConversationFragment(); + if (!isFinishing()) { + + FragmentTransaction transaction = getFragmentManager() + .beginTransaction(); + transaction.replace(R.id.selected_conversation, selectedFragment, + "conversation"); + try { + transaction.commitAllowingStateLoss(); + } catch (IllegalStateException e) { + return selectedFragment; + } + } + return selectedFragment; + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK) { + if (!isConversationsOverviewVisable()) { + showConversationsOverview(); + return false; + } + } + return super.onKeyDown(keyCode, event); + } + + @Override + protected void onNewIntent(Intent intent) { + if (xmppConnectionServiceBound) { + if ((Intent.ACTION_VIEW.equals(intent.getAction()) && (VIEW_CONVERSATION + .equals(intent.getType())))) { + String convToView = (String) intent.getExtras().get( + CONVERSATION); + updateConversationList(); + for (int i = 0; i < conversationList.size(); ++i) { + if (conversationList.get(i).getUuid().equals(convToView)) { + setSelectedConversation(conversationList.get(i)); + break; + } + } + paneShouldBeOpen = false; + String text = intent.getExtras().getString(TEXT, null); + swapConversationFragment().setText(text); + } + } else { + handledViewIntent = false; + setIntent(intent); + } + } + + @Override + public void onStart() { + super.onStart(); + if (this.xmppConnectionServiceBound) { + this.onBackendConnected(); + } + if (conversationList.size() >= 1) { + this.onConversationUpdate(); + } + } + + @Override + protected void onStop() { + if (xmppConnectionServiceBound) { + xmppConnectionService.removeOnConversationListChangedListener(); + xmppConnectionService.removeOnAccountListChangedListener(); + xmppConnectionService.removeOnRosterUpdateListener(); + xmppConnectionService.getNotificationService().setOpenConversation( + null); + } + super.onStop(); + } + + @Override + public void onSaveInstanceState(Bundle savedInstanceState) { + Conversation conversation = getSelectedConversation(); + if (conversation != null) { + savedInstanceState.putString(STATE_OPEN_CONVERSATION, + conversation.getUuid()); + } + savedInstanceState.putBoolean(STATE_PANEL_OPEN, + isConversationsOverviewVisable()); + super.onSaveInstanceState(savedInstanceState); + } + + @Override + void onBackendConnected() { + this.registerListener(); + updateConversationList(); + + if (xmppConnectionService.getAccounts().size() == 0) { + startActivity(new Intent(this, EditAccountActivity.class)); + } else if (conversationList.size() <= 0) { + startActivity(new Intent(this, StartConversationActivity.class)); + finish(); + } else if (mOpenConverstaion != null) { + selectConversationByUuid(mOpenConverstaion); + paneShouldBeOpen = mPanelOpen; + if (paneShouldBeOpen) { + showConversationsOverview(); + } + swapConversationFragment(); + mOpenConverstaion = null; + } else if (getIntent() != null + && VIEW_CONVERSATION.equals(getIntent().getType())) { + String uuid = (String) getIntent().getExtras().get(CONVERSATION); + String text = getIntent().getExtras().getString(TEXT, null); + selectConversationByUuid(uuid); + paneShouldBeOpen = false; + swapConversationFragment().setText(text); + setIntent(null); + } else { + showConversationsOverview(); + ConversationFragment selectedFragment = (ConversationFragment) getFragmentManager() + .findFragmentByTag("conversation"); + if (selectedFragment != null) { + selectedFragment.onBackendConnected(); + } else { + pendingImageUri = null; + setSelectedConversation(conversationList.get(0)); + swapConversationFragment(); + } + } + + if (pendingImageUri != null) { + attachImageToConversation(getSelectedConversation(), + pendingImageUri); + pendingImageUri = null; + } + ExceptionHelper.checkForCrash(this, this.xmppConnectionService); + } + + private void selectConversationByUuid(String uuid) { + for (int i = 0; i < conversationList.size(); ++i) { + if (conversationList.get(i).getUuid().equals(uuid)) { + setSelectedConversation(conversationList.get(i)); + } + } + } + + public void registerListener() { + xmppConnectionService.setOnConversationListChangedListener(this); + xmppConnectionService.setOnAccountListChangedListener(this); + xmppConnectionService.setOnRosterUpdateListener(this); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, + final Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (resultCode == RESULT_OK) { + if (requestCode == REQUEST_DECRYPT_PGP) { + ConversationFragment selectedFragment = (ConversationFragment) getFragmentManager() + .findFragmentByTag("conversation"); + if (selectedFragment != null) { + selectedFragment.hideSnackbar(); + selectedFragment.updateMessages(); + } + } else if (requestCode == REQUEST_ATTACH_FILE_DIALOG) { + pendingImageUri = data.getData(); + if (xmppConnectionServiceBound) { + attachImageToConversation(getSelectedConversation(), + pendingImageUri); + pendingImageUri = null; + } + } else if (requestCode == REQUEST_SEND_PGP_IMAGE) { + + } else if (requestCode == ATTACHMENT_CHOICE_CHOOSE_IMAGE) { + attachFile(ATTACHMENT_CHOICE_CHOOSE_IMAGE); + } else if (requestCode == ATTACHMENT_CHOICE_TAKE_PHOTO) { + attachFile(ATTACHMENT_CHOICE_TAKE_PHOTO); + } else if (requestCode == REQUEST_ANNOUNCE_PGP) { + announcePgp(getSelectedConversation().getAccount(), + getSelectedConversation()); + } else if (requestCode == REQUEST_ENCRYPT_MESSAGE) { + // encryptTextMessage(); + } else if (requestCode == REQUEST_IMAGE_CAPTURE) { + if (xmppConnectionServiceBound) { + attachImageToConversation(getSelectedConversation(), + pendingImageUri); + pendingImageUri = null; + } + Intent intent = new Intent( + Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); + intent.setData(pendingImageUri); + sendBroadcast(intent); + } else if (requestCode == REQUEST_RECORD_AUDIO) { + attachAudioToConversation(getSelectedConversation(), + data.getData()); + } + } else { + if (requestCode == REQUEST_IMAGE_CAPTURE) { + pendingImageUri = null; + } + } + } + + private void attachAudioToConversation(Conversation conversation, Uri uri) { + + } + + private void attachImageToConversation(Conversation conversation, Uri uri) { + prepareImageToast = Toast.makeText(getApplicationContext(), + getText(R.string.preparing_image), Toast.LENGTH_LONG); + prepareImageToast.show(); + xmppConnectionService.attachImageToConversation(conversation, uri, + new UiCallback() { + + @Override + public void userInputRequried(PendingIntent pi, + Message object) { + hidePrepareImageToast(); + ConversationActivity.this.runIntent(pi, + ConversationActivity.REQUEST_SEND_PGP_IMAGE); + } + + @Override + public void success(Message message) { + xmppConnectionService.sendMessage(message); + } + + @Override + public void error(int error, Message message) { + hidePrepareImageToast(); + displayErrorDialog(error); + } + }); + } + + private void hidePrepareImageToast() { + if (prepareImageToast != null) { + runOnUiThread(new Runnable() { + + @Override + public void run() { + prepareImageToast.cancel(); + } + }); + } + } + + public void updateConversationList() { + xmppConnectionService + .populateWithOrderedConversations(conversationList); + listAdapter.notifyDataSetChanged(); + } + + public void runIntent(PendingIntent pi, int requestCode) { + try { + this.startIntentSenderForResult(pi.getIntentSender(), requestCode, + null, 0, 0, 0); + } catch (SendIntentException e1) { + } + } + + public void encryptTextMessage(Message message) { + xmppConnectionService.getPgpEngine().encrypt(message, + new UiCallback() { + + @Override + public void userInputRequried(PendingIntent pi, + Message message) { + ConversationActivity.this.runIntent(pi, + ConversationActivity.REQUEST_SEND_MESSAGE); + } + + @Override + public void success(Message message) { + message.setEncryption(Message.ENCRYPTION_DECRYPTED); + xmppConnectionService.sendMessage(message); + } + + @Override + public void error(int error, Message message) { + + } + }); + } + + public boolean forceEncryption() { + return getPreferences().getBoolean("force_encryption", false); + } + + public boolean useSendButtonToIndicateStatus() { + return getPreferences().getBoolean("send_button_status", false); + } + + public boolean indicateReceived() { + return getPreferences().getBoolean("indicate_received", false); + } + + @Override + public void onAccountUpdate() { + final ConversationFragment fragment = (ConversationFragment) getFragmentManager() + .findFragmentByTag("conversation"); + if (fragment != null) { + runOnUiThread(new Runnable() { + + @Override + public void run() { + fragment.updateMessages(); + } + }); + } + } + + @Override + public void onConversationUpdate() { + runOnUiThread(new Runnable() { + + @Override + public void run() { + updateConversationList(); + if (paneShouldBeOpen) { + if (conversationList.size() >= 1) { + swapConversationFragment(); + } else { + startActivity(new Intent(getApplicationContext(), + StartConversationActivity.class)); + finish(); + } + } + ConversationFragment selectedFragment = (ConversationFragment) getFragmentManager() + .findFragmentByTag("conversation"); + if (selectedFragment != null) { + selectedFragment.updateMessages(); + } + } + }); + } + + @Override + public void onRosterUpdate() { + final ConversationFragment fragment = (ConversationFragment) getFragmentManager() + .findFragmentByTag("conversation"); + if (fragment != null) { + runOnUiThread(new Runnable() { + + @Override + public void run() { + fragment.updateMessages(); + } + }); + } + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/conversations/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java new file mode 100644 index 00000000..0e71801b --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -0,0 +1,781 @@ +package eu.siacs.conversations.ui; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentLinkedQueue; + +import net.java.otr4j.session.SessionStatus; +import eu.siacs.conversations.R; +import eu.siacs.conversations.crypto.PgpEngine; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.entities.MucOptions; +import eu.siacs.conversations.entities.Presences; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.ui.EditMessage.OnEnterPressed; +import eu.siacs.conversations.ui.XmppActivity.OnPresenceSelected; +import eu.siacs.conversations.ui.XmppActivity.OnValueEdited; +import eu.siacs.conversations.ui.adapter.MessageAdapter; +import eu.siacs.conversations.ui.adapter.MessageAdapter.OnContactPictureClicked; +import eu.siacs.conversations.ui.adapter.MessageAdapter.OnContactPictureLongClicked; +import eu.siacs.conversations.utils.UIHelper; +import android.app.AlertDialog; +import android.app.Fragment; +import android.app.PendingIntent; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentSender; +import android.content.IntentSender.SendIntentException; +import android.os.Bundle; +import android.text.Editable; +import android.text.Selection; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.AbsListView.OnScrollListener; +import android.widget.TextView.OnEditorActionListener; +import android.widget.AbsListView; + +import android.widget.ListView; +import android.widget.ImageButton; +import android.widget.RelativeLayout; +import android.widget.TextView; +import android.widget.Toast; + +public class ConversationFragment extends Fragment { + + protected Conversation conversation; + protected ListView messagesView; + protected LayoutInflater inflater; + protected List messageList = new ArrayList(); + protected MessageAdapter messageListAdapter; + protected Contact contact; + + protected String queuedPqpMessage = null; + + private EditMessage mEditMessage; + private ImageButton mSendButton; + private String pastedText = null; + private RelativeLayout snackbar; + private TextView snackbarMessage; + private TextView snackbarAction; + + private boolean messagesLoaded = false; + + private IntentSender askForPassphraseIntent = null; + + private ConcurrentLinkedQueue mEncryptedMessages = new ConcurrentLinkedQueue(); + private boolean mDecryptJobRunning = false; + + private OnEditorActionListener mEditorActionListener = new OnEditorActionListener() { + + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + if (actionId == EditorInfo.IME_ACTION_SEND) { + InputMethodManager imm = (InputMethodManager) v.getContext() + .getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(v.getWindowToken(), 0); + sendMessage(); + return true; + } else { + return false; + } + } + }; + + private OnClickListener mSendButtonListener = new OnClickListener() { + + @Override + public void onClick(View v) { + sendMessage(); + } + }; + protected OnClickListener clickToDecryptListener = new OnClickListener() { + + @Override + public void onClick(View v) { + if (activity.hasPgp() && askForPassphraseIntent != null) { + try { + getActivity().startIntentSenderForResult( + askForPassphraseIntent, + ConversationActivity.REQUEST_DECRYPT_PGP, null, 0, + 0, 0); + } catch (SendIntentException e) { + // + } + } + } + }; + + private OnClickListener clickToMuc = new OnClickListener() { + + @Override + public void onClick(View v) { + Intent intent = new Intent(getActivity(), + ConferenceDetailsActivity.class); + intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC); + intent.putExtra("uuid", conversation.getUuid()); + startActivity(intent); + } + }; + + private OnClickListener leaveMuc = new OnClickListener() { + + @Override + public void onClick(View v) { + activity.endConversation(conversation); + } + }; + + private OnClickListener joinMuc = new OnClickListener() { + + @Override + public void onClick(View v) { + activity.xmppConnectionService.joinMuc(conversation); + } + }; + + private OnClickListener enterPassword = new OnClickListener() { + + @Override + public void onClick(View v) { + MucOptions muc = conversation.getMucOptions(); + String password = muc.getPassword(); + if (password == null) { + password = ""; + } + activity.quickPasswordEdit(password, new OnValueEdited() { + + @Override + public void onValueEdited(String value) { + activity.xmppConnectionService.providePasswordForMuc( + conversation, value); + } + }); + } + }; + + private OnScrollListener mOnScrollListener = new OnScrollListener() { + + @Override + public void onScrollStateChanged(AbsListView view, int scrollState) { + // TODO Auto-generated method stub + + } + + @Override + public void onScroll(AbsListView view, int firstVisibleItem, + int visibleItemCount, int totalItemCount) { + if (firstVisibleItem == 0 && messagesLoaded) { + long timestamp = messageList.get(0).getTimeSent(); + messagesLoaded = false; + int size = activity.xmppConnectionService.loadMoreMessages( + conversation, timestamp); + messageList.clear(); + messageList.addAll(conversation.getMessages()); + updateStatusMessages(); + messageListAdapter.notifyDataSetChanged(); + if (size != 0) { + messagesLoaded = true; + } + messagesView.setSelectionFromTop(size + 1, 0); + } + } + }; + + private ConversationActivity activity; + + private void sendMessage() { + if (this.conversation == null) { + return; + } + if (mEditMessage.getText().length() < 1) { + if (this.conversation.getMode() == Conversation.MODE_MULTI) { + conversation.setNextPresence(null); + updateChatMsgHint(); + } + return; + } + Message message = new Message(conversation, mEditMessage.getText() + .toString(), conversation.getNextEncryption(activity + .forceEncryption())); + if (conversation.getMode() == Conversation.MODE_MULTI) { + if (conversation.getNextPresence() != null) { + message.setPresence(conversation.getNextPresence()); + message.setType(Message.TYPE_PRIVATE); + conversation.setNextPresence(null); + } + } + if (conversation.getNextEncryption(activity.forceEncryption()) == Message.ENCRYPTION_OTR) { + sendOtrMessage(message); + } else if (conversation.getNextEncryption(activity.forceEncryption()) == Message.ENCRYPTION_PGP) { + sendPgpMessage(message); + } else { + sendPlainTextMessage(message); + } + } + + public void updateChatMsgHint() { + if (conversation.getMode() == Conversation.MODE_MULTI + && conversation.getNextPresence() != null) { + this.mEditMessage.setHint(getString( + R.string.send_private_message_to, + conversation.getNextPresence())); + } else { + switch (conversation.getNextEncryption(activity.forceEncryption())) { + case Message.ENCRYPTION_NONE: + mEditMessage + .setHint(getString(R.string.send_plain_text_message)); + break; + case Message.ENCRYPTION_OTR: + mEditMessage.setHint(getString(R.string.send_otr_message)); + break; + case Message.ENCRYPTION_PGP: + mEditMessage.setHint(getString(R.string.send_pgp_message)); + break; + default: + break; + } + } + } + + @Override + public View onCreateView(final LayoutInflater inflater, + ViewGroup container, Bundle savedInstanceState) { + final View view = inflater.inflate(R.layout.fragment_conversation, + container, false); + mEditMessage = (EditMessage) view.findViewById(R.id.textinput); + mEditMessage.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + activity.hideConversationsOverview(); + } + }); + mEditMessage.setOnEditorActionListener(mEditorActionListener); + mEditMessage.setOnEnterPressedListener(new OnEnterPressed() { + + @Override + public void onEnterPressed() { + sendMessage(); + } + }); + + mSendButton = (ImageButton) view.findViewById(R.id.textSendButton); + mSendButton.setOnClickListener(this.mSendButtonListener); + + snackbar = (RelativeLayout) view.findViewById(R.id.snackbar); + snackbarMessage = (TextView) view.findViewById(R.id.snackbar_message); + snackbarAction = (TextView) view.findViewById(R.id.snackbar_action); + + messagesView = (ListView) view.findViewById(R.id.messages_view); + messagesView.setOnScrollListener(mOnScrollListener); + messagesView.setTranscriptMode(ListView.TRANSCRIPT_MODE_NORMAL); + messageListAdapter = new MessageAdapter( + (ConversationActivity) getActivity(), this.messageList); + messageListAdapter + .setOnContactPictureClicked(new OnContactPictureClicked() { + + @Override + public void onContactPictureClicked(Message message) { + if (message.getStatus() <= Message.STATUS_RECEIVED) { + if (message.getConversation().getMode() == Conversation.MODE_MULTI) { + if (message.getPresence() != null) { + highlightInConference(message.getPresence()); + } else { + highlightInConference(message + .getCounterpart()); + } + } else { + Contact contact = message.getConversation() + .getContact(); + if (contact.showInRoster()) { + activity.switchToContactDetails(contact); + } else { + activity.showAddToRosterDialog(message + .getConversation()); + } + } + } + } + }); + messageListAdapter + .setOnContactPictureLongClicked(new OnContactPictureLongClicked() { + + @Override + public void onContactPictureLongClicked(Message message) { + if (message.getStatus() <= Message.STATUS_RECEIVED) { + if (message.getConversation().getMode() == Conversation.MODE_MULTI) { + if (message.getPresence() != null) { + privateMessageWith(message.getPresence()); + } else { + privateMessageWith(message.getCounterpart()); + } + } + } + } + }); + messagesView.setAdapter(messageListAdapter); + + return view; + } + + protected void privateMessageWith(String counterpart) { + this.mEditMessage.setText(""); + this.conversation.setNextPresence(counterpart); + updateChatMsgHint(); + } + + protected void highlightInConference(String nick) { + String oldString = mEditMessage.getText().toString().trim(); + if (oldString.isEmpty() || mEditMessage.getSelectionStart() == 0) { + mEditMessage.getText().insert(0, nick + ": "); + } else { + if (mEditMessage.getText().charAt( + mEditMessage.getSelectionStart() - 1) != ' ') { + nick = " " + nick; + } + mEditMessage.getText().insert(mEditMessage.getSelectionStart(), + nick + " "); + } + } + + @Override + public void onStart() { + super.onStart(); + this.activity = (ConversationActivity) getActivity(); + if (activity.xmppConnectionServiceBound) { + this.onBackendConnected(); + } + } + + @Override + public void onStop() { + mDecryptJobRunning = false; + super.onStop(); + if (this.conversation != null) { + this.conversation.setNextMessage(mEditMessage.getText().toString()); + } + } + + public void onBackendConnected() { + this.activity = (ConversationActivity) getActivity(); + this.conversation = activity.getSelectedConversation(); + if (this.conversation == null) { + return; + } + String oldString = conversation.getNextMessage().trim(); + if (this.pastedText == null) { + this.mEditMessage.setText(oldString); + } else { + + if (oldString.isEmpty()) { + mEditMessage.setText(pastedText); + } else { + mEditMessage.setText(oldString + " " + pastedText); + } + pastedText = null; + } + int position = mEditMessage.length(); + Editable etext = mEditMessage.getText(); + Selection.setSelection(etext, position); + if (activity.isConversationsOverviewHideable()) { + if (!activity.shouldPaneBeOpen()) { + activity.hideConversationsOverview(); + activity.openConversation(conversation); + } + } + if (this.conversation.getMode() == Conversation.MODE_MULTI) { + conversation.setNextPresence(null); + } + updateMessages(); + } + + public void updateMessages() { + if (getView() == null) { + return; + } + hideSnackbar(); + final ConversationActivity activity = (ConversationActivity) getActivity(); + if (this.conversation != null) { + final Contact contact = this.conversation.getContact(); + if (this.conversation.isMuted()) { + showSnackbar(R.string.notifications_disabled, R.string.enable, + new OnClickListener() { + + @Override + public void onClick(View v) { + conversation.setMutedTill(0); + activity.xmppConnectionService.databaseBackend + .updateConversation(conversation); + updateMessages(); + } + }); + } else if (!contact.showInRoster() + && contact + .getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) { + showSnackbar(R.string.contact_added_you, R.string.add_back, + new OnClickListener() { + + @Override + public void onClick(View v) { + activity.xmppConnectionService + .createContact(contact); + activity.switchToContactDetails(contact); + } + }); + } + for (Message message : this.conversation.getMessages()) { + if ((message.getEncryption() == Message.ENCRYPTION_PGP) + && ((message.getStatus() == Message.STATUS_RECEIVED) || (message + .getStatus() == Message.STATUS_SEND))) { + if (!mEncryptedMessages.contains(message)) { + mEncryptedMessages.add(message); + } + } + } + decryptNext(); + this.messageList.clear(); + if (this.conversation.getMessages().size() == 0) { + messagesLoaded = false; + } else { + this.messageList.addAll(this.conversation.getMessages()); + messagesLoaded = true; + updateStatusMessages(); + } + this.messageListAdapter.notifyDataSetChanged(); + if (conversation.getMode() == Conversation.MODE_SINGLE) { + if (messageList.size() >= 1) { + makeFingerprintWarning(conversation.getLatestEncryption()); + } + } else { + if (!conversation.getMucOptions().online() + && conversation.getAccount().getStatus() == Account.STATUS_ONLINE) { + int error = conversation.getMucOptions().getError(); + switch (error) { + case MucOptions.ERROR_NICK_IN_USE: + showSnackbar(R.string.nick_in_use, R.string.edit, + clickToMuc); + break; + case MucOptions.ERROR_ROOM_NOT_FOUND: + showSnackbar(R.string.conference_not_found, + R.string.leave, leaveMuc); + break; + case MucOptions.ERROR_PASSWORD_REQUIRED: + showSnackbar(R.string.conference_requires_password, + R.string.enter_password, enterPassword); + break; + case MucOptions.ERROR_BANNED: + showSnackbar(R.string.conference_banned, + R.string.leave, leaveMuc); + break; + case MucOptions.ERROR_MEMBERS_ONLY: + showSnackbar(R.string.conference_members_only, + R.string.leave, leaveMuc); + break; + case MucOptions.KICKED_FROM_ROOM: + showSnackbar(R.string.conference_kicked, R.string.join, + joinMuc); + break; + default: + break; + } + } + } + getActivity().invalidateOptionsMenu(); + updateChatMsgHint(); + if (!activity.shouldPaneBeOpen()) { + activity.xmppConnectionService.markRead(conversation, true); + activity.updateConversationList(); + } + this.updateSendButton(); + } + } + + private void decryptNext() { + Message next = this.mEncryptedMessages.peek(); + PgpEngine engine = activity.xmppConnectionService.getPgpEngine(); + + if (next != null && engine != null && !mDecryptJobRunning) { + mDecryptJobRunning = true; + engine.decrypt(next, new UiCallback() { + + @Override + public void userInputRequried(PendingIntent pi, Message message) { + mDecryptJobRunning = false; + askForPassphraseIntent = pi.getIntentSender(); + showSnackbar(R.string.openpgp_messages_found, + R.string.decrypt, clickToDecryptListener); + } + + @Override + public void success(Message message) { + mDecryptJobRunning = false; + mEncryptedMessages.remove(); + activity.xmppConnectionService.updateMessage(message); + } + + @Override + public void error(int error, Message message) { + message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED); + mDecryptJobRunning = false; + mEncryptedMessages.remove(); + activity.xmppConnectionService.updateConversationUi(); + } + }); + } + } + + private void messageSent() { + int size = this.messageList.size(); + messagesView.setSelection(size - 1); + mEditMessage.setText(""); + updateChatMsgHint(); + } + + public void updateSendButton() { + Conversation c = this.conversation; + if (activity.useSendButtonToIndicateStatus() && c != null + && c.getAccount().getStatus() == Account.STATUS_ONLINE) { + if (c.getMode() == Conversation.MODE_SINGLE) { + switch (c.getContact().getMostAvailableStatus()) { + case Presences.CHAT: + this.mSendButton + .setImageResource(R.drawable.ic_action_send_now_online); + break; + case Presences.ONLINE: + this.mSendButton + .setImageResource(R.drawable.ic_action_send_now_online); + break; + case Presences.AWAY: + this.mSendButton + .setImageResource(R.drawable.ic_action_send_now_away); + break; + case Presences.XA: + this.mSendButton + .setImageResource(R.drawable.ic_action_send_now_away); + break; + case Presences.DND: + this.mSendButton + .setImageResource(R.drawable.ic_action_send_now_dnd); + break; + default: + this.mSendButton + .setImageResource(R.drawable.ic_action_send_now_offline); + break; + } + } else if (c.getMode() == Conversation.MODE_MULTI) { + if (c.getMucOptions().online()) { + this.mSendButton + .setImageResource(R.drawable.ic_action_send_now_online); + } else { + this.mSendButton + .setImageResource(R.drawable.ic_action_send_now_offline); + } + } else { + this.mSendButton + .setImageResource(R.drawable.ic_action_send_now_offline); + } + } else { + this.mSendButton + .setImageResource(R.drawable.ic_action_send_now_offline); + } + } + + protected void updateStatusMessages() { + if (conversation.getMode() == Conversation.MODE_SINGLE) { + for (int i = this.messageList.size() - 1; i >= 0; --i) { + if (this.messageList.get(i).getStatus() == Message.STATUS_RECEIVED) { + return; + } else { + if (this.messageList.get(i).getStatus() == Message.STATUS_SEND_DISPLAYED) { + this.messageList.add(i + 1, + Message.createStatusMessage(conversation)); + return; + } + } + } + } + } + + protected void makeFingerprintWarning(int latestEncryption) { + Set knownFingerprints = conversation.getContact() + .getOtrFingerprints(); + if ((latestEncryption == Message.ENCRYPTION_OTR) + && (conversation.hasValidOtrSession() + && (!conversation.isMuted()) + && (conversation.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) && (!knownFingerprints + .contains(conversation.getOtrFingerprint())))) { + showSnackbar(R.string.unknown_otr_fingerprint, R.string.verify, + new OnClickListener() { + + @Override + public void onClick(View v) { + if (conversation.getOtrFingerprint() != null) { + AlertDialog dialog = UIHelper + .getVerifyFingerprintDialog( + (ConversationActivity) getActivity(), + conversation, snackbar); + dialog.show(); + } + } + }); + } + } + + protected void showSnackbar(int message, int action, + OnClickListener clickListener) { + snackbar.setVisibility(View.VISIBLE); + snackbar.setOnClickListener(null); + snackbarMessage.setText(message); + snackbarMessage.setOnClickListener(null); + snackbarAction.setText(action); + snackbarAction.setOnClickListener(clickListener); + } + + protected void hideSnackbar() { + snackbar.setVisibility(View.GONE); + } + + protected void sendPlainTextMessage(Message message) { + ConversationActivity activity = (ConversationActivity) getActivity(); + activity.xmppConnectionService.sendMessage(message); + messageSent(); + } + + protected void sendPgpMessage(final Message message) { + final ConversationActivity activity = (ConversationActivity) getActivity(); + final XmppConnectionService xmppService = activity.xmppConnectionService; + final Contact contact = message.getConversation().getContact(); + if (activity.hasPgp()) { + if (conversation.getMode() == Conversation.MODE_SINGLE) { + if (contact.getPgpKeyId() != 0) { + xmppService.getPgpEngine().hasKey(contact, + new UiCallback() { + + @Override + public void userInputRequried(PendingIntent pi, + Contact contact) { + activity.runIntent( + pi, + ConversationActivity.REQUEST_ENCRYPT_MESSAGE); + } + + @Override + public void success(Contact contact) { + messageSent(); + activity.encryptTextMessage(message); + } + + @Override + public void error(int error, Contact contact) { + + } + }); + + } else { + showNoPGPKeyDialog(false, + new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, + int which) { + conversation + .setNextEncryption(Message.ENCRYPTION_NONE); + xmppService.databaseBackend + .updateConversation(conversation); + message.setEncryption(Message.ENCRYPTION_NONE); + xmppService.sendMessage(message); + messageSent(); + } + }); + } + } else { + if (conversation.getMucOptions().pgpKeysInUse()) { + if (!conversation.getMucOptions().everybodyHasKeys()) { + Toast warning = Toast + .makeText(getActivity(), + R.string.missing_public_keys, + Toast.LENGTH_LONG); + warning.setGravity(Gravity.CENTER_VERTICAL, 0, 0); + warning.show(); + } + activity.encryptTextMessage(message); + messageSent(); + } else { + showNoPGPKeyDialog(true, + new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, + int which) { + conversation + .setNextEncryption(Message.ENCRYPTION_NONE); + message.setEncryption(Message.ENCRYPTION_NONE); + xmppService.databaseBackend + .updateConversation(conversation); + xmppService.sendMessage(message); + messageSent(); + } + }); + } + } + } else { + activity.showInstallPgpDialog(); + } + } + + public void showNoPGPKeyDialog(boolean plural, + DialogInterface.OnClickListener listener) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setIconAttribute(android.R.attr.alertDialogIcon); + if (plural) { + builder.setTitle(getString(R.string.no_pgp_keys)); + builder.setMessage(getText(R.string.contacts_have_no_pgp_keys)); + } else { + builder.setTitle(getString(R.string.no_pgp_key)); + builder.setMessage(getText(R.string.contact_has_no_pgp_key)); + } + builder.setNegativeButton(getString(R.string.cancel), null); + builder.setPositiveButton(getString(R.string.send_unencrypted), + listener); + builder.create().show(); + } + + protected void sendOtrMessage(final Message message) { + final ConversationActivity activity = (ConversationActivity) getActivity(); + final XmppConnectionService xmppService = activity.xmppConnectionService; + if (conversation.hasValidOtrSession()) { + activity.xmppConnectionService.sendMessage(message); + messageSent(); + } else { + activity.selectPresence(message.getConversation(), + new OnPresenceSelected() { + + @Override + public void onPresenceSelected() { + message.setPresence(conversation.getNextPresence()); + xmppService.sendMessage(message); + messageSent(); + } + }); + } + } + + public void setText(String text) { + this.pastedText = text; + } + + public void clearInputField() { + this.mEditMessage.setText(""); + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/conversations/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java new file mode 100644 index 00000000..1543d740 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -0,0 +1,423 @@ +package eu.siacs.conversations.ui; + +import android.app.PendingIntent; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Intent; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.AutoCompleteTextView; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import android.widget.CompoundButton.OnCheckedChangeListener; +import android.widget.RelativeLayout; +import android.widget.TextView; +import android.widget.Toast; +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate; +import eu.siacs.conversations.ui.adapter.KnownHostsAdapter; +import eu.siacs.conversations.utils.UIHelper; +import eu.siacs.conversations.utils.Validator; +import eu.siacs.conversations.xmpp.XmppConnection.Features; +import eu.siacs.conversations.xmpp.pep.Avatar; + +public class EditAccountActivity extends XmppActivity { + + private AutoCompleteTextView mAccountJid; + private EditText mPassword; + private EditText mPasswordConfirm; + private CheckBox mRegisterNew; + private Button mCancelButton; + private Button mSaveButton; + + private LinearLayout mStats; + private TextView mServerInfoSm; + private TextView mServerInfoCarbons; + private TextView mServerInfoPep; + private TextView mSessionEst; + private TextView mOtrFingerprint; + private RelativeLayout mOtrFingerprintBox; + private ImageButton mOtrFingerprintToClipboardButton; + + private String jidToEdit; + private Account mAccount; + + private boolean mFetchingAvatar = false; + + private OnClickListener mSaveButtonClickListener = new OnClickListener() { + + @Override + public void onClick(View v) { + if (mAccount != null + && mAccount.getStatus() == Account.STATUS_DISABLED) { + mAccount.setOption(Account.OPTION_DISABLED, false); + xmppConnectionService.updateAccount(mAccount); + return; + } + if (!Validator.isValidJid(mAccountJid.getText().toString())) { + mAccountJid.setError(getString(R.string.invalid_jid)); + mAccountJid.requestFocus(); + return; + } + boolean registerNewAccount = mRegisterNew.isChecked(); + String[] jidParts = mAccountJid.getText().toString().split("@"); + String username = jidParts[0]; + String server; + if (jidParts.length >= 2) { + server = jidParts[1]; + } else { + server = ""; + } + String password = mPassword.getText().toString(); + String passwordConfirm = mPasswordConfirm.getText().toString(); + if (registerNewAccount) { + if (!password.equals(passwordConfirm)) { + mPasswordConfirm + .setError(getString(R.string.passwords_do_not_match)); + mPasswordConfirm.requestFocus(); + return; + } + } + if (mAccount != null) { + mAccount.setPassword(password); + mAccount.setUsername(username); + mAccount.setServer(server); + mAccount.setOption(Account.OPTION_REGISTER, registerNewAccount); + xmppConnectionService.updateAccount(mAccount); + } else { + if (xmppConnectionService.findAccountByJid(mAccountJid + .getText().toString()) != null) { + mAccountJid + .setError(getString(R.string.account_already_exists)); + mAccountJid.requestFocus(); + return; + } + mAccount = new Account(username, server, password); + mAccount.setOption(Account.OPTION_USETLS, true); + mAccount.setOption(Account.OPTION_USECOMPRESSION, true); + mAccount.setOption(Account.OPTION_REGISTER, registerNewAccount); + xmppConnectionService.createAccount(mAccount); + } + if (jidToEdit != null) { + finish(); + } else { + updateSaveButton(); + updateAccountInformation(); + } + + } + }; + private OnClickListener mCancelButtonClickListener = new OnClickListener() { + + @Override + public void onClick(View v) { + finish(); + } + }; + private OnAccountUpdate mOnAccountUpdateListener = new OnAccountUpdate() { + + @Override + public void onAccountUpdate() { + runOnUiThread(new Runnable() { + + @Override + public void run() { + if (mAccount != null + && mAccount.getStatus() != Account.STATUS_ONLINE + && mFetchingAvatar) { + startActivity(new Intent(getApplicationContext(), + ManageAccountActivity.class)); + finish(); + } else if (jidToEdit == null && mAccount != null + && mAccount.getStatus() == Account.STATUS_ONLINE) { + if (!mFetchingAvatar) { + mFetchingAvatar = true; + xmppConnectionService.checkForAvatar(mAccount, + mAvatarFetchCallback); + } + } else { + updateSaveButton(); + } + if (mAccount != null) { + updateAccountInformation(); + } + } + }); + } + }; + private UiCallback mAvatarFetchCallback = new UiCallback() { + + @Override + public void userInputRequried(PendingIntent pi, Avatar avatar) { + finishInitialSetup(avatar); + } + + @Override + public void success(Avatar avatar) { + finishInitialSetup(avatar); + } + + @Override + public void error(int errorCode, Avatar avatar) { + finishInitialSetup(avatar); + } + }; + private KnownHostsAdapter mKnownHostsAdapter; + private TextWatcher mTextWatcher = new TextWatcher() { + + @Override + public void onTextChanged(CharSequence s, int start, int before, + int count) { + updateSaveButton(); + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, + int after) { + + } + + @Override + public void afterTextChanged(Editable s) { + + } + }; + + protected void finishInitialSetup(final Avatar avatar) { + runOnUiThread(new Runnable() { + + @Override + public void run() { + Intent intent; + if (avatar != null) { + intent = new Intent(getApplicationContext(), + StartConversationActivity.class); + } else { + intent = new Intent(getApplicationContext(), + PublishProfilePictureActivity.class); + intent.putExtra("account", mAccount.getJid()); + intent.putExtra("setup", true); + } + startActivity(intent); + finish(); + } + }); + } + + protected boolean inputDataDiffersFromAccount() { + if (mAccount == null) { + return true; + } else { + return (!mAccount.getJid().equals(mAccountJid.getText().toString())) + || (!mAccount.getPassword().equals( + mPassword.getText().toString()) || mAccount + .isOptionSet(Account.OPTION_REGISTER) != mRegisterNew + .isChecked()); + } + } + + protected void updateSaveButton() { + if (mAccount != null + && mAccount.getStatus() == Account.STATUS_CONNECTING) { + this.mSaveButton.setEnabled(false); + this.mSaveButton.setTextColor(getSecondaryTextColor()); + this.mSaveButton.setText(R.string.account_status_connecting); + } else if (mAccount != null + && mAccount.getStatus() == Account.STATUS_DISABLED) { + this.mSaveButton.setEnabled(true); + this.mSaveButton.setTextColor(getPrimaryTextColor()); + this.mSaveButton.setText(R.string.enable); + } else { + this.mSaveButton.setEnabled(true); + this.mSaveButton.setTextColor(getPrimaryTextColor()); + if (jidToEdit != null) { + if (mAccount != null + && mAccount.getStatus() == Account.STATUS_ONLINE) { + this.mSaveButton.setText(R.string.save); + if (!accountInfoEdited()) { + this.mSaveButton.setEnabled(false); + this.mSaveButton.setTextColor(getSecondaryTextColor()); + } + } else { + this.mSaveButton.setText(R.string.connect); + } + } else { + this.mSaveButton.setText(R.string.next); + } + } + } + + protected boolean accountInfoEdited() { + return (!this.mAccount.getJid().equals( + this.mAccountJid.getText().toString())) + || (!this.mAccount.getPassword().equals( + this.mPassword.getText().toString())); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_edit_account); + this.mAccountJid = (AutoCompleteTextView) findViewById(R.id.account_jid); + this.mAccountJid.addTextChangedListener(this.mTextWatcher); + this.mPassword = (EditText) findViewById(R.id.account_password); + this.mPassword.addTextChangedListener(this.mTextWatcher); + this.mPasswordConfirm = (EditText) findViewById(R.id.account_password_confirm); + this.mRegisterNew = (CheckBox) findViewById(R.id.account_register_new); + this.mStats = (LinearLayout) findViewById(R.id.stats); + this.mSessionEst = (TextView) findViewById(R.id.session_est); + this.mServerInfoCarbons = (TextView) findViewById(R.id.server_info_carbons); + this.mServerInfoSm = (TextView) findViewById(R.id.server_info_sm); + this.mServerInfoPep = (TextView) findViewById(R.id.server_info_pep); + this.mOtrFingerprint = (TextView) findViewById(R.id.otr_fingerprint); + this.mOtrFingerprintBox = (RelativeLayout) findViewById(R.id.otr_fingerprint_box); + this.mOtrFingerprintToClipboardButton = (ImageButton) findViewById(R.id.action_copy_to_clipboard); + this.mSaveButton = (Button) findViewById(R.id.save_button); + this.mCancelButton = (Button) findViewById(R.id.cancel_button); + this.mSaveButton.setOnClickListener(this.mSaveButtonClickListener); + this.mCancelButton.setOnClickListener(this.mCancelButtonClickListener); + this.mRegisterNew + .setOnCheckedChangeListener(new OnCheckedChangeListener() { + + @Override + public void onCheckedChanged(CompoundButton buttonView, + boolean isChecked) { + if (isChecked) { + mPasswordConfirm.setVisibility(View.VISIBLE); + } else { + mPasswordConfirm.setVisibility(View.GONE); + } + updateSaveButton(); + } + }); + } + + @Override + protected void onStart() { + super.onStart(); + if (getIntent() != null) { + this.jidToEdit = getIntent().getStringExtra("jid"); + if (this.jidToEdit != null) { + this.mRegisterNew.setVisibility(View.GONE); + getActionBar().setTitle(jidToEdit); + } else { + getActionBar().setTitle(R.string.action_add_account); + } + } + } + + @Override + protected void onStop() { + if (xmppConnectionServiceBound) { + xmppConnectionService.removeOnAccountListChangedListener(); + } + super.onStop(); + } + + @Override + protected void onBackendConnected() { + this.mKnownHostsAdapter = new KnownHostsAdapter(this, + android.R.layout.simple_list_item_1, + xmppConnectionService.getKnownHosts()); + this.xmppConnectionService + .setOnAccountListChangedListener(this.mOnAccountUpdateListener); + if (this.jidToEdit != null) { + this.mAccount = xmppConnectionService.findAccountByJid(jidToEdit); + updateAccountInformation(); + } else if (this.xmppConnectionService.getAccounts().size() == 0) { + getActionBar().setDisplayHomeAsUpEnabled(false); + getActionBar().setDisplayShowHomeEnabled(false); + this.mCancelButton.setEnabled(false); + this.mCancelButton.setTextColor(getSecondaryTextColor()); + } + this.mAccountJid.setAdapter(this.mKnownHostsAdapter); + updateSaveButton(); + } + + private void updateAccountInformation() { + this.mAccountJid.setText(this.mAccount.getJid()); + this.mPassword.setText(this.mAccount.getPassword()); + if (this.mAccount.isOptionSet(Account.OPTION_REGISTER)) { + this.mRegisterNew.setVisibility(View.VISIBLE); + this.mRegisterNew.setChecked(true); + this.mPasswordConfirm.setText(this.mAccount.getPassword()); + } else { + this.mRegisterNew.setVisibility(View.GONE); + this.mRegisterNew.setChecked(false); + } + if (this.mAccount.getStatus() == Account.STATUS_ONLINE + && !this.mFetchingAvatar) { + this.mStats.setVisibility(View.VISIBLE); + this.mSessionEst.setText(UIHelper.readableTimeDifference( + getApplicationContext(), this.mAccount.getXmppConnection() + .getLastSessionEstablished())); + Features features = this.mAccount.getXmppConnection().getFeatures(); + if (features.carbons()) { + this.mServerInfoCarbons.setText(R.string.server_info_available); + } else { + this.mServerInfoCarbons + .setText(R.string.server_info_unavailable); + } + if (features.sm()) { + this.mServerInfoSm.setText(R.string.server_info_available); + } else { + this.mServerInfoSm.setText(R.string.server_info_unavailable); + } + if (features.pubsub()) { + this.mServerInfoPep.setText(R.string.server_info_available); + } else { + this.mServerInfoPep.setText(R.string.server_info_unavailable); + } + final String fingerprint = this.mAccount + .getOtrFingerprint(xmppConnectionService); + if (fingerprint != null) { + this.mOtrFingerprintBox.setVisibility(View.VISIBLE); + this.mOtrFingerprint.setText(fingerprint); + this.mOtrFingerprintToClipboardButton + .setVisibility(View.VISIBLE); + this.mOtrFingerprintToClipboardButton + .setOnClickListener(new View.OnClickListener() { + + @Override + public void onClick(View v) { + + if (OtrFingerprintToClipBoard(fingerprint)) { + Toast.makeText( + EditAccountActivity.this, + R.string.toast_message_otr_fingerprint, + Toast.LENGTH_SHORT).show(); + } + } + }); + } else { + this.mOtrFingerprintBox.setVisibility(View.GONE); + } + } else { + if (this.mAccount.errorStatus()) { + this.mAccountJid.setError(getString(this.mAccount + .getReadableStatusId())); + this.mAccountJid.requestFocus(); + } + this.mStats.setVisibility(View.GONE); + } + } + + private boolean OtrFingerprintToClipBoard(String fingerprint) { + ClipboardManager mClipBoardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); + String label = getResources().getString(R.string.otr_fingerprint); + if (mClipBoardManager != null) { + ClipData mClipData = ClipData.newPlainText(label, fingerprint); + mClipBoardManager.setPrimaryClip(mClipData); + return true; + } + return false; + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/EditMessage.java b/conversations/src/main/java/eu/siacs/conversations/ui/EditMessage.java new file mode 100644 index 00000000..f8302050 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/ui/EditMessage.java @@ -0,0 +1,39 @@ +package eu.siacs.conversations.ui; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.widget.EditText; + +public class EditMessage extends EditText { + + public EditMessage(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public EditMessage(Context context) { + super(context); + } + + protected OnEnterPressed mOnEnterPressed; + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_ENTER) { + if (mOnEnterPressed != null) { + mOnEnterPressed.onEnterPressed(); + } + return true; + } + return super.onKeyDown(keyCode, event); + } + + public void setOnEnterPressedListener(OnEnterPressed listener) { + this.mOnEnterPressed = listener; + } + + public interface OnEnterPressed { + public void onEnterPressed(); + } + +} diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java b/conversations/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java new file mode 100644 index 00000000..5b5b0608 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java @@ -0,0 +1,217 @@ +package eu.siacs.conversations.ui; + +import java.util.ArrayList; +import java.util.List; + +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate; +import eu.siacs.conversations.ui.adapter.AccountAdapter; +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.Intent; +import android.os.Bundle; +import android.view.ContextMenu; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ContextMenu.ContextMenuInfo; +import android.widget.AdapterView; +import android.widget.AdapterView.AdapterContextMenuInfo; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ListView; + +public class ManageAccountActivity extends XmppActivity { + + protected Account selectedAccount = null; + + protected List accountList = new ArrayList(); + protected ListView accountListView; + protected AccountAdapter mAccountAdapter; + protected OnAccountUpdate accountChanged = new OnAccountUpdate() { + + @Override + public void onAccountUpdate() { + accountList.clear(); + accountList.addAll(xmppConnectionService.getAccounts()); + runOnUiThread(new Runnable() { + + @Override + public void run() { + mAccountAdapter.notifyDataSetChanged(); + } + }); + } + }; + + @Override + protected void onCreate(Bundle savedInstanceState) { + + super.onCreate(savedInstanceState); + + setContentView(R.layout.manage_accounts); + + accountListView = (ListView) findViewById(R.id.account_list); + this.mAccountAdapter = new AccountAdapter(this, accountList); + accountListView.setAdapter(this.mAccountAdapter); + accountListView.setOnItemClickListener(new OnItemClickListener() { + + @Override + public void onItemClick(AdapterView arg0, View view, + int position, long arg3) { + switchToAccount(accountList.get(position)); + } + }); + registerForContextMenu(accountListView); + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View v, + ContextMenuInfo menuInfo) { + super.onCreateContextMenu(menu, v, menuInfo); + ManageAccountActivity.this.getMenuInflater().inflate( + R.menu.manageaccounts_context, menu); + AdapterView.AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo; + this.selectedAccount = accountList.get(acmi.position); + if (this.selectedAccount.isOptionSet(Account.OPTION_DISABLED)) { + menu.findItem(R.id.mgmt_account_disable).setVisible(false); + menu.findItem(R.id.mgmt_account_announce_pgp).setVisible(false); + menu.findItem(R.id.mgmt_account_publish_avatar).setVisible(false); + } else { + menu.findItem(R.id.mgmt_account_enable).setVisible(false); + } + menu.setHeaderTitle(this.selectedAccount.getJid()); + } + + @Override + protected void onStop() { + if (xmppConnectionServiceBound) { + xmppConnectionService.removeOnAccountListChangedListener(); + } + super.onStop(); + } + + @Override + void onBackendConnected() { + xmppConnectionService.setOnAccountListChangedListener(accountChanged); + this.accountList.clear(); + this.accountList.addAll(xmppConnectionService.getAccounts()); + mAccountAdapter.notifyDataSetChanged(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.manageaccounts, menu); + return true; + } + + @Override + public boolean onContextItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.mgmt_account_publish_avatar: + publishAvatar(selectedAccount); + return true; + case R.id.mgmt_account_disable: + disableAccount(selectedAccount); + return true; + case R.id.mgmt_account_enable: + enableAccount(selectedAccount); + return true; + case R.id.mgmt_account_delete: + deleteAccount(selectedAccount); + return true; + case R.id.mgmt_account_announce_pgp: + publishOpenPGPPublicKey(selectedAccount); + default: + return super.onContextItemSelected(item); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.action_add_account: + startActivity(new Intent(getApplicationContext(), + EditAccountActivity.class)); + break; + default: + break; + } + return super.onOptionsItemSelected(item); + } + + @Override + public boolean onNavigateUp() { + if (xmppConnectionService.getConversations().size() == 0) { + Intent contactsIntent = new Intent(this, + StartConversationActivity.class); + contactsIntent.setFlags( + // if activity exists in stack, pop the stack and go back to it + Intent.FLAG_ACTIVITY_CLEAR_TOP | + // otherwise, make a new task for it + Intent.FLAG_ACTIVITY_NEW_TASK | + // don't use the new activity animation; finish + // animation runs instead + Intent.FLAG_ACTIVITY_NO_ANIMATION); + startActivity(contactsIntent); + finish(); + return true; + } else { + return super.onNavigateUp(); + } + } + + private void publishAvatar(Account account) { + Intent intent = new Intent(getApplicationContext(), + PublishProfilePictureActivity.class); + intent.putExtra("account", account.getJid()); + startActivity(intent); + } + + private void disableAccount(Account account) { + account.setOption(Account.OPTION_DISABLED, true); + xmppConnectionService.updateAccount(account); + } + + private void enableAccount(Account account) { + account.setOption(Account.OPTION_DISABLED, false); + xmppConnectionService.updateAccount(account); + } + + private void publishOpenPGPPublicKey(Account account) { + if (ManageAccountActivity.this.hasPgp()) { + announcePgp(account, null); + } else { + this.showInstallPgpDialog(); + } + } + + private void deleteAccount(final Account account) { + AlertDialog.Builder builder = new AlertDialog.Builder( + ManageAccountActivity.this); + builder.setTitle(getString(R.string.mgmt_account_are_you_sure)); + builder.setIconAttribute(android.R.attr.alertDialogIcon); + builder.setMessage(getString(R.string.mgmt_account_delete_confirm_text)); + builder.setPositiveButton(getString(R.string.delete), + new OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + xmppConnectionService.deleteAccount(account); + selectedAccount = null; + } + }); + builder.setNegativeButton(getString(R.string.cancel), null); + builder.create().show(); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (resultCode == RESULT_OK) { + if (requestCode == REQUEST_ANNOUNCE_PGP) { + announcePgp(selectedAccount, null); + } + } + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java b/conversations/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java new file mode 100644 index 00000000..6aa40c41 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java @@ -0,0 +1,242 @@ +package eu.siacs.conversations.ui; + +import android.app.PendingIntent; +import android.content.Intent; +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.Bundle; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnLongClickListener; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.TextView; +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.utils.PhoneHelper; +import eu.siacs.conversations.xmpp.pep.Avatar; + +public class PublishProfilePictureActivity extends XmppActivity { + + private static final int REQUEST_CHOOSE_FILE = 0xac23; + + private ImageView avatar; + private TextView accountTextView; + private TextView hintOrWarning; + private TextView secondaryHint; + private Button cancelButton; + private Button publishButton; + + private Uri avatarUri; + private Uri defaultUri; + + private Account account; + + private boolean support = false; + + private boolean mInitialAccountSetup; + + private UiCallback avatarPublication = new UiCallback() { + + @Override + public void success(Avatar object) { + runOnUiThread(new Runnable() { + + @Override + public void run() { + if (mInitialAccountSetup) { + startActivity(new Intent(getApplicationContext(), + StartConversationActivity.class)); + } + finish(); + } + }); + } + + @Override + public void error(final int errorCode, Avatar object) { + runOnUiThread(new Runnable() { + + @Override + public void run() { + hintOrWarning.setText(errorCode); + hintOrWarning.setTextColor(getWarningTextColor()); + publishButton.setText(R.string.publish); + enablePublishButton(); + } + }); + + } + + @Override + public void userInputRequried(PendingIntent pi, Avatar object) { + } + }; + + private OnLongClickListener backToDefaultListener = new OnLongClickListener() { + + @Override + public boolean onLongClick(View v) { + avatarUri = defaultUri; + loadImageIntoPreview(defaultUri); + return true; + } + }; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_publish_profile_picture); + this.avatar = (ImageView) findViewById(R.id.account_image); + this.cancelButton = (Button) findViewById(R.id.cancel_button); + this.publishButton = (Button) findViewById(R.id.publish_button); + this.accountTextView = (TextView) findViewById(R.id.account); + this.hintOrWarning = (TextView) findViewById(R.id.hint_or_warning); + this.secondaryHint = (TextView) findViewById(R.id.secondary_hint); + this.publishButton.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + if (avatarUri != null) { + publishButton.setText(R.string.publishing); + disablePublishButton(); + xmppConnectionService.publishAvatar(account, avatarUri, + avatarPublication); + } + } + }); + this.cancelButton.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + if (mInitialAccountSetup) { + startActivity(new Intent(getApplicationContext(), + StartConversationActivity.class)); + } + finish(); + } + }); + this.avatar.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + Intent attachFileIntent = new Intent(); + attachFileIntent.setType("image/*"); + attachFileIntent.setAction(Intent.ACTION_GET_CONTENT); + Intent chooser = Intent.createChooser(attachFileIntent, + getString(R.string.attach_file)); + startActivityForResult(chooser, REQUEST_CHOOSE_FILE); + } + }); + this.defaultUri = PhoneHelper.getSefliUri(getApplicationContext()); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, + final Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (resultCode == RESULT_OK) { + if (requestCode == REQUEST_CHOOSE_FILE) { + this.avatarUri = data.getData(); + if (xmppConnectionServiceBound) { + loadImageIntoPreview(this.avatarUri); + } + } + } + } + + @Override + protected void onBackendConnected() { + if (getIntent() != null) { + String jid = getIntent().getStringExtra("account"); + if (jid != null) { + this.account = xmppConnectionService.findAccountByJid(jid); + if (this.account.getXmppConnection() != null) { + this.support = this.account.getXmppConnection() + .getFeatures().pubsub(); + } + if (this.avatarUri == null) { + if (this.account.getAvatar() != null + || this.defaultUri == null) { + this.avatar.setImageBitmap(avatarService().get(account, + getPixel(194))); + if (this.defaultUri != null) { + this.avatar + .setOnLongClickListener(this.backToDefaultListener); + } else { + this.secondaryHint.setVisibility(View.INVISIBLE); + } + if (!support) { + this.hintOrWarning + .setTextColor(getWarningTextColor()); + this.hintOrWarning + .setText(R.string.error_publish_avatar_no_server_support); + } + } else { + this.avatarUri = this.defaultUri; + loadImageIntoPreview(this.defaultUri); + this.secondaryHint.setVisibility(View.INVISIBLE); + } + } else { + loadImageIntoPreview(avatarUri); + } + this.accountTextView.setText(this.account.getJid()); + } + } + + } + + @Override + protected void onStart() { + super.onStart(); + if (getIntent() != null) { + this.mInitialAccountSetup = getIntent().getBooleanExtra("setup", + false); + } + if (this.mInitialAccountSetup) { + this.cancelButton.setText(R.string.skip); + } + } + + protected void loadImageIntoPreview(Uri uri) { + Bitmap bm = xmppConnectionService.getFileBackend().cropCenterSquare( + uri, 384); + if (bm == null) { + disablePublishButton(); + this.hintOrWarning.setTextColor(getWarningTextColor()); + this.hintOrWarning + .setText(R.string.error_publish_avatar_converting); + return; + } + this.avatar.setImageBitmap(bm); + if (support) { + enablePublishButton(); + this.publishButton.setText(R.string.publish); + this.hintOrWarning.setText(R.string.publish_avatar_explanation); + this.hintOrWarning.setTextColor(getPrimaryTextColor()); + } else { + disablePublishButton(); + this.hintOrWarning.setTextColor(getWarningTextColor()); + this.hintOrWarning + .setText(R.string.error_publish_avatar_no_server_support); + } + if (this.defaultUri != null && uri.equals(this.defaultUri)) { + this.secondaryHint.setVisibility(View.INVISIBLE); + this.avatar.setOnLongClickListener(null); + } else if (this.defaultUri != null) { + this.secondaryHint.setVisibility(View.VISIBLE); + this.avatar.setOnLongClickListener(this.backToDefaultListener); + } + } + + protected void enablePublishButton() { + this.publishButton.setEnabled(true); + this.publishButton.setTextColor(getPrimaryTextColor()); + } + + protected void disablePublishButton() { + this.publishButton.setEnabled(false); + this.publishButton.setTextColor(getSecondaryTextColor()); + } + +} diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java b/conversations/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java new file mode 100644 index 00000000..fc6308fc --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java @@ -0,0 +1,74 @@ +package eu.siacs.conversations.ui; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Locale; + +import eu.siacs.conversations.entities.Account; +import android.content.SharedPreferences; +import android.content.SharedPreferences.OnSharedPreferenceChangeListener; +import android.os.Build; +import android.os.Bundle; +import android.preference.ListPreference; +import android.preference.PreferenceManager; + +public class SettingsActivity extends XmppActivity implements + OnSharedPreferenceChangeListener { + private SettingsFragment mSettingsFragment; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mSettingsFragment = new SettingsFragment(); + getFragmentManager().beginTransaction() + .replace(android.R.id.content, mSettingsFragment).commit(); + } + + @Override + void onBackendConnected() { + + } + + @Override + public void onStart() { + super.onStart(); + PreferenceManager.getDefaultSharedPreferences(this) + .registerOnSharedPreferenceChangeListener(this); + ListPreference resources = (ListPreference) mSettingsFragment + .findPreference("resource"); + if (resources != null) { + ArrayList entries = new ArrayList( + Arrays.asList(resources.getEntries())); + entries.add(0, Build.MODEL); + resources.setEntries(entries.toArray(new CharSequence[entries + .size()])); + resources.setEntryValues(entries.toArray(new CharSequence[entries + .size()])); + } + } + + @Override + public void onStop() { + super.onStop(); + PreferenceManager.getDefaultSharedPreferences(this) + .unregisterOnSharedPreferenceChangeListener(this); + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences preferences, + String name) { + if (name.equals("resource")) { + String resource = preferences.getString("resource", "mobile") + .toLowerCase(Locale.US); + if (xmppConnectionServiceBound) { + for (Account account : xmppConnectionService.getAccounts()) { + account.setResource(resource); + if (!account.isOptionSet(Account.OPTION_DISABLED)) { + xmppConnectionService.reconnectAccount(account, false); + } + } + } + } + } + +} diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/SettingsFragment.java b/conversations/src/main/java/eu/siacs/conversations/ui/SettingsFragment.java new file mode 100644 index 00000000..7e1c3698 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/ui/SettingsFragment.java @@ -0,0 +1,15 @@ +package eu.siacs.conversations.ui; + +import eu.siacs.conversations.R; +import android.os.Bundle; +import android.preference.PreferenceFragment; + +public class SettingsFragment extends PreferenceFragment { + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Load the preferences from an XML resource + addPreferencesFromResource(R.xml.preferences); + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java b/conversations/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java new file mode 100644 index 00000000..9fbc3db1 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java @@ -0,0 +1,185 @@ +package eu.siacs.conversations.ui; + +import java.util.ArrayList; +import java.util.List; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.ui.adapter.ConversationAdapter; +import android.app.PendingIntent; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ListView; +import android.widget.Toast; + +public class ShareWithActivity extends XmppActivity { + + private class Share { + public Uri uri; + public String account; + public String contact; + public String text; + } + + private Share share; + + private static final int REQUEST_START_NEW_CONVERSATION = 0x0501; + private ListView mListView; + private List mConversations = new ArrayList(); + + private UiCallback attachImageCallback = new UiCallback() { + + @Override + public void userInputRequried(PendingIntent pi, Message object) { + // TODO Auto-generated method stub + + } + + @Override + public void success(Message message) { + xmppConnectionService.sendMessage(message); + } + + @Override + public void error(int errorCode, Message object) { + // TODO Auto-generated method stub + + } + }; + + protected void onActivityResult(int requestCode, int resultCode, + final Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == REQUEST_START_NEW_CONVERSATION + && resultCode == RESULT_OK) { + share.contact = data.getStringExtra("contact"); + share.account = data.getStringExtra("account"); + Log.d(Config.LOGTAG, "contact: " + share.contact + " account:" + + share.account); + } + if (xmppConnectionServiceBound && share != null + && share.contact != null && share.account != null) { + share(); + } + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + + super.onCreate(savedInstanceState); + + getActionBar().setDisplayHomeAsUpEnabled(false); + getActionBar().setHomeButtonEnabled(false); + + setContentView(R.layout.share_with); + setTitle(getString(R.string.title_activity_sharewith)); + + mListView = (ListView) findViewById(R.id.choose_conversation_list); + ConversationAdapter mAdapter = new ConversationAdapter(this, + this.mConversations); + mListView.setAdapter(mAdapter); + mListView.setOnItemClickListener(new OnItemClickListener() { + + @Override + public void onItemClick(AdapterView arg0, View arg1, + int position, long arg3) { + Conversation conversation = mConversations.get(position); + if (conversation.getMode() == Conversation.MODE_SINGLE + || share.uri == null) { + share(mConversations.get(position)); + } + } + }); + + this.share = new Share(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.share_with, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.action_add: + Intent intent = new Intent(getApplicationContext(), + ChooseContactActivity.class); + startActivityForResult(intent, REQUEST_START_NEW_CONVERSATION); + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + public void onStart() { + if (getIntent().getType() != null + && getIntent().getType().startsWith("image/")) { + this.share.uri = (Uri) getIntent().getParcelableExtra( + Intent.EXTRA_STREAM); + } else { + this.share.text = getIntent().getStringExtra(Intent.EXTRA_TEXT); + } + if (xmppConnectionServiceBound) { + xmppConnectionService.populateWithOrderedConversations( + mConversations, this.share.uri == null); + } + super.onStart(); + } + + @Override + void onBackendConnected() { + if (xmppConnectionServiceBound && share != null + && share.contact != null && share.account != null) { + share(); + return; + } + xmppConnectionService.populateWithOrderedConversations(mConversations, + this.share != null && this.share.uri == null); + } + + private void share() { + Account account = xmppConnectionService.findAccountByJid(share.account); + if (account == null) { + return; + } + Conversation conversation = xmppConnectionService + .findOrCreateConversation(account, share.contact, false); + share(conversation); + } + + private void share(final Conversation conversation) { + if (share.uri != null) { + selectPresence(conversation, new OnPresenceSelected() { + @Override + public void onPresenceSelected() { + Toast.makeText(getApplicationContext(), + getText(R.string.preparing_image), + Toast.LENGTH_LONG).show(); + ShareWithActivity.this.xmppConnectionService + .attachImageToConversation(conversation, share.uri, + attachImageCallback); + switchToConversation(conversation, null, true); + finish(); + } + }); + + } else { + switchToConversation(conversation, this.share.text, true); + finish(); + } + + } + +} \ No newline at end of file diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java b/conversations/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java new file mode 100644 index 00000000..a1a2d4c2 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java @@ -0,0 +1,677 @@ +package eu.siacs.conversations.ui; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import android.annotation.SuppressLint; +import android.app.ActionBar; +import android.app.ActionBar.Tab; +import android.app.ActionBar.TabListener; +import android.app.AlertDialog; +import android.app.Fragment; +import android.app.FragmentTransaction; +import android.app.ListFragment; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.support.v13.app.FragmentPagerAdapter; +import android.support.v4.view.ViewPager; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.ContextMenu; +import android.view.ContextMenu.ContextMenuInfo; +import android.view.KeyEvent; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import android.widget.AdapterView; +import android.widget.AdapterView.AdapterContextMenuInfo; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ArrayAdapter; +import android.widget.AutoCompleteTextView; +import android.widget.CheckBox; +import android.widget.EditText; +import android.widget.ListView; +import android.widget.Spinner; +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Bookmark; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.ListItem; +import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate; +import eu.siacs.conversations.ui.adapter.KnownHostsAdapter; +import eu.siacs.conversations.ui.adapter.ListItemAdapter; +import eu.siacs.conversations.utils.Validator; + +public class StartConversationActivity extends XmppActivity { + + private Tab mContactsTab; + private Tab mConferencesTab; + private ViewPager mViewPager; + + private MyListFragment mContactsListFragment = new MyListFragment(); + private List contacts = new ArrayList(); + private ArrayAdapter mContactsAdapter; + + private MyListFragment mConferenceListFragment = new MyListFragment(); + private List conferences = new ArrayList(); + private ArrayAdapter mConferenceAdapter; + + private List mActivatedAccounts = new ArrayList(); + private List mKnownHosts; + private List mKnownConferenceHosts; + + private Menu mOptionsMenu; + private EditText mSearchEditText; + + public int conference_context_id; + public int contact_context_id; + + private TabListener mTabListener = new TabListener() { + + @Override + public void onTabUnselected(Tab tab, FragmentTransaction ft) { + return; + } + + @Override + public void onTabSelected(Tab tab, FragmentTransaction ft) { + mViewPager.setCurrentItem(tab.getPosition()); + onTabChanged(); + } + + @Override + public void onTabReselected(Tab tab, FragmentTransaction ft) { + return; + } + }; + + private ViewPager.SimpleOnPageChangeListener mOnPageChangeListener = new ViewPager.SimpleOnPageChangeListener() { + @Override + public void onPageSelected(int position) { + getActionBar().setSelectedNavigationItem(position); + onTabChanged(); + } + }; + + private MenuItem.OnActionExpandListener mOnActionExpandListener = new MenuItem.OnActionExpandListener() { + + @Override + public boolean onMenuItemActionExpand(MenuItem item) { + mSearchEditText.post(new Runnable() { + + @Override + public void run() { + mSearchEditText.requestFocus(); + InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + imm.showSoftInput(mSearchEditText, + InputMethodManager.SHOW_IMPLICIT); + } + }); + + return true; + } + + @Override + public boolean onMenuItemActionCollapse(MenuItem item) { + InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(mSearchEditText.getWindowToken(), + InputMethodManager.HIDE_IMPLICIT_ONLY); + mSearchEditText.setText(""); + filter(null); + return true; + } + }; + private TextWatcher mSearchTextWatcher = new TextWatcher() { + + @Override + public void afterTextChanged(Editable editable) { + filter(editable.toString()); + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, + int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, + int count) { + } + }; + private OnRosterUpdate onRosterUpdate = new OnRosterUpdate() { + + @Override + public void onRosterUpdate() { + runOnUiThread(new Runnable() { + + @Override + public void run() { + if (mSearchEditText != null) { + filter(mSearchEditText.getText().toString()); + } + } + }); + } + }; + private MenuItem mMenuSearchView; + private String mInitialJid; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_start_conversation); + mViewPager = (ViewPager) findViewById(R.id.start_conversation_view_pager); + ActionBar actionBar = getActionBar(); + actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS); + + mContactsTab = actionBar.newTab().setText(R.string.contacts) + .setTabListener(mTabListener); + mConferencesTab = actionBar.newTab().setText(R.string.conferences) + .setTabListener(mTabListener); + actionBar.addTab(mContactsTab); + actionBar.addTab(mConferencesTab); + + mViewPager.setOnPageChangeListener(mOnPageChangeListener); + mViewPager.setAdapter(new FragmentPagerAdapter(getFragmentManager()) { + + @Override + public int getCount() { + return 2; + } + + @Override + public Fragment getItem(int position) { + if (position == 0) { + return mContactsListFragment; + } else { + return mConferenceListFragment; + } + } + }); + + mConferenceAdapter = new ListItemAdapter(this, conferences); + mConferenceListFragment.setListAdapter(mConferenceAdapter); + mConferenceListFragment.setContextMenu(R.menu.conference_context); + mConferenceListFragment + .setOnListItemClickListener(new OnItemClickListener() { + + @Override + public void onItemClick(AdapterView arg0, View arg1, + int position, long arg3) { + openConversationForBookmark(position); + } + }); + + mContactsAdapter = new ListItemAdapter(this, contacts); + mContactsListFragment.setListAdapter(mContactsAdapter); + mContactsListFragment.setContextMenu(R.menu.contact_context); + mContactsListFragment + .setOnListItemClickListener(new OnItemClickListener() { + + @Override + public void onItemClick(AdapterView arg0, View arg1, + int position, long arg3) { + openConversationForContact(position); + } + }); + + } + + @Override + public void onStop() { + super.onStop(); + xmppConnectionService.removeOnRosterUpdateListener(); + } + + protected void openConversationForContact(int position) { + Contact contact = (Contact) contacts.get(position); + Conversation conversation = xmppConnectionService + .findOrCreateConversation(contact.getAccount(), + contact.getJid(), false); + switchToConversation(conversation); + } + + protected void openConversationForContact() { + int position = contact_context_id; + openConversationForContact(position); + } + + protected void openConversationForBookmark() { + openConversationForBookmark(conference_context_id); + } + + protected void openConversationForBookmark(int position) { + Bookmark bookmark = (Bookmark) conferences.get(position); + Conversation conversation = xmppConnectionService + .findOrCreateConversation(bookmark.getAccount(), + bookmark.getJid(), true); + conversation.setBookmark(bookmark); + if (!conversation.getMucOptions().online()) { + xmppConnectionService.joinMuc(conversation); + } + if (!bookmark.autojoin()) { + bookmark.setAutojoin(true); + xmppConnectionService.pushBookmarks(bookmark.getAccount()); + } + switchToConversation(conversation); + } + + protected void openDetailsForContact() { + int position = contact_context_id; + Contact contact = (Contact) contacts.get(position); + switchToContactDetails(contact); + } + + protected void deleteContact() { + int position = contact_context_id; + final Contact contact = (Contact) contacts.get(position); + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setNegativeButton(R.string.cancel, null); + builder.setTitle(R.string.action_delete_contact); + builder.setMessage(getString(R.string.remove_contact_text, + contact.getJid())); + builder.setPositiveButton(R.string.delete, new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + xmppConnectionService.deleteContactOnServer(contact); + filter(mSearchEditText.getText().toString()); + } + }); + builder.create().show(); + + } + + protected void deleteConference() { + int position = conference_context_id; + final Bookmark bookmark = (Bookmark) conferences.get(position); + + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setNegativeButton(R.string.cancel, null); + builder.setTitle(R.string.delete_bookmark); + builder.setMessage(getString(R.string.remove_bookmark_text, + bookmark.getJid())); + builder.setPositiveButton(R.string.delete, new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + bookmark.unregisterConversation(); + Account account = bookmark.getAccount(); + account.getBookmarks().remove(bookmark); + xmppConnectionService.pushBookmarks(account); + filter(mSearchEditText.getText().toString()); + } + }); + builder.create().show(); + + } + + @SuppressLint("InflateParams") + protected void showCreateContactDialog(String prefilledJid) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(R.string.create_contact); + View dialogView = getLayoutInflater().inflate( + R.layout.create_contact_dialog, null); + final Spinner spinner = (Spinner) dialogView.findViewById(R.id.account); + final AutoCompleteTextView jid = (AutoCompleteTextView) dialogView + .findViewById(R.id.jid); + jid.setAdapter(new KnownHostsAdapter(this, + android.R.layout.simple_list_item_1, mKnownHosts)); + if (prefilledJid != null) { + jid.append(prefilledJid); + } + populateAccountSpinner(spinner); + builder.setView(dialogView); + builder.setNegativeButton(R.string.cancel, null); + builder.setPositiveButton(R.string.create, null); + final AlertDialog dialog = builder.create(); + dialog.show(); + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener( + new View.OnClickListener() { + + @Override + public void onClick(View v) { + if (!xmppConnectionServiceBound) { + return; + } + if (Validator.isValidJid(jid.getText().toString())) { + String accountJid = (String) spinner + .getSelectedItem(); + String contactJid = jid.getText().toString(); + Account account = xmppConnectionService + .findAccountByJid(accountJid); + if (account == null) { + dialog.dismiss(); + return; + } + Contact contact = account.getRoster().getContact( + contactJid); + if (contact.showInRoster()) { + jid.setError(getString(R.string.contact_already_exists)); + } else { + xmppConnectionService.createContact(contact); + dialog.dismiss(); + switchToConversation(contact); + } + } else { + jid.setError(getString(R.string.invalid_jid)); + } + } + }); + + } + + @SuppressLint("InflateParams") + protected void showJoinConferenceDialog() { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(R.string.join_conference); + View dialogView = getLayoutInflater().inflate( + R.layout.join_conference_dialog, null); + final Spinner spinner = (Spinner) dialogView.findViewById(R.id.account); + final AutoCompleteTextView jid = (AutoCompleteTextView) dialogView + .findViewById(R.id.jid); + jid.setAdapter(new KnownHostsAdapter(this, + android.R.layout.simple_list_item_1, mKnownConferenceHosts)); + populateAccountSpinner(spinner); + final CheckBox bookmarkCheckBox = (CheckBox) dialogView + .findViewById(R.id.bookmark); + builder.setView(dialogView); + builder.setNegativeButton(R.string.cancel, null); + builder.setPositiveButton(R.string.join, null); + final AlertDialog dialog = builder.create(); + dialog.show(); + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener( + new View.OnClickListener() { + + @Override + public void onClick(View v) { + if (!xmppConnectionServiceBound) { + return; + } + if (Validator.isValidJid(jid.getText().toString())) { + String accountJid = (String) spinner + .getSelectedItem(); + String conferenceJid = jid.getText().toString(); + Account account = xmppConnectionService + .findAccountByJid(accountJid); + if (account == null) { + dialog.dismiss(); + return; + } + if (bookmarkCheckBox.isChecked()) { + if (account.hasBookmarkFor(conferenceJid)) { + jid.setError(getString(R.string.bookmark_already_exists)); + } else { + Bookmark bookmark = new Bookmark(account, + conferenceJid); + bookmark.setAutojoin(true); + account.getBookmarks().add(bookmark); + xmppConnectionService + .pushBookmarks(account); + Conversation conversation = xmppConnectionService + .findOrCreateConversation(account, + conferenceJid, true); + conversation.setBookmark(bookmark); + if (!conversation.getMucOptions().online()) { + xmppConnectionService + .joinMuc(conversation); + } + dialog.dismiss(); + switchToConversation(conversation); + } + } else { + Conversation conversation = xmppConnectionService + .findOrCreateConversation(account, + conferenceJid, true); + if (!conversation.getMucOptions().online()) { + xmppConnectionService.joinMuc(conversation); + } + dialog.dismiss(); + switchToConversation(conversation); + } + } else { + jid.setError(getString(R.string.invalid_jid)); + } + } + }); + } + + protected void switchToConversation(Contact contact) { + Conversation conversation = xmppConnectionService + .findOrCreateConversation(contact.getAccount(), + contact.getJid(), false); + switchToConversation(conversation); + } + + private void populateAccountSpinner(Spinner spinner) { + ArrayAdapter adapter = new ArrayAdapter(this, + android.R.layout.simple_spinner_item, mActivatedAccounts); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + spinner.setAdapter(adapter); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + this.mOptionsMenu = menu; + getMenuInflater().inflate(R.menu.start_conversation, menu); + MenuItem menuCreateContact = (MenuItem) menu + .findItem(R.id.action_create_contact); + MenuItem menuCreateConference = (MenuItem) menu + .findItem(R.id.action_join_conference); + mMenuSearchView = (MenuItem) menu.findItem(R.id.action_search); + mMenuSearchView.setOnActionExpandListener(mOnActionExpandListener); + View mSearchView = mMenuSearchView.getActionView(); + mSearchEditText = (EditText) mSearchView + .findViewById(R.id.search_field); + mSearchEditText.addTextChangedListener(mSearchTextWatcher); + if (getActionBar().getSelectedNavigationIndex() == 0) { + menuCreateConference.setVisible(false); + } else { + menuCreateContact.setVisible(false); + } + if (mInitialJid != null) { + mMenuSearchView.expandActionView(); + mSearchEditText.append(mInitialJid); + filter(mInitialJid); + } + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.action_create_contact: + showCreateContactDialog(null); + break; + case R.id.action_join_conference: + showJoinConferenceDialog(); + break; + } + return super.onOptionsItemSelected(item); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_SEARCH && !event.isLongPress()) { + mOptionsMenu.findItem(R.id.action_search).expandActionView(); + return true; + } + return super.onKeyUp(keyCode, event); + } + + @Override + protected void onBackendConnected() { + xmppConnectionService.setOnRosterUpdateListener(this.onRosterUpdate); + this.mActivatedAccounts.clear(); + for (Account account : xmppConnectionService.getAccounts()) { + if (account.getStatus() != Account.STATUS_DISABLED) { + this.mActivatedAccounts.add(account.getJid()); + } + } + this.mKnownHosts = xmppConnectionService.getKnownHosts(); + this.mKnownConferenceHosts = xmppConnectionService + .getKnownConferenceHosts(); + if (!startByIntent()) { + if (mSearchEditText != null) { + filter(mSearchEditText.getText().toString()); + } else { + filter(null); + } + } + } + + protected boolean startByIntent() { + if (getIntent() != null + && Intent.ACTION_SENDTO.equals(getIntent().getAction())) { + try { + String jid = URLDecoder.decode( + getIntent().getData().getEncodedPath(), "UTF-8").split( + "/")[1]; + setIntent(null); + return handleJid(jid); + } catch (UnsupportedEncodingException e) { + setIntent(null); + return false; + } + } else if (getIntent() != null + && Intent.ACTION_VIEW.equals(getIntent().getAction())) { + Uri uri = getIntent().getData(); + String jid = uri.getSchemeSpecificPart().split("\\?")[0]; + return handleJid(jid); + } + return false; + } + + private boolean handleJid(String jid) { + List contacts = xmppConnectionService.findContacts(jid); + if (contacts.size() == 0) { + showCreateContactDialog(jid); + return false; + } else if (contacts.size() == 1) { + switchToConversation(contacts.get(0)); + return true; + } else { + if (mMenuSearchView != null) { + mMenuSearchView.expandActionView(); + mSearchEditText.setText(jid); + filter(jid); + } else { + mInitialJid = jid; + } + return true; + } + } + + protected void filter(String needle) { + if (xmppConnectionServiceBound) { + this.filterContacts(needle); + this.filterConferences(needle); + } + } + + protected void filterContacts(String needle) { + this.contacts.clear(); + for (Account account : xmppConnectionService.getAccounts()) { + if (account.getStatus() != Account.STATUS_DISABLED) { + for (Contact contact : account.getRoster().getContacts()) { + if (contact.showInRoster() && contact.match(needle)) { + this.contacts.add(contact); + } + } + } + } + Collections.sort(this.contacts); + mContactsAdapter.notifyDataSetChanged(); + } + + protected void filterConferences(String needle) { + this.conferences.clear(); + for (Account account : xmppConnectionService.getAccounts()) { + if (account.getStatus() != Account.STATUS_DISABLED) { + for (Bookmark bookmark : account.getBookmarks()) { + if (bookmark.match(needle)) { + this.conferences.add(bookmark); + } + } + } + } + Collections.sort(this.conferences); + mConferenceAdapter.notifyDataSetChanged(); + } + + private void onTabChanged() { + invalidateOptionsMenu(); + } + + public static class MyListFragment extends ListFragment { + private AdapterView.OnItemClickListener mOnItemClickListener; + private int mResContextMenu; + + public void setContextMenu(int res) { + this.mResContextMenu = res; + } + + @Override + public void onListItemClick(ListView l, View v, int position, long id) { + if (mOnItemClickListener != null) { + mOnItemClickListener.onItemClick(l, v, position, id); + } + } + + public void setOnListItemClickListener(AdapterView.OnItemClickListener l) { + this.mOnItemClickListener = l; + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + registerForContextMenu(getListView()); + getListView().setFastScrollEnabled(true); + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View v, + ContextMenuInfo menuInfo) { + super.onCreateContextMenu(menu, v, menuInfo); + StartConversationActivity activity = (StartConversationActivity) getActivity(); + activity.getMenuInflater().inflate(mResContextMenu, menu); + AdapterView.AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo; + if (mResContextMenu == R.menu.conference_context) { + activity.conference_context_id = acmi.position; + } else { + activity.contact_context_id = acmi.position; + } + } + + @Override + public boolean onContextItemSelected(MenuItem item) { + StartConversationActivity activity = (StartConversationActivity) getActivity(); + switch (item.getItemId()) { + case R.id.context_start_conversation: + activity.openConversationForContact(); + break; + case R.id.context_contact_details: + activity.openDetailsForContact(); + break; + case R.id.context_delete_contact: + activity.deleteContact(); + break; + case R.id.context_join_conference: + activity.openConversationForBookmark(); + break; + case R.id.context_delete_conference: + activity.deleteConference(); + } + return true; + } + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/UiCallback.java b/conversations/src/main/java/eu/siacs/conversations/ui/UiCallback.java new file mode 100644 index 00000000..c80199e1 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/ui/UiCallback.java @@ -0,0 +1,11 @@ +package eu.siacs.conversations.ui; + +import android.app.PendingIntent; + +public interface UiCallback { + public void success(T object); + + public void error(int errorCode, T object); + + public void userInputRequried(PendingIntent pi, T object); +} diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/conversations/src/main/java/eu/siacs/conversations/ui/XmppActivity.java new file mode 100644 index 00000000..d26f0e31 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/ui/XmppActivity.java @@ -0,0 +1,637 @@ +package eu.siacs.conversations.ui; + +import java.io.FileNotFoundException; +import java.lang.ref.WeakReference; +import java.util.List; +import java.util.concurrent.RejectedExecutionException; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.entities.Presences; +import eu.siacs.conversations.services.AvatarService; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder; +import eu.siacs.conversations.utils.ExceptionHelper; +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.AlertDialog; +import android.app.PendingIntent; +import android.app.AlertDialog.Builder; +import android.content.ComponentName; +import android.content.Context; +import android.content.DialogInterface; +import android.content.SharedPreferences; +import android.content.DialogInterface.OnClickListener; +import android.content.IntentSender.SendIntentException; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.res.Resources; +import android.content.Intent; +import android.content.ServiceConnection; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.IBinder; +import android.preference.PreferenceManager; +import android.text.InputType; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.MenuItem; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import android.widget.EditText; +import android.widget.ImageView; + +public abstract class XmppActivity extends Activity { + + protected static final int REQUEST_ANNOUNCE_PGP = 0x0101; + protected static final int REQUEST_INVITE_TO_CONVERSATION = 0x0102; + + public XmppConnectionService xmppConnectionService; + public boolean xmppConnectionServiceBound = false; + protected boolean handledViewIntent = false; + + protected int mPrimaryTextColor; + protected int mSecondaryTextColor; + protected int mSecondaryBackgroundColor; + protected int mColorRed; + protected int mColorOrange; + protected int mColorGreen; + protected int mPrimaryColor; + + protected boolean mUseSubject = true; + + private DisplayMetrics metrics; + + protected interface OnValueEdited { + public void onValueEdited(String value); + } + + public interface OnPresenceSelected { + public void onPresenceSelected(); + } + + protected ServiceConnection mConnection = new ServiceConnection() { + + @Override + public void onServiceConnected(ComponentName className, IBinder service) { + XmppConnectionBinder binder = (XmppConnectionBinder) service; + xmppConnectionService = binder.getService(); + xmppConnectionServiceBound = true; + onBackendConnected(); + } + + @Override + public void onServiceDisconnected(ComponentName arg0) { + xmppConnectionServiceBound = false; + } + }; + + @Override + protected void onStart() { + super.onStart(); + if (!xmppConnectionServiceBound) { + connectToBackend(); + } + } + + public void connectToBackend() { + Intent intent = new Intent(this, XmppConnectionService.class); + intent.setAction("ui"); + startService(intent); + bindService(intent, mConnection, Context.BIND_AUTO_CREATE); + } + + @Override + protected void onStop() { + super.onStop(); + if (xmppConnectionServiceBound) { + unbindService(mConnection); + xmppConnectionServiceBound = false; + } + } + + protected void hideKeyboard() { + InputMethodManager inputManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + + View focus = getCurrentFocus(); + + if (focus != null) { + + inputManager.hideSoftInputFromWindow(focus.getWindowToken(), + InputMethodManager.HIDE_NOT_ALWAYS); + } + } + + public boolean hasPgp() { + return xmppConnectionService.getPgpEngine() != null; + } + + public void showInstallPgpDialog() { + Builder builder = new AlertDialog.Builder(this); + builder.setTitle(getString(R.string.openkeychain_required)); + builder.setIconAttribute(android.R.attr.alertDialogIcon); + builder.setMessage(getText(R.string.openkeychain_required_long)); + builder.setNegativeButton(getString(R.string.cancel), null); + builder.setNeutralButton(getString(R.string.restart), + new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + if (xmppConnectionServiceBound) { + unbindService(mConnection); + xmppConnectionServiceBound = false; + } + stopService(new Intent(XmppActivity.this, + XmppConnectionService.class)); + finish(); + } + }); + builder.setPositiveButton(getString(R.string.install), + new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + Uri uri = Uri + .parse("market://details?id=org.sufficientlysecure.keychain"); + Intent marketIntent = new Intent(Intent.ACTION_VIEW, + uri); + PackageManager manager = getApplicationContext() + .getPackageManager(); + List infos = manager + .queryIntentActivities(marketIntent, 0); + if (infos.size() > 0) { + startActivity(marketIntent); + } else { + uri = Uri.parse("http://www.openkeychain.org/"); + Intent browserIntent = new Intent( + Intent.ACTION_VIEW, uri); + startActivity(browserIntent); + } + finish(); + } + }); + builder.create().show(); + } + + abstract void onBackendConnected(); + + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.action_settings: + startActivity(new Intent(this, SettingsActivity.class)); + break; + case R.id.action_accounts: + startActivity(new Intent(this, ManageAccountActivity.class)); + break; + case android.R.id.home: + finish(); + break; + } + return super.onOptionsItemSelected(item); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + metrics = getResources().getDisplayMetrics(); + ExceptionHelper.init(getApplicationContext()); + mPrimaryTextColor = getResources().getColor(R.color.primarytext); + mSecondaryTextColor = getResources().getColor(R.color.secondarytext); + mColorRed = getResources().getColor(R.color.red); + mColorOrange = getResources().getColor(R.color.orange); + mColorGreen = getResources().getColor(R.color.green); + mPrimaryColor = getResources().getColor(R.color.primary); + mSecondaryBackgroundColor = getResources().getColor( + R.color.secondarybackground); + if (getPreferences().getBoolean("use_larger_font", false)) { + setTheme(R.style.ConversationsTheme_LargerText); + } + mUseSubject = getPreferences().getBoolean("use_subject", true); + } + + protected SharedPreferences getPreferences() { + return PreferenceManager + .getDefaultSharedPreferences(getApplicationContext()); + } + + public boolean useSubjectToIdentifyConference() { + return mUseSubject; + } + + public void switchToConversation(Conversation conversation) { + switchToConversation(conversation, null, false); + } + + public void switchToConversation(Conversation conversation, String text, + boolean newTask) { + Intent viewConversationIntent = new Intent(this, + ConversationActivity.class); + viewConversationIntent.setAction(Intent.ACTION_VIEW); + viewConversationIntent.putExtra(ConversationActivity.CONVERSATION, + conversation.getUuid()); + if (text != null) { + viewConversationIntent.putExtra(ConversationActivity.TEXT, text); + } + viewConversationIntent.setType(ConversationActivity.VIEW_CONVERSATION); + if (newTask) { + viewConversationIntent.setFlags(viewConversationIntent.getFlags() + | Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_SINGLE_TOP); + } else { + viewConversationIntent.setFlags(viewConversationIntent.getFlags() + | Intent.FLAG_ACTIVITY_CLEAR_TOP); + } + startActivity(viewConversationIntent); + finish(); + } + + public void switchToContactDetails(Contact contact) { + Intent intent = new Intent(this, ContactDetailsActivity.class); + intent.setAction(ContactDetailsActivity.ACTION_VIEW_CONTACT); + intent.putExtra("account", contact.getAccount().getJid()); + intent.putExtra("contact", contact.getJid()); + startActivity(intent); + } + + public void switchToAccount(Account account) { + Intent intent = new Intent(this, EditAccountActivity.class); + intent.putExtra("jid", account.getJid()); + startActivity(intent); + } + + protected void inviteToConversation(Conversation conversation) { + Intent intent = new Intent(getApplicationContext(), + ChooseContactActivity.class); + intent.putExtra("conversation", conversation.getUuid()); + startActivityForResult(intent, REQUEST_INVITE_TO_CONVERSATION); + } + + protected void announcePgp(Account account, final Conversation conversation) { + xmppConnectionService.getPgpEngine().generateSignature(account, + "online", new UiCallback() { + + @Override + public void userInputRequried(PendingIntent pi, + Account account) { + try { + startIntentSenderForResult(pi.getIntentSender(), + REQUEST_ANNOUNCE_PGP, null, 0, 0, 0); + } catch (SendIntentException e) { + } + } + + @Override + public void success(Account account) { + xmppConnectionService.databaseBackend + .updateAccount(account); + xmppConnectionService.sendPresencePacket(account, + xmppConnectionService.getPresenceGenerator() + .sendPresence(account)); + if (conversation != null) { + conversation + .setNextEncryption(Message.ENCRYPTION_PGP); + xmppConnectionService.databaseBackend + .updateConversation(conversation); + } + } + + @Override + public void error(int error, Account account) { + displayErrorDialog(error); + } + }); + } + + protected void displayErrorDialog(final int errorCode) { + runOnUiThread(new Runnable() { + + @Override + public void run() { + AlertDialog.Builder builder = new AlertDialog.Builder( + XmppActivity.this); + builder.setIconAttribute(android.R.attr.alertDialogIcon); + builder.setTitle(getString(R.string.error)); + builder.setMessage(errorCode); + builder.setNeutralButton(R.string.accept, null); + builder.create().show(); + } + }); + + } + + protected void showAddToRosterDialog(final Conversation conversation) { + String jid = conversation.getContactJid(); + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(jid); + builder.setMessage(getString(R.string.not_in_roster)); + builder.setNegativeButton(getString(R.string.cancel), null); + builder.setPositiveButton(getString(R.string.add_contact), + new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + String jid = conversation.getContactJid(); + Account account = conversation.getAccount(); + Contact contact = account.getRoster().getContact(jid); + xmppConnectionService.createContact(contact); + switchToContactDetails(contact); + } + }); + builder.create().show(); + } + + private void showAskForPresenceDialog(final Contact contact) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(contact.getJid()); + builder.setMessage(R.string.request_presence_updates); + builder.setNegativeButton(R.string.cancel, null); + builder.setPositiveButton(R.string.request_now, + new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + if (xmppConnectionServiceBound) { + xmppConnectionService.sendPresencePacket(contact + .getAccount(), xmppConnectionService + .getPresenceGenerator() + .requestPresenceUpdatesFrom(contact)); + } + } + }); + builder.create().show(); + } + + private void warnMutalPresenceSubscription(final Conversation conversation, + final OnPresenceSelected listener) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(conversation.getContact().getJid()); + builder.setMessage(R.string.without_mutual_presence_updates); + builder.setNegativeButton(R.string.cancel, null); + builder.setPositiveButton(R.string.ignore, new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + conversation.setNextPresence(null); + if (listener != null) { + listener.onPresenceSelected(); + } + } + }); + builder.create().show(); + } + + protected void quickEdit(String previousValue, OnValueEdited callback) { + quickEdit(previousValue, callback, false); + } + + protected void quickPasswordEdit(String previousValue, + OnValueEdited callback) { + quickEdit(previousValue, callback, true); + } + + @SuppressLint("InflateParams") + private void quickEdit(final String previousValue, + final OnValueEdited callback, boolean password) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + View view = (View) getLayoutInflater() + .inflate(R.layout.quickedit, null); + final EditText editor = (EditText) view.findViewById(R.id.editor); + OnClickListener mClickListener = new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + String value = editor.getText().toString(); + if (!previousValue.equals(value) && value.trim().length() > 0) { + callback.onValueEdited(value); + } + } + }; + if (password) { + editor.setInputType(InputType.TYPE_CLASS_TEXT + | InputType.TYPE_TEXT_VARIATION_PASSWORD); + editor.setHint(R.string.password); + builder.setPositiveButton(R.string.accept, mClickListener); + } else { + builder.setPositiveButton(R.string.edit, mClickListener); + } + editor.requestFocus(); + editor.setText(previousValue); + builder.setView(view); + builder.setNegativeButton(R.string.cancel, null); + builder.create().show(); + } + + public void selectPresence(final Conversation conversation, + final OnPresenceSelected listener) { + Contact contact = conversation.getContact(); + if (!contact.showInRoster()) { + showAddToRosterDialog(conversation); + } else { + Presences presences = contact.getPresences(); + if (presences.size() == 0) { + if (!contact.getOption(Contact.Options.TO) + && !contact.getOption(Contact.Options.ASKING) + && contact.getAccount().getStatus() == Account.STATUS_ONLINE) { + showAskForPresenceDialog(contact); + } else if (!contact.getOption(Contact.Options.TO) + || !contact.getOption(Contact.Options.FROM)) { + warnMutalPresenceSubscription(conversation, listener); + } else { + conversation.setNextPresence(null); + listener.onPresenceSelected(); + } + } else if (presences.size() == 1) { + String presence = (String) presences.asStringArray()[0]; + conversation.setNextPresence(presence); + listener.onPresenceSelected(); + } else { + final StringBuilder presence = new StringBuilder(); + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(getString(R.string.choose_presence)); + final String[] presencesArray = presences.asStringArray(); + int preselectedPresence = 0; + for (int i = 0; i < presencesArray.length; ++i) { + if (presencesArray[i].equals(contact.lastseen.presence)) { + preselectedPresence = i; + break; + } + } + presence.append(presencesArray[preselectedPresence]); + builder.setSingleChoiceItems(presencesArray, + preselectedPresence, + new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, + int which) { + presence.delete(0, presence.length()); + presence.append(presencesArray[which]); + } + }); + builder.setNegativeButton(R.string.cancel, null); + builder.setPositiveButton(R.string.ok, new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + conversation.setNextPresence(presence.toString()); + listener.onPresenceSelected(); + } + }); + builder.create().show(); + } + } + } + + protected void onActivityResult(int requestCode, int resultCode, + final Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == REQUEST_INVITE_TO_CONVERSATION + && resultCode == RESULT_OK) { + String contactJid = data.getStringExtra("contact"); + String conversationUuid = data.getStringExtra("conversation"); + Conversation conversation = xmppConnectionService + .findConversationByUuid(conversationUuid); + if (conversation.getMode() == Conversation.MODE_MULTI) { + xmppConnectionService.invite(conversation, contactJid); + } + Log.d(Config.LOGTAG, "inviting " + contactJid + " to " + + conversation.getName()); + } + } + + public int getSecondaryTextColor() { + return this.mSecondaryTextColor; + } + + public int getPrimaryTextColor() { + return this.mPrimaryTextColor; + } + + public int getWarningTextColor() { + return this.mColorRed; + } + + public int getPrimaryColor() { + return this.mPrimaryColor; + } + + public int getSecondaryBackgroundColor() { + return this.mSecondaryBackgroundColor; + } + + public int getPixel(int dp) { + DisplayMetrics metrics = getResources().getDisplayMetrics(); + return ((int) (dp * metrics.density)); + } + + public AvatarService avatarService() { + return xmppConnectionService.getAvatarService(); + } + + class BitmapWorkerTask extends AsyncTask { + private final WeakReference imageViewReference; + private Message message = null; + + public BitmapWorkerTask(ImageView imageView) { + imageViewReference = new WeakReference(imageView); + } + + @Override + protected Bitmap doInBackground(Message... params) { + message = params[0]; + try { + return xmppConnectionService.getFileBackend().getThumbnail( + message, (int) (metrics.density * 288), false); + } catch (FileNotFoundException e) { + return null; + } + } + + @Override + protected void onPostExecute(Bitmap bitmap) { + if (imageViewReference != null && bitmap != null) { + final ImageView imageView = imageViewReference.get(); + if (imageView != null) { + imageView.setImageBitmap(bitmap); + imageView.setBackgroundColor(0x00000000); + } + } + } + } + + public void loadBitmap(Message message, ImageView imageView) { + Bitmap bm; + try { + bm = xmppConnectionService.getFileBackend().getThumbnail(message, + (int) (metrics.density * 288), true); + } catch (FileNotFoundException e) { + bm = null; + } + if (bm != null) { + imageView.setImageBitmap(bm); + imageView.setBackgroundColor(0x00000000); + } else { + if (cancelPotentialWork(message, imageView)) { + imageView.setBackgroundColor(0xff333333); + final BitmapWorkerTask task = new BitmapWorkerTask(imageView); + final AsyncDrawable asyncDrawable = new AsyncDrawable( + getResources(), null, task); + imageView.setImageDrawable(asyncDrawable); + try { + task.execute(message); + } catch (RejectedExecutionException e) { + return; + } + } + } + } + + public static boolean cancelPotentialWork(Message message, + ImageView imageView) { + final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); + + if (bitmapWorkerTask != null) { + final Message oldMessage = bitmapWorkerTask.message; + if (oldMessage == null || message != oldMessage) { + bitmapWorkerTask.cancel(true); + } else { + return false; + } + } + return true; + } + + private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) { + if (imageView != null) { + final Drawable drawable = imageView.getDrawable(); + if (drawable instanceof AsyncDrawable) { + final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable; + return asyncDrawable.getBitmapWorkerTask(); + } + } + return null; + } + + static class AsyncDrawable extends BitmapDrawable { + private final WeakReference bitmapWorkerTaskReference; + + public AsyncDrawable(Resources res, Bitmap bitmap, + BitmapWorkerTask bitmapWorkerTask) { + super(res, bitmap); + bitmapWorkerTaskReference = new WeakReference( + bitmapWorkerTask); + } + + public BitmapWorkerTask getBitmapWorkerTask() { + return bitmapWorkerTaskReference.get(); + } + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java b/conversations/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java new file mode 100644 index 00000000..4ca21a3b --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java @@ -0,0 +1,102 @@ +package eu.siacs.conversations.ui.adapter; + +import java.util.List; + +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.ui.XmppActivity; +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.TextView; + +public class AccountAdapter extends ArrayAdapter { + + private XmppActivity activity; + + public AccountAdapter(XmppActivity activity, List objects) { + super(activity, 0, objects); + this.activity = activity; + } + + @Override + public View getView(int position, View view, ViewGroup parent) { + Account account = getItem(position); + if (view == null) { + LayoutInflater inflater = (LayoutInflater) getContext() + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + view = (View) inflater.inflate(R.layout.account_row, parent, false); + } + TextView jid = (TextView) view.findViewById(R.id.account_jid); + jid.setText(account.getJid()); + TextView statusView = (TextView) view.findViewById(R.id.account_status); + ImageView imageView = (ImageView) view.findViewById(R.id.account_image); + imageView.setImageBitmap(activity.avatarService().get(account, + activity.getPixel(48))); + switch (account.getStatus()) { + case Account.STATUS_DISABLED: + statusView.setText(getContext().getString( + R.string.account_status_disabled)); + statusView.setTextColor(activity.getSecondaryTextColor()); + break; + case Account.STATUS_ONLINE: + statusView.setText(getContext().getString( + R.string.account_status_online)); + statusView.setTextColor(activity.getPrimaryColor()); + break; + case Account.STATUS_CONNECTING: + statusView.setText(getContext().getString( + R.string.account_status_connecting)); + statusView.setTextColor(activity.getSecondaryTextColor()); + break; + case Account.STATUS_OFFLINE: + statusView.setText(getContext().getString( + R.string.account_status_offline)); + statusView.setTextColor(activity.getWarningTextColor()); + break; + case Account.STATUS_UNAUTHORIZED: + statusView.setText(getContext().getString( + R.string.account_status_unauthorized)); + statusView.setTextColor(activity.getWarningTextColor()); + break; + case Account.STATUS_SERVER_NOT_FOUND: + statusView.setText(getContext().getString( + R.string.account_status_not_found)); + statusView.setTextColor(activity.getWarningTextColor()); + break; + case Account.STATUS_NO_INTERNET: + statusView.setText(getContext().getString( + R.string.account_status_no_internet)); + statusView.setTextColor(activity.getWarningTextColor()); + break; + case Account.STATUS_REGISTRATION_FAILED: + statusView.setText(getContext().getString( + R.string.account_status_regis_fail)); + statusView.setTextColor(activity.getWarningTextColor()); + break; + case Account.STATUS_REGISTRATION_CONFLICT: + statusView.setText(getContext().getString( + R.string.account_status_regis_conflict)); + statusView.setTextColor(activity.getWarningTextColor()); + break; + case Account.STATUS_REGISTRATION_SUCCESSFULL: + statusView.setText(getContext().getString( + R.string.account_status_regis_success)); + statusView.setTextColor(activity.getSecondaryTextColor()); + break; + case Account.STATUS_REGISTRATION_NOT_SUPPORTED: + statusView.setText(getContext().getString( + R.string.account_status_regis_not_sup)); + statusView.setTextColor(activity.getWarningTextColor()); + break; + default: + statusView.setText(""); + break; + } + + return view; + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java b/conversations/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java new file mode 100644 index 00000000..183c89fa --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java @@ -0,0 +1,135 @@ +package eu.siacs.conversations.ui.adapter; + +import java.util.List; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Downloadable; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.ui.ConversationActivity; +import eu.siacs.conversations.ui.XmppActivity; +import eu.siacs.conversations.utils.UIHelper; +import android.content.Context; +import android.graphics.Color; +import android.graphics.Typeface; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.TextView; + +public class ConversationAdapter extends ArrayAdapter { + + private XmppActivity activity; + + public ConversationAdapter(XmppActivity activity, + List conversations) { + super(activity, 0, conversations); + this.activity = activity; + } + + @Override + public View getView(int position, View view, ViewGroup parent) { + if (view == null) { + LayoutInflater inflater = (LayoutInflater) activity + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + view = (View) inflater.inflate(R.layout.conversation_list_row, + parent, false); + } + Conversation conversation = getItem(position); + if (this.activity instanceof ConversationActivity) { + ConversationActivity activity = (ConversationActivity) this.activity; + if (!activity.isConversationsOverviewHideable()) { + if (conversation == activity.getSelectedConversation()) { + view.setBackgroundColor(activity + .getSecondaryBackgroundColor()); + } else { + view.setBackgroundColor(Color.TRANSPARENT); + } + } else { + view.setBackgroundColor(Color.TRANSPARENT); + } + } + TextView convName = (TextView) view + .findViewById(R.id.conversation_name); + if (conversation.getMode() == Conversation.MODE_SINGLE + || activity.useSubjectToIdentifyConference()) { + convName.setText(conversation.getName()); + } else { + convName.setText(conversation.getContactJid().split("/")[0]); + } + TextView mLastMessage = (TextView) view + .findViewById(R.id.conversation_lastmsg); + TextView mTimestamp = (TextView) view + .findViewById(R.id.conversation_lastupdate); + ImageView imagePreview = (ImageView) view + .findViewById(R.id.conversation_lastimage); + + Message message = conversation.getLatestMessage(); + + if (!conversation.isRead()) { + convName.setTypeface(null, Typeface.BOLD); + } else { + convName.setTypeface(null, Typeface.NORMAL); + } + + if (message.getType() == Message.TYPE_IMAGE + || message.getDownloadable() != null) { + Downloadable d = message.getDownloadable(); + if (d != null) { + mLastMessage.setVisibility(View.VISIBLE); + imagePreview.setVisibility(View.GONE); + if (conversation.isRead()) { + mLastMessage.setTypeface(null, Typeface.ITALIC); + } else { + mLastMessage.setTypeface(null, Typeface.BOLD_ITALIC); + } + if (d.getStatus() == Downloadable.STATUS_CHECKING) { + mLastMessage.setText(R.string.checking_image); + } else if (d.getStatus() == Downloadable.STATUS_DOWNLOADING) { + mLastMessage.setText(R.string.receiving_image); + } else if (d.getStatus() == Downloadable.STATUS_OFFER) { + mLastMessage.setText(R.string.image_offered_for_download); + } else if (d.getStatus() == Downloadable.STATUS_OFFER_CHECK_FILESIZE) { + mLastMessage.setText(R.string.image_offered_for_download); + } else if (d.getStatus() == Downloadable.STATUS_DELETED) { + mLastMessage.setText(R.string.image_file_deleted); + } else { + mLastMessage.setText(""); + } + } else { + mLastMessage.setVisibility(View.GONE); + imagePreview.setVisibility(View.VISIBLE); + activity.loadBitmap(message, imagePreview); + } + } else { + if ((message.getEncryption() != Message.ENCRYPTION_PGP) + && (message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED)) { + String body = Config.PARSE_EMOTICONS ? UIHelper + .transformAsciiEmoticons(message.getBody()) : message + .getBody(); + mLastMessage.setText(body); + } else { + mLastMessage.setText(R.string.encrypted_message_received); + } + if (!conversation.isRead()) { + mLastMessage.setTypeface(null, Typeface.BOLD); + } else { + mLastMessage.setTypeface(null, Typeface.NORMAL); + } + mLastMessage.setVisibility(View.VISIBLE); + imagePreview.setVisibility(View.GONE); + } + mTimestamp.setText(UIHelper.readableTimeDifference(getContext(), + conversation.getLatestMessage().getTimeSent())); + + ImageView profilePicture = (ImageView) view + .findViewById(R.id.conversation_image); + profilePicture.setImageBitmap(activity.avatarService().get( + conversation, activity.getPixel(56))); + + return view; + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java b/conversations/src/main/java/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java new file mode 100644 index 00000000..143dfda1 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java @@ -0,0 +1,74 @@ +package eu.siacs.conversations.ui.adapter; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import android.content.Context; +import android.widget.ArrayAdapter; +import android.widget.Filter; + +public class KnownHostsAdapter extends ArrayAdapter { + private ArrayList domains; + private Filter domainFilter = new Filter() { + + @Override + protected FilterResults performFiltering(CharSequence constraint) { + if (constraint != null) { + ArrayList suggestions = new ArrayList(); + final String[] split = constraint.toString().split("@"); + if (split.length == 1) { + for (String domain : domains) { + suggestions.add(split[0].toLowerCase(Locale + .getDefault()) + "@" + domain); + } + } else if (split.length == 2) { + for (String domain : domains) { + if (domain.contentEquals(split[1])) { + suggestions.clear(); + break; + } else if (domain.contains(split[1])) { + suggestions.add(split[0].toLowerCase(Locale + .getDefault()) + "@" + domain); + } + } + } else { + return new FilterResults(); + } + FilterResults filterResults = new FilterResults(); + filterResults.values = suggestions; + filterResults.count = suggestions.size(); + return filterResults; + } else { + return new FilterResults(); + } + } + + @Override + protected void publishResults(CharSequence constraint, + FilterResults results) { + ArrayList filteredList = (ArrayList) results.values; + if (results != null && results.count > 0) { + clear(); + for (Object c : filteredList) { + add((String) c); + } + notifyDataSetChanged(); + } + } + }; + + public KnownHostsAdapter(Context context, int viewResourceId, + List mKnownHosts) { + super(context, viewResourceId, mKnownHosts); + domains = new ArrayList(mKnownHosts.size()); + for (String domain : mKnownHosts) { + domains.add(new String(domain)); + } + } + + @Override + public Filter getFilter() { + return domainFilter; + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java b/conversations/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java new file mode 100644 index 00000000..977aa7b5 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java @@ -0,0 +1,44 @@ +package eu.siacs.conversations.ui.adapter; + +import java.util.List; + +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.ListItem; +import eu.siacs.conversations.ui.XmppActivity; +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.TextView; + +public class ListItemAdapter extends ArrayAdapter { + + protected XmppActivity activity; + + public ListItemAdapter(XmppActivity activity, List objects) { + super(activity, 0, objects); + this.activity = activity; + } + + @Override + public View getView(int position, View view, ViewGroup parent) { + LayoutInflater inflater = (LayoutInflater) getContext() + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + ListItem item = getItem(position); + if (view == null) { + view = (View) inflater.inflate(R.layout.contact, parent, false); + } + TextView name = (TextView) view.findViewById(R.id.contact_display_name); + TextView jid = (TextView) view.findViewById(R.id.contact_jid); + ImageView picture = (ImageView) view.findViewById(R.id.contact_photo); + + jid.setText(item.getJid()); + name.setText(item.getDisplayName()); + picture.setImageBitmap(activity.avatarService().get(item, + activity.getPixel(48))); + return view; + } + +} diff --git a/conversations/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/conversations/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java new file mode 100644 index 00000000..a9a55cbf --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -0,0 +1,560 @@ +package eu.siacs.conversations.ui.adapter; + +import java.util.List; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Downloadable; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.entities.Message.ImageParams; +import eu.siacs.conversations.ui.ConversationActivity; +import eu.siacs.conversations.utils.UIHelper; +import android.content.Intent; +import android.graphics.Typeface; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.style.ForegroundColorSpan; +import android.text.style.StyleSpan; +import android.util.DisplayMetrics; +import android.view.View; +import android.view.ViewGroup; +import android.view.View.OnClickListener; +import android.view.View.OnLongClickListener; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; + +public class MessageAdapter extends ArrayAdapter { + + private static final int SENT = 0; + private static final int RECEIVED = 1; + private static final int STATUS = 2; + private static final int NULL = 3; + + private ConversationActivity activity; + + private DisplayMetrics metrics; + + private OnContactPictureClicked mOnContactPictureClickedListener; + private OnContactPictureLongClicked mOnContactPictureLongClickedListener; + + public MessageAdapter(ConversationActivity activity, List messages) { + super(activity, 0, messages); + this.activity = activity; + metrics = getContext().getResources().getDisplayMetrics(); + } + + public void setOnContactPictureClicked(OnContactPictureClicked listener) { + this.mOnContactPictureClickedListener = listener; + } + + public void setOnContactPictureLongClicked( + OnContactPictureLongClicked listener) { + this.mOnContactPictureLongClickedListener = listener; + } + + @Override + public int getViewTypeCount() { + return 4; + } + + @Override + public int getItemViewType(int position) { + if (getItem(position).wasMergedIntoPrevious()) { + return NULL; + } else if (getItem(position).getType() == Message.TYPE_STATUS) { + return STATUS; + } else if (getItem(position).getStatus() <= Message.STATUS_RECEIVED) { + return RECEIVED; + } else { + return SENT; + } + } + + private void displayStatus(ViewHolder viewHolder, Message message) { + String filesize = null; + String info = null; + boolean error = false; + if (viewHolder.indicatorReceived != null) { + viewHolder.indicatorReceived.setVisibility(View.GONE); + } + boolean multiReceived = message.getConversation().getMode() == Conversation.MODE_MULTI + && message.getMergedStatus() <= Message.STATUS_RECEIVED; + if (message.getType() == Message.TYPE_IMAGE + || message.getDownloadable() != null) { + ImageParams params = message.getImageParams(); + if (params.size != 0) { + filesize = params.size / 1024 + " KB"; + } + } + switch (message.getMergedStatus()) { + case Message.STATUS_WAITING: + info = getContext().getString(R.string.waiting); + break; + case Message.STATUS_UNSEND: + info = getContext().getString(R.string.sending); + break; + case Message.STATUS_OFFERED: + info = getContext().getString(R.string.offering); + break; + case Message.STATUS_SEND_RECEIVED: + if (activity.indicateReceived()) { + viewHolder.indicatorReceived.setVisibility(View.VISIBLE); + } + break; + case Message.STATUS_SEND_DISPLAYED: + if (activity.indicateReceived()) { + viewHolder.indicatorReceived.setVisibility(View.VISIBLE); + } + break; + case Message.STATUS_SEND_FAILED: + info = getContext().getString(R.string.send_failed); + error = true; + break; + case Message.STATUS_SEND_REJECTED: + info = getContext().getString(R.string.send_rejected); + error = true; + break; + default: + if (multiReceived) { + Contact contact = message.getContact(); + if (contact != null) { + info = contact.getDisplayName(); + } else { + if (message.getPresence() != null) { + info = message.getPresence(); + } else { + info = message.getCounterpart(); + } + } + } + break; + } + if (error) { + viewHolder.time.setTextColor(activity.getWarningTextColor()); + } else { + viewHolder.time.setTextColor(activity.getSecondaryTextColor()); + } + if (message.getEncryption() == Message.ENCRYPTION_NONE) { + viewHolder.indicator.setVisibility(View.GONE); + } else { + viewHolder.indicator.setVisibility(View.VISIBLE); + } + + String formatedTime = UIHelper.readableTimeDifferenceFull(getContext(), + message.getMergedTimeSent()); + if (message.getStatus() <= Message.STATUS_RECEIVED) { + if ((filesize != null) && (info != null)) { + viewHolder.time.setText(filesize + " \u00B7 " + info); + } else if ((filesize == null) && (info != null)) { + viewHolder.time.setText(formatedTime + " \u00B7 " + info); + } else if ((filesize != null) && (info == null)) { + viewHolder.time.setText(formatedTime + " \u00B7 " + filesize); + } else { + viewHolder.time.setText(formatedTime); + } + } else { + if ((filesize != null) && (info != null)) { + viewHolder.time.setText(filesize + " \u00B7 " + info); + } else if ((filesize == null) && (info != null)) { + if (error) { + viewHolder.time.setText(info + " \u00B7 " + formatedTime); + } else { + viewHolder.time.setText(info); + } + } else if ((filesize != null) && (info == null)) { + viewHolder.time.setText(filesize + " \u00B7 " + formatedTime); + } else { + viewHolder.time.setText(formatedTime); + } + } + } + + private void displayInfoMessage(ViewHolder viewHolder, int r) { + if (viewHolder.download_button != null) { + viewHolder.download_button.setVisibility(View.GONE); + } + viewHolder.image.setVisibility(View.GONE); + viewHolder.messageBody.setVisibility(View.VISIBLE); + viewHolder.messageBody.setText(getContext().getString(r)); + viewHolder.messageBody.setTextColor(activity.getSecondaryTextColor()); + viewHolder.messageBody.setTypeface(null, Typeface.ITALIC); + viewHolder.messageBody.setTextIsSelectable(false); + } + + private void displayDecryptionFailed(ViewHolder viewHolder) { + if (viewHolder.download_button != null) { + viewHolder.download_button.setVisibility(View.GONE); + } + viewHolder.image.setVisibility(View.GONE); + viewHolder.messageBody.setVisibility(View.VISIBLE); + viewHolder.messageBody.setText(getContext().getString( + R.string.decryption_failed)); + viewHolder.messageBody.setTextColor(activity.getWarningTextColor()); + viewHolder.messageBody.setTypeface(null, Typeface.NORMAL); + viewHolder.messageBody.setTextIsSelectable(false); + } + + private void displayTextMessage(ViewHolder viewHolder, Message message) { + if (viewHolder.download_button != null) { + viewHolder.download_button.setVisibility(View.GONE); + } + viewHolder.image.setVisibility(View.GONE); + viewHolder.messageBody.setVisibility(View.VISIBLE); + if (message.getBody() != null) { + if (message.getType() != Message.TYPE_PRIVATE) { + String body = Config.PARSE_EMOTICONS ? UIHelper + .transformAsciiEmoticons(message.getMergedBody()) + : message.getMergedBody(); + viewHolder.messageBody.setText(body); + } else { + String privateMarker; + if (message.getStatus() <= Message.STATUS_RECEIVED) { + privateMarker = activity + .getString(R.string.private_message); + } else { + String to; + if (message.getPresence() != null) { + to = message.getPresence(); + } else { + to = message.getCounterpart(); + } + privateMarker = activity.getString( + R.string.private_message_to, to); + } + SpannableString span = new SpannableString(privateMarker + " " + + message.getBody()); + span.setSpan( + new ForegroundColorSpan(activity + .getSecondaryTextColor()), 0, privateMarker + .length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + span.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), 0, + privateMarker.length(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + viewHolder.messageBody.setText(span); + } + } else { + viewHolder.messageBody.setText(""); + } + viewHolder.messageBody.setTextColor(activity.getPrimaryTextColor()); + viewHolder.messageBody.setTypeface(null, Typeface.NORMAL); + viewHolder.messageBody.setTextIsSelectable(true); + } + + private void displayDownloadableMessage(ViewHolder viewHolder, + final Message message, int resid) { + viewHolder.image.setVisibility(View.GONE); + viewHolder.messageBody.setVisibility(View.GONE); + viewHolder.download_button.setVisibility(View.VISIBLE); + viewHolder.download_button.setText(resid); + viewHolder.download_button.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + startDonwloadable(message); + } + }); + } + + private void displayImageMessage(ViewHolder viewHolder, + final Message message) { + if (viewHolder.download_button != null) { + viewHolder.download_button.setVisibility(View.GONE); + } + viewHolder.messageBody.setVisibility(View.GONE); + viewHolder.image.setVisibility(View.VISIBLE); + ImageParams params = message.getImageParams(); + double target = metrics.density * 288; + int scalledW; + int scalledH; + if (params.width <= params.height) { + scalledW = (int) (params.width / ((double) params.height / target)); + scalledH = (int) target; + } else { + scalledW = (int) target; + scalledH = (int) (params.height / ((double) params.width / target)); + } + viewHolder.image.setLayoutParams(new LinearLayout.LayoutParams( + scalledW, scalledH)); + activity.loadBitmap(message, viewHolder.image); + viewHolder.image.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setDataAndType(activity.xmppConnectionService + .getFileBackend().getJingleFileUri(message), "image/*"); + getContext().startActivity(intent); + } + }); + viewHolder.image.setOnLongClickListener(new OnLongClickListener() { + + @Override + public boolean onLongClick(View v) { + Intent shareIntent = new Intent(); + shareIntent.setAction(Intent.ACTION_SEND); + shareIntent.putExtra(Intent.EXTRA_STREAM, + activity.xmppConnectionService.getFileBackend() + .getJingleFileUri(message)); + shareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + shareIntent.setType("image/webp"); + getContext().startActivity( + Intent.createChooser(shareIntent, + getContext().getText(R.string.share_with))); + return true; + } + }); + } + + @Override + public View getView(int position, View view, ViewGroup parent) { + final Message item = getItem(position); + int type = getItemViewType(position); + ViewHolder viewHolder; + if (view == null) { + viewHolder = new ViewHolder(); + switch (type) { + case NULL: + view = (View) activity.getLayoutInflater().inflate( + R.layout.message_null, parent, false); + break; + case SENT: + view = (View) activity.getLayoutInflater().inflate( + R.layout.message_sent, parent, false); + viewHolder.message_box = (LinearLayout) view + .findViewById(R.id.message_box); + viewHolder.contact_picture = (ImageView) view + .findViewById(R.id.message_photo); + viewHolder.contact_picture.setImageBitmap(activity + .avatarService().get( + item.getConversation().getAccount(), + activity.getPixel(48))); + viewHolder.download_button = (Button) view + .findViewById(R.id.download_button); + viewHolder.indicator = (ImageView) view + .findViewById(R.id.security_indicator); + viewHolder.image = (ImageView) view + .findViewById(R.id.message_image); + viewHolder.messageBody = (TextView) view + .findViewById(R.id.message_body); + viewHolder.time = (TextView) view + .findViewById(R.id.message_time); + viewHolder.indicatorReceived = (ImageView) view + .findViewById(R.id.indicator_received); + view.setTag(viewHolder); + break; + case RECEIVED: + view = (View) activity.getLayoutInflater().inflate( + R.layout.message_received, parent, false); + viewHolder.message_box = (LinearLayout) view + .findViewById(R.id.message_box); + viewHolder.contact_picture = (ImageView) view + .findViewById(R.id.message_photo); + viewHolder.download_button = (Button) view + .findViewById(R.id.download_button); + if (item.getConversation().getMode() == Conversation.MODE_SINGLE) { + viewHolder.contact_picture.setImageBitmap(activity + .avatarService().get(item.getContact(), + activity.getPixel(48))); + } + viewHolder.indicator = (ImageView) view + .findViewById(R.id.security_indicator); + viewHolder.image = (ImageView) view + .findViewById(R.id.message_image); + viewHolder.messageBody = (TextView) view + .findViewById(R.id.message_body); + viewHolder.time = (TextView) view + .findViewById(R.id.message_time); + view.setTag(viewHolder); + break; + case STATUS: + view = (View) activity.getLayoutInflater().inflate( + R.layout.message_status, parent, false); + viewHolder.contact_picture = (ImageView) view + .findViewById(R.id.message_photo); + if (item.getConversation().getMode() == Conversation.MODE_SINGLE) { + + viewHolder.contact_picture.setImageBitmap(activity + .avatarService().get( + item.getConversation().getContact(), + activity.getPixel(32))); + viewHolder.contact_picture.setAlpha(0.5f); + viewHolder.contact_picture + .setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + String name = item.getConversation() + .getName(); + String read = getContext() + .getString( + R.string.contact_has_read_up_to_this_point, + name); + Toast.makeText(getContext(), read, + Toast.LENGTH_SHORT).show(); + } + }); + + } + break; + default: + viewHolder = null; + break; + } + } else { + viewHolder = (ViewHolder) view.getTag(); + } + + if (type == STATUS) { + return view; + } + if (type == NULL) { + if (position == getCount() - 1) { + view.getLayoutParams().height = 1; + } else { + view.getLayoutParams().height = 0; + + } + view.setLayoutParams(view.getLayoutParams()); + return view; + } + + if (viewHolder.contact_picture != null) { + viewHolder.contact_picture + .setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + if (MessageAdapter.this.mOnContactPictureClickedListener != null) { + MessageAdapter.this.mOnContactPictureClickedListener + .onContactPictureClicked(item); + ; + } + + } + }); + viewHolder.contact_picture + .setOnLongClickListener(new OnLongClickListener() { + + @Override + public boolean onLongClick(View v) { + if (MessageAdapter.this.mOnContactPictureLongClickedListener != null) { + MessageAdapter.this.mOnContactPictureLongClickedListener + .onContactPictureLongClicked(item); + return true; + } else { + return false; + } + } + }); + } + + if (type == RECEIVED) { + if (item.getConversation().getMode() == Conversation.MODE_MULTI) { + Contact contact = item.getContact(); + if (contact != null) { + viewHolder.contact_picture.setImageBitmap(activity + .avatarService() + .get(contact, activity.getPixel(48))); + } else { + String name = item.getPresence(); + if (name == null) { + name = item.getCounterpart(); + } + viewHolder.contact_picture.setImageBitmap(activity + .avatarService().get(name, activity.getPixel(48))); + } + } + } + + if (item.getType() == Message.TYPE_IMAGE + || item.getDownloadable() != null) { + Downloadable d = item.getDownloadable(); + if (d != null && d.getStatus() == Downloadable.STATUS_DOWNLOADING) { + displayInfoMessage(viewHolder, R.string.receiving_image); + } else if (d != null + && d.getStatus() == Downloadable.STATUS_CHECKING) { + displayInfoMessage(viewHolder, R.string.checking_image); + } else if (d != null + && d.getStatus() == Downloadable.STATUS_DELETED) { + displayInfoMessage(viewHolder, R.string.image_file_deleted); + } else if (d != null && d.getStatus() == Downloadable.STATUS_OFFER) { + displayDownloadableMessage(viewHolder, item, + R.string.download_image); + } else if (d != null + && d.getStatus() == Downloadable.STATUS_OFFER_CHECK_FILESIZE) { + displayDownloadableMessage(viewHolder, item, + R.string.check_image_filesize); + } else if ((item.getEncryption() == Message.ENCRYPTION_DECRYPTED) + || (item.getEncryption() == Message.ENCRYPTION_NONE) + || (item.getEncryption() == Message.ENCRYPTION_OTR)) { + displayImageMessage(viewHolder, item); + } else if (item.getEncryption() == Message.ENCRYPTION_PGP) { + displayInfoMessage(viewHolder, R.string.encrypted_message); + } else { + displayDecryptionFailed(viewHolder); + } + } else { + if (item.getEncryption() == Message.ENCRYPTION_PGP) { + if (activity.hasPgp()) { + displayInfoMessage(viewHolder, R.string.encrypted_message); + } else { + displayInfoMessage(viewHolder, + R.string.install_openkeychain); + viewHolder.message_box + .setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + activity.showInstallPgpDialog(); + } + }); + } + } else if (item.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) { + displayDecryptionFailed(viewHolder); + } else { + displayTextMessage(viewHolder, item); + } + } + + displayStatus(viewHolder, item); + + return view; + } + + public void startDonwloadable(Message message) { + Downloadable downloadable = message.getDownloadable(); + if (downloadable != null) { + if (!downloadable.start()) { + Toast.makeText(activity, R.string.not_connected_try_again, + Toast.LENGTH_SHORT).show(); + } + } + } + + private static class ViewHolder { + + protected LinearLayout message_box; + protected Button download_button; + protected ImageView image; + protected ImageView indicator; + protected ImageView indicatorReceived; + protected TextView time; + protected TextView messageBody; + protected ImageView contact_picture; + + } + + public interface OnContactPictureClicked { + public void onContactPictureClicked(Message message); + } + + public interface OnContactPictureLongClicked { + public void onContactPictureLongClicked(Message message); + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java b/conversations/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java new file mode 100644 index 00000000..47595c6e --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java @@ -0,0 +1,112 @@ +package eu.siacs.conversations.utils; + +import java.math.BigInteger; +import java.nio.charset.Charset; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; + +import eu.siacs.conversations.entities.Account; +import android.util.Base64; + +public class CryptoHelper { + public static final String FILETRANSFER = "?FILETRANSFERv1:"; + final protected static char[] hexArray = "0123456789abcdef".toCharArray(); + final protected static char[] vowels = "aeiou".toCharArray(); + final protected static char[] consonants = "bcdfghjklmnpqrstvwxyz" + .toCharArray(); + + public static String bytesToHex(byte[] bytes) { + char[] hexChars = new char[bytes.length * 2]; + for (int j = 0; j < bytes.length; j++) { + int v = bytes[j] & 0xFF; + hexChars[j * 2] = hexArray[v >>> 4]; + hexChars[j * 2 + 1] = hexArray[v & 0x0F]; + } + return new String(hexChars); + } + + public static byte[] hexToBytes(String hexString) { + int len = hexString.length(); + byte[] array = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + array[i / 2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4) + Character + .digit(hexString.charAt(i + 1), 16)); + } + return array; + } + + public static String saslPlain(String username, String password) { + String sasl = '\u0000' + username + '\u0000' + password; + return Base64.encodeToString(sasl.getBytes(Charset.defaultCharset()), + Base64.NO_WRAP); + } + + private static byte[] concatenateByteArrays(byte[] a, byte[] b) { + byte[] result = new byte[a.length + b.length]; + System.arraycopy(a, 0, result, 0, a.length); + System.arraycopy(b, 0, result, a.length, b.length); + return result; + } + + public static String saslDigestMd5(Account account, String challenge, + SecureRandom random) { + try { + String[] challengeParts = new String(Base64.decode(challenge, + Base64.DEFAULT)).split(","); + String nonce = ""; + for (int i = 0; i < challengeParts.length; ++i) { + String[] parts = challengeParts[i].split("="); + if (parts[0].equals("nonce")) { + nonce = parts[1].replace("\"", ""); + } else if (parts[0].equals("rspauth")) { + return null; + } + } + String digestUri = "xmpp/" + account.getServer(); + String nonceCount = "00000001"; + String x = account.getUsername() + ":" + account.getServer() + ":" + + account.getPassword(); + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] y = md.digest(x.getBytes(Charset.defaultCharset())); + String cNonce = new BigInteger(100, random).toString(32); + byte[] a1 = concatenateByteArrays(y, + (":" + nonce + ":" + cNonce).getBytes(Charset + .defaultCharset())); + String a2 = "AUTHENTICATE:" + digestUri; + String ha1 = bytesToHex(md.digest(a1)); + String ha2 = bytesToHex(md.digest(a2.getBytes(Charset + .defaultCharset()))); + String kd = ha1 + ":" + nonce + ":" + nonceCount + ":" + cNonce + + ":auth:" + ha2; + String response = bytesToHex(md.digest(kd.getBytes(Charset + .defaultCharset()))); + String saslString = "username=\"" + account.getUsername() + + "\",realm=\"" + account.getServer() + "\",nonce=\"" + + nonce + "\",cnonce=\"" + cNonce + "\",nc=" + nonceCount + + ",qop=auth,digest-uri=\"" + digestUri + "\",response=" + + response + ",charset=utf-8"; + return Base64.encodeToString( + saslString.getBytes(Charset.defaultCharset()), + Base64.NO_WRAP); + } catch (NoSuchAlgorithmException e) { + return null; + } + } + + public static String randomMucName(SecureRandom random) { + return randomWord(3, random) + "." + randomWord(7, random); + } + + protected static String randomWord(int lenght, SecureRandom random) { + StringBuilder builder = new StringBuilder(lenght); + for (int i = 0; i < lenght; ++i) { + if (i % 2 == 0) { + builder.append(consonants[random.nextInt(consonants.length)]); + } else { + builder.append(vowels[random.nextInt(vowels.length)]); + } + } + return builder.toString(); + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/utils/DNSHelper.java b/conversations/src/main/java/eu/siacs/conversations/utils/DNSHelper.java new file mode 100644 index 00000000..c51a75ac --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/utils/DNSHelper.java @@ -0,0 +1,185 @@ +package eu.siacs.conversations.utils; + +import de.measite.minidns.Client; +import de.measite.minidns.DNSMessage; +import de.measite.minidns.Record; +import de.measite.minidns.Record.TYPE; +import de.measite.minidns.Record.CLASS; +import de.measite.minidns.record.SRV; +import de.measite.minidns.record.A; +import de.measite.minidns.record.AAAA; +import de.measite.minidns.record.Data; +import de.measite.minidns.util.NameUtil; +import eu.siacs.conversations.Config; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.SocketTimeoutException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Random; +import java.util.TreeMap; + +import android.os.Bundle; +import android.util.Log; + +public class DNSHelper { + protected static Client client = new Client(); + + public static Bundle getSRVRecord(String host) throws IOException { + String dns[] = client.findDNS(); + + if (dns != null) { + for (String dnsserver : dns) { + InetAddress ip = InetAddress.getByName(dnsserver); + Bundle b = queryDNS(host, ip); + if (b.containsKey("name")) { + return b; + } else if (b.containsKey("error") + && "nosrv".equals(b.getString("error", null))) { + return b; + } + } + } + return queryDNS(host, InetAddress.getByName("8.8.8.8")); + } + + public static Bundle queryDNS(String host, InetAddress dnsServer) { + Bundle namePort = new Bundle(); + try { + String qname = "_xmpp-client._tcp." + host; + Log.d(Config.LOGTAG, + "using dns server: " + dnsServer.getHostAddress() + + " to look up " + host); + DNSMessage message = client.query(qname, TYPE.SRV, CLASS.IN, + dnsServer.getHostAddress()); + + // How should we handle priorities and weight? + // Wikipedia has a nice article about priorities vs. weights: + // https://en.wikipedia.org/wiki/SRV_record#Provisioning_for_high_service_availability + + // we bucket the SRV records based on priority, pick per priority + // a random order respecting the weight, and dump that priority by + // priority + + TreeMap> priorities = new TreeMap>(); + TreeMap> ips4 = new TreeMap>(); + TreeMap> ips6 = new TreeMap>(); + + for (Record[] rrset : new Record[][] { message.getAnswers(), + message.getAdditionalResourceRecords() }) { + for (Record rr : rrset) { + Data d = rr.getPayload(); + if (d instanceof SRV + && NameUtil.idnEquals(qname, rr.getName())) { + SRV srv = (SRV) d; + if (!priorities.containsKey(srv.getPriority())) { + priorities.put(srv.getPriority(), + new ArrayList(2)); + } + priorities.get(srv.getPriority()).add(srv); + } + if (d instanceof A) { + A arecord = (A) d; + if (!ips4.containsKey(rr.getName())) { + ips4.put(rr.getName(), new ArrayList(3)); + } + ips4.get(rr.getName()).add(arecord.toString()); + } + if (d instanceof AAAA) { + AAAA aaaa = (AAAA) d; + if (!ips6.containsKey(rr.getName())) { + ips6.put(rr.getName(), new ArrayList(3)); + } + ips6.get(rr.getName()).add("[" + aaaa.toString() + "]"); + } + } + } + + Random rnd = new Random(); + ArrayList result = new ArrayList( + priorities.size() * 2 + 1); + for (ArrayList s : priorities.values()) { + + // trivial case + if (s.size() <= 1) { + result.addAll(s); + continue; + } + + long totalweight = 0l; + for (SRV srv : s) { + totalweight += srv.getWeight(); + } + + while (totalweight > 0l && s.size() > 0) { + long p = (rnd.nextLong() & 0x7fffffffffffffffl) + % totalweight; + int i = 0; + while (p > 0) { + p -= s.get(i++).getPriority(); + } + i--; + // remove is expensive, but we have only a few entries + // anyway + SRV srv = s.remove(i); + totalweight -= srv.getWeight(); + result.add(srv); + } + + Collections.shuffle(s, rnd); + result.addAll(s); + + } + + if (result.size() == 0) { + namePort.putString("error", "nosrv"); + return namePort; + } + // we now have a list of servers to try :-) + + // classic name/port pair + String resultName = result.get(0).getName(); + namePort.putString("name", resultName); + namePort.putInt("port", result.get(0).getPort()); + + if (ips4.containsKey(resultName)) { + // we have an ip! + ArrayList ip = ips4.get(resultName); + Collections.shuffle(ip, rnd); + namePort.putString("ipv4", ip.get(0)); + } + if (ips6.containsKey(resultName)) { + ArrayList ip = ips6.get(resultName); + Collections.shuffle(ip, rnd); + namePort.putString("ipv6", ip.get(0)); + } + + // add all other records + int i = 0; + for (SRV srv : result) { + namePort.putString("name" + i, srv.getName()); + namePort.putInt("port" + i, srv.getPort()); + i++; + } + + } catch (SocketTimeoutException e) { + namePort.putString("error", "timeout"); + } catch (Exception e) { + namePort.putString("error", "unhandled"); + } + return namePort; + } + + final protected static char[] hexArray = "0123456789ABCDEF".toCharArray(); + + public static String bytesToHex(byte[] bytes) { + char[] hexChars = new char[bytes.length * 2]; + for (int j = 0; j < bytes.length; j++) { + int v = bytes[j] & 0xFF; + hexChars[j * 2] = hexArray[v >>> 4]; + hexChars[j * 2 + 1] = hexArray[v & 0x0F]; + } + return new String(hexChars); + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java b/conversations/src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java new file mode 100644 index 00000000..88fa18ff --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java @@ -0,0 +1,44 @@ +package eu.siacs.conversations.utils; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.io.Writer; +import java.lang.Thread.UncaughtExceptionHandler; + +import android.content.Context; + +public class ExceptionHandler implements UncaughtExceptionHandler { + + private UncaughtExceptionHandler defaultHandler; + private Context context; + + public ExceptionHandler(Context context) { + this.context = context; + this.defaultHandler = Thread.getDefaultUncaughtExceptionHandler(); + } + + @Override + public void uncaughtException(Thread thread, Throwable ex) { + Writer result = new StringWriter(); + PrintWriter printWriter = new PrintWriter(result); + ex.printStackTrace(printWriter); + String stacktrace = result.toString(); + printWriter.close(); + try { + OutputStream os = context.openFileOutput("stacktrace.txt", + Context.MODE_PRIVATE); + os.write(stacktrace.getBytes()); + } catch (FileNotFoundException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + this.defaultHandler.uncaughtException(thread, ex); + } + +} diff --git a/conversations/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java b/conversations/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java new file mode 100644 index 00000000..b5fc88bd --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java @@ -0,0 +1,117 @@ +package eu.siacs.conversations.utils; + +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.List; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.services.XmppConnectionService; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.SharedPreferences; +import android.content.DialogInterface.OnClickListener; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.preference.PreferenceManager; +import android.text.format.DateUtils; +import android.util.Log; + +public class ExceptionHelper { + public static void init(Context context) { + if (!(Thread.getDefaultUncaughtExceptionHandler() instanceof ExceptionHandler)) { + Thread.setDefaultUncaughtExceptionHandler(new ExceptionHandler( + context)); + } + } + + public static void checkForCrash(Context context, + final XmppConnectionService service) { + try { + final SharedPreferences preferences = PreferenceManager + .getDefaultSharedPreferences(context); + boolean neverSend = preferences.getBoolean("never_send", false); + if (neverSend) { + return; + } + List accounts = service.getAccounts(); + Account account = null; + for (int i = 0; i < accounts.size(); ++i) { + if (!accounts.get(i).isOptionSet(Account.OPTION_DISABLED)) { + account = accounts.get(i); + break; + } + } + if (account == null) { + return; + } + final Account finalAccount = account; + FileInputStream file = context.openFileInput("stacktrace.txt"); + InputStreamReader inputStreamReader = new InputStreamReader(file); + BufferedReader stacktrace = new BufferedReader(inputStreamReader); + final StringBuilder report = new StringBuilder(); + PackageManager pm = context.getPackageManager(); + PackageInfo packageInfo = null; + try { + packageInfo = pm.getPackageInfo(context.getPackageName(), 0); + report.append("Version: " + packageInfo.versionName + '\n'); + report.append("Last Update: " + + DateUtils.formatDateTime(context, + packageInfo.lastUpdateTime, + DateUtils.FORMAT_SHOW_TIME + | DateUtils.FORMAT_SHOW_DATE) + '\n'); + } catch (NameNotFoundException e) { + } + String line; + while ((line = stacktrace.readLine()) != null) { + report.append(line); + report.append('\n'); + } + file.close(); + context.deleteFile("stacktrace.txt"); + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(context.getString(R.string.crash_report_title)); + builder.setMessage(context.getText(R.string.crash_report_message)); + builder.setPositiveButton(context.getText(R.string.send_now), + new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + + Log.d(Config.LOGTAG, "using account=" + + finalAccount.getJid() + + " to send in stack trace"); + Conversation conversation = service + .findOrCreateConversation(finalAccount, + "bugs@siacs.eu", false); + Message message = new Message(conversation, report + .toString(), Message.ENCRYPTION_NONE); + service.sendMessage(message); + } + }); + builder.setNegativeButton(context.getText(R.string.send_never), + new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + preferences.edit().putBoolean("never_send", true) + .commit(); + } + }); + builder.create().show(); + } catch (FileNotFoundException e) { + return; + } catch (IOException e) { + return; + } + + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/utils/OnPhoneContactsLoadedListener.java b/conversations/src/main/java/eu/siacs/conversations/utils/OnPhoneContactsLoadedListener.java new file mode 100644 index 00000000..9a689768 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/utils/OnPhoneContactsLoadedListener.java @@ -0,0 +1,9 @@ +package eu.siacs.conversations.utils; + +import java.util.List; + +import android.os.Bundle; + +public interface OnPhoneContactsLoadedListener { + public void onPhoneContactsLoaded(List phoneContacts); +} diff --git a/conversations/src/main/java/eu/siacs/conversations/utils/PRNGFixes.java b/conversations/src/main/java/eu/siacs/conversations/utils/PRNGFixes.java new file mode 100644 index 00000000..8fe67234 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/utils/PRNGFixes.java @@ -0,0 +1,327 @@ +package eu.siacs.conversations.utils; + +import android.os.Build; +import android.os.Process; +import android.util.Log; + +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.security.NoSuchAlgorithmException; +import java.security.Provider; +import java.security.SecureRandom; +import java.security.SecureRandomSpi; +import java.security.Security; + +/** + * Fixes for the output of the default PRNG having low entropy. + * + * The fixes need to be applied via {@link #apply()} before any use of Java + * Cryptography Architecture primitives. A good place to invoke them is in the + * application's {@code onCreate}. + */ +public final class PRNGFixes { + + private static final int VERSION_CODE_JELLY_BEAN = 16; + private static final int VERSION_CODE_JELLY_BEAN_MR2 = 18; + private static final byte[] BUILD_FINGERPRINT_AND_DEVICE_SERIAL = getBuildFingerprintAndDeviceSerial(); + + /** Hidden constructor to prevent instantiation. */ + private PRNGFixes() { + } + + /** + * Applies all fixes. + * + * @throws SecurityException + * if a fix is needed but could not be applied. + */ + public static void apply() { + applyOpenSSLFix(); + installLinuxPRNGSecureRandom(); + } + + /** + * Applies the fix for OpenSSL PRNG having low entropy. Does nothing if the + * fix is not needed. + * + * @throws SecurityException + * if the fix is needed but could not be applied. + */ + private static void applyOpenSSLFix() throws SecurityException { + if ((Build.VERSION.SDK_INT < VERSION_CODE_JELLY_BEAN) + || (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2)) { + // No need to apply the fix + return; + } + + try { + // Mix in the device- and invocation-specific seed. + Class.forName("org.apache.harmony.xnet.provider.jsse.NativeCrypto") + .getMethod("RAND_seed", byte[].class) + .invoke(null, generateSeed()); + + // Mix output of Linux PRNG into OpenSSL's PRNG + int bytesRead = (Integer) Class + .forName( + "org.apache.harmony.xnet.provider.jsse.NativeCrypto") + .getMethod("RAND_load_file", String.class, long.class) + .invoke(null, "/dev/urandom", 1024); + if (bytesRead != 1024) { + throw new IOException( + "Unexpected number of bytes read from Linux PRNG: " + + bytesRead); + } + } catch (Exception e) { + throw new SecurityException("Failed to seed OpenSSL PRNG", e); + } + } + + /** + * Installs a Linux PRNG-backed {@code SecureRandom} implementation as the + * default. Does nothing if the implementation is already the default or if + * there is not need to install the implementation. + * + * @throws SecurityException + * if the fix is needed but could not be applied. + */ + private static void installLinuxPRNGSecureRandom() throws SecurityException { + if (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2) { + // No need to apply the fix + return; + } + + // Install a Linux PRNG-based SecureRandom implementation as the + // default, if not yet installed. + Provider[] secureRandomProviders = Security + .getProviders("SecureRandom.SHA1PRNG"); + if ((secureRandomProviders == null) + || (secureRandomProviders.length < 1) + || (!LinuxPRNGSecureRandomProvider.class + .equals(secureRandomProviders[0].getClass()))) { + Security.insertProviderAt(new LinuxPRNGSecureRandomProvider(), 1); + } + + // Assert that new SecureRandom() and + // SecureRandom.getInstance("SHA1PRNG") return a SecureRandom backed + // by the Linux PRNG-based SecureRandom implementation. + SecureRandom rng1 = new SecureRandom(); + if (!LinuxPRNGSecureRandomProvider.class.equals(rng1.getProvider() + .getClass())) { + throw new SecurityException( + "new SecureRandom() backed by wrong Provider: " + + rng1.getProvider().getClass()); + } + + SecureRandom rng2; + try { + rng2 = SecureRandom.getInstance("SHA1PRNG"); + } catch (NoSuchAlgorithmException e) { + throw new SecurityException("SHA1PRNG not available", e); + } + if (!LinuxPRNGSecureRandomProvider.class.equals(rng2.getProvider() + .getClass())) { + throw new SecurityException( + "SecureRandom.getInstance(\"SHA1PRNG\") backed by wrong" + + " Provider: " + rng2.getProvider().getClass()); + } + } + + /** + * {@code Provider} of {@code SecureRandom} engines which pass through all + * requests to the Linux PRNG. + */ + private static class LinuxPRNGSecureRandomProvider extends Provider { + + public LinuxPRNGSecureRandomProvider() { + super("LinuxPRNG", 1.0, + "A Linux-specific random number provider that uses" + + " /dev/urandom"); + // Although /dev/urandom is not a SHA-1 PRNG, some apps + // explicitly request a SHA1PRNG SecureRandom and we thus need to + // prevent them from getting the default implementation whose output + // may have low entropy. + put("SecureRandom.SHA1PRNG", LinuxPRNGSecureRandom.class.getName()); + put("SecureRandom.SHA1PRNG ImplementedIn", "Software"); + } + } + + /** + * {@link SecureRandomSpi} which passes all requests to the Linux PRNG ( + * {@code /dev/urandom}). + */ + public static class LinuxPRNGSecureRandom extends SecureRandomSpi { + + /* + * IMPLEMENTATION NOTE: Requests to generate bytes and to mix in a seed + * are passed through to the Linux PRNG (/dev/urandom). Instances of + * this class seed themselves by mixing in the current time, PID, UID, + * build fingerprint, and hardware serial number (where available) into + * Linux PRNG. + * + * Concurrency: Read requests to the underlying Linux PRNG are + * serialized (on sLock) to ensure that multiple threads do not get + * duplicated PRNG output. + */ + + private static final File URANDOM_FILE = new File("/dev/urandom"); + + private static final Object sLock = new Object(); + + /** + * Input stream for reading from Linux PRNG or {@code null} if not yet + * opened. + * + * @GuardedBy("sLock") + */ + private static DataInputStream sUrandomIn; + + /** + * Output stream for writing to Linux PRNG or {@code null} if not yet + * opened. + * + * @GuardedBy("sLock") + */ + private static OutputStream sUrandomOut; + + /** + * Whether this engine instance has been seeded. This is needed because + * each instance needs to seed itself if the client does not explicitly + * seed it. + */ + private boolean mSeeded; + + @Override + protected void engineSetSeed(byte[] bytes) { + try { + OutputStream out; + synchronized (sLock) { + out = getUrandomOutputStream(); + } + out.write(bytes); + out.flush(); + } catch (IOException e) { + // On a small fraction of devices /dev/urandom is not writable. + // Log and ignore. + Log.w(PRNGFixes.class.getSimpleName(), + "Failed to mix seed into " + URANDOM_FILE); + } finally { + mSeeded = true; + } + } + + @Override + protected void engineNextBytes(byte[] bytes) { + if (!mSeeded) { + // Mix in the device- and invocation-specific seed. + engineSetSeed(generateSeed()); + } + + try { + DataInputStream in; + synchronized (sLock) { + in = getUrandomInputStream(); + } + synchronized (in) { + in.readFully(bytes); + } + } catch (IOException e) { + throw new SecurityException("Failed to read from " + + URANDOM_FILE, e); + } + } + + @Override + protected byte[] engineGenerateSeed(int size) { + byte[] seed = new byte[size]; + engineNextBytes(seed); + return seed; + } + + private DataInputStream getUrandomInputStream() { + synchronized (sLock) { + if (sUrandomIn == null) { + // NOTE: Consider inserting a BufferedInputStream between + // DataInputStream and FileInputStream if you need higher + // PRNG output performance and can live with future PRNG + // output being pulled into this process prematurely. + try { + sUrandomIn = new DataInputStream(new FileInputStream( + URANDOM_FILE)); + } catch (IOException e) { + throw new SecurityException("Failed to open " + + URANDOM_FILE + " for reading", e); + } + } + return sUrandomIn; + } + } + + private OutputStream getUrandomOutputStream() throws IOException { + synchronized (sLock) { + if (sUrandomOut == null) { + sUrandomOut = new FileOutputStream(URANDOM_FILE); + } + return sUrandomOut; + } + } + } + + /** + * Generates a device- and invocation-specific seed to be mixed into the + * Linux PRNG. + */ + private static byte[] generateSeed() { + try { + ByteArrayOutputStream seedBuffer = new ByteArrayOutputStream(); + DataOutputStream seedBufferOut = new DataOutputStream(seedBuffer); + seedBufferOut.writeLong(System.currentTimeMillis()); + seedBufferOut.writeLong(System.nanoTime()); + seedBufferOut.writeInt(Process.myPid()); + seedBufferOut.writeInt(Process.myUid()); + seedBufferOut.write(BUILD_FINGERPRINT_AND_DEVICE_SERIAL); + seedBufferOut.close(); + return seedBuffer.toByteArray(); + } catch (IOException e) { + throw new SecurityException("Failed to generate seed", e); + } + } + + /** + * Gets the hardware serial number of this device. + * + * @return serial number or {@code null} if not available. + */ + private static String getDeviceSerialNumber() { + // We're using the Reflection API because Build.SERIAL is only available + // since API Level 9 (Gingerbread, Android 2.3). + try { + return (String) Build.class.getField("SERIAL").get(null); + } catch (Exception ignored) { + return null; + } + } + + private static byte[] getBuildFingerprintAndDeviceSerial() { + StringBuilder result = new StringBuilder(); + String fingerprint = Build.FINGERPRINT; + if (fingerprint != null) { + result.append(fingerprint); + } + String serial = getDeviceSerialNumber(); + if (serial != null) { + result.append(serial); + } + try { + return result.toString().getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("UTF-8 encoding not supported"); + } + } +} \ No newline at end of file diff --git a/conversations/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java b/conversations/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java new file mode 100644 index 00000000..5becc7e7 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java @@ -0,0 +1,95 @@ +package eu.siacs.conversations.utils; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.RejectedExecutionException; + +import android.content.Context; +import android.content.CursorLoader; +import android.content.Loader; +import android.content.Loader.OnLoadCompleteListener; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.provider.ContactsContract; +import android.provider.ContactsContract.Profile; + +public class PhoneHelper { + + public static void loadPhoneContacts(Context context, + final OnPhoneContactsLoadedListener listener) { + final List phoneContacts = new ArrayList(); + + final String[] PROJECTION = new String[] { ContactsContract.Data._ID, + ContactsContract.Data.DISPLAY_NAME, + ContactsContract.Data.PHOTO_URI, + ContactsContract.Data.LOOKUP_KEY, + ContactsContract.CommonDataKinds.Im.DATA }; + + final String SELECTION = "(" + ContactsContract.Data.MIMETYPE + "=\"" + + ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE + + "\") AND (" + ContactsContract.CommonDataKinds.Im.PROTOCOL + + "=\"" + ContactsContract.CommonDataKinds.Im.PROTOCOL_JABBER + + "\")"; + + CursorLoader mCursorLoader = new CursorLoader(context, + ContactsContract.Data.CONTENT_URI, PROJECTION, SELECTION, null, + null); + mCursorLoader.registerListener(0, new OnLoadCompleteListener() { + + @Override + public void onLoadComplete(Loader arg0, Cursor cursor) { + if (cursor == null) { + return; + } + while (cursor.moveToNext()) { + Bundle contact = new Bundle(); + contact.putInt("phoneid", cursor.getInt(cursor + .getColumnIndex(ContactsContract.Data._ID))); + contact.putString( + "displayname", + cursor.getString(cursor + .getColumnIndex(ContactsContract.Data.DISPLAY_NAME))); + contact.putString("photouri", cursor.getString(cursor + .getColumnIndex(ContactsContract.Data.PHOTO_URI))); + contact.putString("lookup", cursor.getString(cursor + .getColumnIndex(ContactsContract.Data.LOOKUP_KEY))); + + contact.putString( + "jid", + cursor.getString(cursor + .getColumnIndex(ContactsContract.CommonDataKinds.Im.DATA))); + phoneContacts.add(contact); + } + if (listener != null) { + listener.onPhoneContactsLoaded(phoneContacts); + } + } + }); + try { + mCursorLoader.startLoading(); + } catch (RejectedExecutionException e) { + if (listener != null) { + listener.onPhoneContactsLoaded(phoneContacts); + } + } + } + + public static Uri getSefliUri(Context context) { + String[] mProjection = new String[] { Profile._ID, Profile.PHOTO_URI }; + Cursor mProfileCursor = context.getContentResolver().query( + Profile.CONTENT_URI, mProjection, null, null, null); + + if (mProfileCursor == null || mProfileCursor.getCount() == 0) { + return null; + } else { + mProfileCursor.moveToFirst(); + String uri = mProfileCursor.getString(1); + if (uri == null) { + return null; + } else { + return Uri.parse(uri); + } + } + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/utils/UIHelper.java b/conversations/src/main/java/eu/siacs/conversations/utils/UIHelper.java new file mode 100644 index 00000000..5141c83c --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/utils/UIHelper.java @@ -0,0 +1,225 @@ +package eu.siacs.conversations.utils; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.regex.Pattern; + +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.ui.ConversationActivity; +import eu.siacs.conversations.ui.ManageAccountActivity; +import android.annotation.SuppressLint; +import android.app.AlertDialog; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.Intent; +import android.support.v4.app.NotificationCompat; +import android.support.v4.app.TaskStackBuilder; +import android.text.format.DateFormat; +import android.text.format.DateUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.TextView; + +public class UIHelper { + private static final int SHORT_DATE_FLAGS = DateUtils.FORMAT_SHOW_DATE + | DateUtils.FORMAT_NO_YEAR | DateUtils.FORMAT_ABBREV_ALL; + private static final int FULL_DATE_FLAGS = DateUtils.FORMAT_SHOW_TIME + | DateUtils.FORMAT_ABBREV_ALL | DateUtils.FORMAT_SHOW_DATE; + + public static String readableTimeDifference(Context context, long time) { + return readableTimeDifference(context, time, false); + } + + public static String readableTimeDifferenceFull(Context context, long time) { + return readableTimeDifference(context, time, true); + } + + private static String readableTimeDifference(Context context, long time, + boolean fullDate) { + if (time == 0) { + return context.getString(R.string.just_now); + } + Date date = new Date(time); + long difference = (System.currentTimeMillis() - time) / 1000; + if (difference < 60) { + return context.getString(R.string.just_now); + } else if (difference < 60 * 2) { + return context.getString(R.string.minute_ago); + } else if (difference < 60 * 15) { + return context.getString(R.string.minutes_ago, + Math.round(difference / 60.0)); + } else if (today(date)) { + java.text.DateFormat df = DateFormat.getTimeFormat(context); + return df.format(date); + } else { + if (fullDate) { + return DateUtils.formatDateTime(context, date.getTime(), + FULL_DATE_FLAGS); + } else { + return DateUtils.formatDateTime(context, date.getTime(), + SHORT_DATE_FLAGS); + } + } + } + + private static boolean today(Date date) { + Calendar cal1 = Calendar.getInstance(); + Calendar cal2 = Calendar.getInstance(); + cal1.setTime(date); + cal2.setTimeInMillis(System.currentTimeMillis()); + return cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) + && cal1.get(Calendar.DAY_OF_YEAR) == cal2 + .get(Calendar.DAY_OF_YEAR); + } + + public static String lastseen(Context context, long time) { + if (time == 0) { + return context.getString(R.string.never_seen); + } + long difference = (System.currentTimeMillis() - time) / 1000; + if (difference < 60) { + return context.getString(R.string.last_seen_now); + } else if (difference < 60 * 2) { + return context.getString(R.string.last_seen_min); + } else if (difference < 60 * 60) { + return context.getString(R.string.last_seen_mins, + Math.round(difference / 60.0)); + } else if (difference < 60 * 60 * 2) { + return context.getString(R.string.last_seen_hour); + } else if (difference < 60 * 60 * 24) { + return context.getString(R.string.last_seen_hours, + Math.round(difference / (60.0 * 60.0))); + } else if (difference < 60 * 60 * 48) { + return context.getString(R.string.last_seen_day); + } else { + return context.getString(R.string.last_seen_days, + Math.round(difference / (60.0 * 60.0 * 24.0))); + } + } + + public static void showErrorNotification(Context context, + List accounts) { + NotificationManager mNotificationManager = (NotificationManager) context + .getSystemService(Context.NOTIFICATION_SERVICE); + List accountsWproblems = new ArrayList(); + for (Account account : accounts) { + if (account.hasErrorStatus()) { + accountsWproblems.add(account); + } + } + NotificationCompat.Builder mBuilder = new NotificationCompat.Builder( + context); + if (accountsWproblems.size() == 0) { + mNotificationManager.cancel(1111); + return; + } else if (accountsWproblems.size() == 1) { + mBuilder.setContentTitle(context + .getString(R.string.problem_connecting_to_account)); + mBuilder.setContentText(accountsWproblems.get(0).getJid()); + } else { + mBuilder.setContentTitle(context + .getString(R.string.problem_connecting_to_accounts)); + mBuilder.setContentText(context.getString(R.string.touch_to_fix)); + } + mBuilder.setOngoing(true); + mBuilder.setLights(0xffffffff, 2000, 4000); + mBuilder.setSmallIcon(R.drawable.ic_notification); + TaskStackBuilder stackBuilder = TaskStackBuilder.create(context); + stackBuilder.addParentStack(ConversationActivity.class); + + Intent manageAccountsIntent = new Intent(context, + ManageAccountActivity.class); + stackBuilder.addNextIntent(manageAccountsIntent); + + PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0, + PendingIntent.FLAG_UPDATE_CURRENT); + + mBuilder.setContentIntent(resultPendingIntent); + Notification notification = mBuilder.build(); + mNotificationManager.notify(1111, notification); + } + + @SuppressLint("InflateParams") + public static AlertDialog getVerifyFingerprintDialog( + final ConversationActivity activity, + final Conversation conversation, final View msg) { + final Contact contact = conversation.getContact(); + final Account account = conversation.getAccount(); + + AlertDialog.Builder builder = new AlertDialog.Builder(activity); + builder.setTitle("Verify fingerprint"); + LayoutInflater inflater = activity.getLayoutInflater(); + View view = inflater.inflate(R.layout.dialog_verify_otr, null); + TextView jid = (TextView) view.findViewById(R.id.verify_otr_jid); + TextView fingerprint = (TextView) view + .findViewById(R.id.verify_otr_fingerprint); + TextView yourprint = (TextView) view + .findViewById(R.id.verify_otr_yourprint); + + jid.setText(contact.getJid()); + fingerprint.setText(conversation.getOtrFingerprint()); + yourprint.setText(account.getOtrFingerprint()); + builder.setNegativeButton("Cancel", null); + builder.setPositiveButton("Verify", new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + contact.addOtrFingerprint(conversation.getOtrFingerprint()); + msg.setVisibility(View.GONE); + activity.xmppConnectionService.syncRosterToDisk(account); + } + }); + builder.setView(view); + return builder.create(); + } + + private final static class EmoticonPattern { + Pattern pattern; + String replacement; + + EmoticonPattern(String ascii, int unicode) { + this.pattern = Pattern.compile("(?<=(^|\\s))" + ascii + + "(?=(\\s|$))"); + this.replacement = new String(new int[] { unicode, }, 0, 1); + } + + String replaceAll(String body) { + return pattern.matcher(body).replaceAll(replacement); + } + } + + private static final EmoticonPattern[] patterns = new EmoticonPattern[] { + new EmoticonPattern(":-?D", 0x1f600), + new EmoticonPattern("\\^\\^", 0x1f601), + new EmoticonPattern(":'D", 0x1f602), + new EmoticonPattern("\\]-?D", 0x1f608), + new EmoticonPattern(";-?\\)", 0x1f609), + new EmoticonPattern(":-?\\)", 0x1f60a), + new EmoticonPattern("[B8]-?\\)", 0x1f60e), + new EmoticonPattern(":-?\\|", 0x1f610), + new EmoticonPattern(":-?[/\\\\]", 0x1f615), + new EmoticonPattern(":-?\\*", 0x1f617), + new EmoticonPattern(":-?[Ppb]", 0x1f61b), + new EmoticonPattern(":-?\\(", 0x1f61e), + new EmoticonPattern(":-?[0Oo]", 0x1f62e), + new EmoticonPattern("\\\\o/", 0x1F631), }; + + public static String transformAsciiEmoticons(String body) { + if (body != null) { + for (EmoticonPattern p : patterns) { + body = p.replaceAll(body); + } + body = body.trim(); + } + return body; + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/utils/Validator.java b/conversations/src/main/java/eu/siacs/conversations/utils/Validator.java new file mode 100644 index 00000000..00130fa2 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/utils/Validator.java @@ -0,0 +1,14 @@ +package eu.siacs.conversations.utils; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class Validator { + public static final Pattern VALID_JID = Pattern.compile( + "^[^@/<>'\"\\s]+@[^@/<>'\"\\s]+$", Pattern.CASE_INSENSITIVE); + + public static boolean isValidJid(String jid) { + Matcher matcher = VALID_JID.matcher(jid); + return matcher.find(); + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/utils/XmlHelper.java b/conversations/src/main/java/eu/siacs/conversations/utils/XmlHelper.java new file mode 100644 index 00000000..4dee07cf --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/utils/XmlHelper.java @@ -0,0 +1,12 @@ +package eu.siacs.conversations.utils; + +public class XmlHelper { + public static String encodeEntities(String content) { + content = content.replace("&", "&"); + content = content.replace("<", "<"); + content = content.replace(">", ">"); + content = content.replace("\"", """); + content = content.replace("'", "'"); + return content; + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/utils/zlib/ZLibInputStream.java b/conversations/src/main/java/eu/siacs/conversations/utils/zlib/ZLibInputStream.java new file mode 100644 index 00000000..b777c10c --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/utils/zlib/ZLibInputStream.java @@ -0,0 +1,54 @@ +package eu.siacs.conversations.utils.zlib; + +import java.io.IOException; +import java.io.InputStream; +import java.util.zip.Inflater; +import java.util.zip.InflaterInputStream; + +/** + * ZLibInputStream is a zlib and input stream compatible version of an + * InflaterInputStream. This class solves the incompatibility between + * {@link InputStream#available()} and {@link InflaterInputStream#available()}. + */ +public class ZLibInputStream extends InflaterInputStream { + + /** + * Construct a ZLibInputStream, reading data from the underlying stream. + * + * @param is + * The {@code InputStream} to read data from. + * @throws IOException + * If an {@code IOException} occurs. + */ + public ZLibInputStream(InputStream is) throws IOException { + super(is, new Inflater(), 512); + } + + /** + * Provide a more InputStream compatible version of available. A return + * value of 1 means that it is likly to read one byte without blocking, 0 + * means that the system is known to block for more input. + * + * @return 0 if no data is available, 1 otherwise + * @throws IOException + */ + @Override + public int available() throws IOException { + /* + * This is one of the funny code blocks. InflaterInputStream.available + * violates the contract of InputStream.available, which breaks kXML2. + * + * I'm not sure who's to blame, oracle/sun for a broken api or the + * google guys for mixing a sun bug with a xml reader that can't handle + * it.... + * + * Anyway, this simple if breaks suns distorted reality, but helps to + * use the api as intended. + */ + if (inf.needsInput()) { + return 0; + } + return super.available(); + } + +} diff --git a/conversations/src/main/java/eu/siacs/conversations/utils/zlib/ZLibOutputStream.java b/conversations/src/main/java/eu/siacs/conversations/utils/zlib/ZLibOutputStream.java new file mode 100644 index 00000000..8b3f5e68 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/utils/zlib/ZLibOutputStream.java @@ -0,0 +1,95 @@ +package eu.siacs.conversations.utils.zlib; + +import java.io.IOException; +import java.io.OutputStream; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.security.NoSuchAlgorithmException; +import java.util.zip.Deflater; +import java.util.zip.DeflaterOutputStream; + +/** + *

+ * Android 2.2 includes Java7 FLUSH_SYNC option, which will be used by this + * Implementation, preferable via reflection. The @hide was remove in API level + * 19. This class might thus go away in the future. + *

+ *

+ * Please use {@link ZLibOutputStream#SUPPORTED} to check for flush + * compatibility. + *

+ */ +public class ZLibOutputStream extends DeflaterOutputStream { + + /** + * The reflection based flush method. + */ + + private final static Method method; + /** + * SUPPORTED is true if a flush compatible method exists. + */ + public final static boolean SUPPORTED; + + /** + * Static block to initialize {@link #SUPPORTED} and {@link #method}. + */ + static { + Method m = null; + try { + m = Deflater.class.getMethod("deflate", byte[].class, int.class, + int.class, int.class); + } catch (SecurityException e) { + } catch (NoSuchMethodException e) { + } + method = m; + SUPPORTED = (method != null); + } + + /** + * Create a new ZLib compatible output stream wrapping the given low level + * stream. ZLib compatiblity means we will send a zlib header. + * + * @param os + * OutputStream The underlying stream. + * @throws IOException + * In case of a lowlevel transfer problem. + * @throws NoSuchAlgorithmException + * In case of a {@link Deflater} error. + */ + public ZLibOutputStream(OutputStream os) throws IOException, + NoSuchAlgorithmException { + super(os, new Deflater(Deflater.BEST_COMPRESSION)); + } + + /** + * Flush the given stream, preferring Java7 FLUSH_SYNC if available. + * + * @throws IOException + * In case of a lowlevel exception. + */ + @Override + public void flush() throws IOException { + if (!SUPPORTED) { + super.flush(); + return; + } + try { + int count = 0; + do { + count = (Integer) method.invoke(def, buf, 0, buf.length, 3); + if (count > 0) { + out.write(buf, 0, count); + } + } while (count > 0); + } catch (IllegalArgumentException e) { + throw new IOException("Can't flush"); + } catch (IllegalAccessException e) { + throw new IOException("Can't flush"); + } catch (InvocationTargetException e) { + throw new IOException("Can't flush"); + } + super.flush(); + } + +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xml/Element.java b/conversations/src/main/java/eu/siacs/conversations/xml/Element.java new file mode 100644 index 00000000..4e11ee2c --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xml/Element.java @@ -0,0 +1,148 @@ +package eu.siacs.conversations.xml; + +import java.util.ArrayList; +import java.util.Hashtable; +import java.util.List; + +import eu.siacs.conversations.utils.XmlHelper; + +public class Element { + protected String name; + protected Hashtable attributes = new Hashtable(); + protected String content; + protected List children = new ArrayList(); + + public Element(String name) { + this.name = name; + } + + public Element addChild(Element child) { + this.content = null; + children.add(child); + return child; + } + + public Element addChild(String name) { + this.content = null; + Element child = new Element(name); + children.add(child); + return child; + } + + public Element addChild(String name, String xmlns) { + this.content = null; + Element child = new Element(name); + child.setAttribute("xmlns", xmlns); + children.add(child); + return child; + } + + public Element setContent(String content) { + this.content = content; + this.children.clear(); + return this; + } + + public Element findChild(String name) { + for (Element child : this.children) { + if (child.getName().equals(name)) { + return child; + } + } + return null; + } + + public Element findChild(String name, String xmlns) { + for (Element child : this.children) { + if (child.getName().equals(name) + && (child.getAttribute("xmlns").equals(xmlns))) { + return child; + } + } + return null; + } + + public boolean hasChild(String name) { + return findChild(name) != null; + } + + public boolean hasChild(String name, String xmlns) { + return findChild(name, xmlns) != null; + } + + public List getChildren() { + return this.children; + } + + public Element setChildren(List children) { + this.children = children; + return this; + } + + public String getContent() { + return content; + } + + public Element setAttribute(String name, String value) { + if (name != null && value != null) { + this.attributes.put(name, value); + } + return this; + } + + public Element setAttributes(Hashtable attributes) { + this.attributes = attributes; + return this; + } + + public String getAttribute(String name) { + if (this.attributes.containsKey(name)) { + return this.attributes.get(name); + } else { + return null; + } + } + + public Hashtable getAttributes() { + return this.attributes; + } + + public String toString() { + StringBuilder elementOutput = new StringBuilder(); + if ((content == null) && (children.size() == 0)) { + Tag emptyTag = Tag.empty(name); + emptyTag.setAtttributes(this.attributes); + elementOutput.append(emptyTag.toString()); + } else { + Tag startTag = Tag.start(name); + startTag.setAtttributes(this.attributes); + elementOutput.append(startTag); + if (content != null) { + elementOutput.append(XmlHelper.encodeEntities(content)); + } else { + for (Element child : children) { + elementOutput.append(child.toString()); + } + } + Tag endTag = Tag.end(name); + elementOutput.append(endTag); + } + return elementOutput.toString(); + } + + public String getName() { + return name; + } + + public void clearChildren() { + this.children.clear(); + } + + public void setAttribute(String name, long value) { + this.setAttribute(name, Long.toString(value)); + } + + public void setAttribute(String name, int value) { + this.setAttribute(name, Integer.toString(value)); + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xml/Tag.java b/conversations/src/main/java/eu/siacs/conversations/xml/Tag.java new file mode 100644 index 00000000..b9ef979f --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xml/Tag.java @@ -0,0 +1,104 @@ +package eu.siacs.conversations.xml; + +import java.util.Hashtable; +import java.util.Iterator; +import java.util.Map.Entry; +import java.util.Set; + +import eu.siacs.conversations.utils.XmlHelper; + +public class Tag { + public static final int NO = -1; + public static final int START = 0; + public static final int END = 1; + public static final int EMPTY = 2; + + protected int type; + protected String name; + protected Hashtable attributes = new Hashtable(); + + protected Tag(int type, String name) { + this.type = type; + this.name = name; + } + + public static Tag no(String text) { + return new Tag(NO, text); + } + + public static Tag start(String name) { + return new Tag(START, name); + } + + public static Tag end(String name) { + return new Tag(END, name); + } + + public static Tag empty(String name) { + return new Tag(EMPTY, name); + } + + public String getName() { + return name; + } + + public String getAttribute(String attrName) { + return this.attributes.get(attrName); + } + + public Tag setAttribute(String attrName, String attrValue) { + this.attributes.put(attrName, attrValue); + return this; + } + + public Tag setAtttributes(Hashtable attributes) { + this.attributes = attributes; + return this; + } + + public boolean isStart(String needle) { + if (needle == null) + return false; + return (this.type == START) && (needle.equals(this.name)); + } + + public boolean isEnd(String needle) { + if (needle == null) + return false; + return (this.type == END) && (needle.equals(this.name)); + } + + public boolean isNo() { + return (this.type == NO); + } + + public String toString() { + StringBuilder tagOutput = new StringBuilder(); + tagOutput.append('<'); + if (type == END) { + tagOutput.append('/'); + } + tagOutput.append(name); + if (type != END) { + Set> attributeSet = attributes.entrySet(); + Iterator> it = attributeSet.iterator(); + while (it.hasNext()) { + Entry entry = it.next(); + tagOutput.append(' '); + tagOutput.append(entry.getKey()); + tagOutput.append("=\""); + tagOutput.append(XmlHelper.encodeEntities(entry.getValue())); + tagOutput.append('"'); + } + } + if (type == EMPTY) { + tagOutput.append('/'); + } + tagOutput.append('>'); + return tagOutput.toString(); + } + + public Hashtable getAttributes() { + return this.attributes; + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xml/TagWriter.java b/conversations/src/main/java/eu/siacs/conversations/xml/TagWriter.java new file mode 100644 index 00000000..f11c1846 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xml/TagWriter.java @@ -0,0 +1,114 @@ +package eu.siacs.conversations.xml; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.util.concurrent.LinkedBlockingQueue; + +import eu.siacs.conversations.xmpp.stanzas.AbstractStanza; + +public class TagWriter { + + private OutputStream plainOutputStream; + private OutputStreamWriter outputStream; + private boolean finshed = false; + private LinkedBlockingQueue writeQueue = new LinkedBlockingQueue(); + private Thread asyncStanzaWriter = new Thread() { + private boolean shouldStop = false; + + @Override + public void run() { + while (!shouldStop) { + if ((finshed) && (writeQueue.size() == 0)) { + return; + } + try { + AbstractStanza output = writeQueue.take(); + if (outputStream == null) { + shouldStop = true; + } else { + outputStream.write(output.toString()); + outputStream.flush(); + } + } catch (IOException e) { + shouldStop = true; + } catch (InterruptedException e) { + shouldStop = true; + } + } + } + }; + + public TagWriter() { + } + + public void setOutputStream(OutputStream out) throws IOException { + if (out == null) { + throw new IOException(); + } + this.plainOutputStream = out; + this.outputStream = new OutputStreamWriter(out); + } + + public OutputStream getOutputStream() throws IOException { + if (this.plainOutputStream == null) { + throw new IOException(); + } + return this.plainOutputStream; + } + + public TagWriter beginDocument() throws IOException { + if (outputStream == null) { + throw new IOException("output stream was null"); + } + outputStream.write(""); + outputStream.flush(); + return this; + } + + public TagWriter writeTag(Tag tag) throws IOException { + if (outputStream == null) { + throw new IOException("output stream was null"); + } + outputStream.write(tag.toString()); + outputStream.flush(); + return this; + } + + public TagWriter writeElement(Element element) throws IOException { + if (outputStream == null) { + throw new IOException("output stream was null"); + } + outputStream.write(element.toString()); + outputStream.flush(); + return this; + } + + public TagWriter writeStanzaAsync(AbstractStanza stanza) { + if (finshed) { + return this; + } else { + if (!asyncStanzaWriter.isAlive()) { + try { + asyncStanzaWriter.start(); + } catch (IllegalThreadStateException e) { + // already started + } + } + writeQueue.add(stanza); + return this; + } + } + + public void finish() { + this.finshed = true; + } + + public boolean finished() { + return (this.writeQueue.size() == 0); + } + + public boolean isActive() { + return outputStream != null; + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xml/XmlReader.java b/conversations/src/main/java/eu/siacs/conversations/xml/XmlReader.java new file mode 100644 index 00000000..52d3d46a --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xml/XmlReader.java @@ -0,0 +1,141 @@ +package eu.siacs.conversations.xml; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import eu.siacs.conversations.Config; + +import android.os.PowerManager; +import android.os.PowerManager.WakeLock; +import android.util.Log; +import android.util.Xml; + +public class XmlReader { + private XmlPullParser parser; + private PowerManager.WakeLock wakeLock; + private InputStream is; + + public XmlReader(WakeLock wakeLock) { + this.parser = Xml.newPullParser(); + try { + this.parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, + true); + } catch (XmlPullParserException e) { + Log.d(Config.LOGTAG, "error setting namespace feature on parser"); + } + this.wakeLock = wakeLock; + } + + public void setInputStream(InputStream inputStream) throws IOException { + if (inputStream == null) { + throw new IOException(); + } + this.is = inputStream; + try { + parser.setInput(new InputStreamReader(this.is)); + } catch (XmlPullParserException e) { + throw new IOException("error resetting parser"); + } + } + + public InputStream getInputStream() throws IOException { + if (this.is == null) { + throw new IOException(); + } + return is; + } + + public void reset() throws IOException { + if (this.is == null) { + throw new IOException(); + } + try { + parser.setInput(new InputStreamReader(this.is)); + } catch (XmlPullParserException e) { + throw new IOException("error resetting parser"); + } + } + + public Tag readTag() throws XmlPullParserException, IOException { + if (wakeLock.isHeld()) { + try { + wakeLock.release(); + } catch (RuntimeException re) { + } + } + try { + while (this.is != null + && parser.next() != XmlPullParser.END_DOCUMENT) { + wakeLock.acquire(); + if (parser.getEventType() == XmlPullParser.START_TAG) { + Tag tag = Tag.start(parser.getName()); + for (int i = 0; i < parser.getAttributeCount(); ++i) { + tag.setAttribute(parser.getAttributeName(i), + parser.getAttributeValue(i)); + } + String xmlns = parser.getNamespace(); + if (xmlns != null) { + tag.setAttribute("xmlns", xmlns); + } + return tag; + } else if (parser.getEventType() == XmlPullParser.END_TAG) { + Tag tag = Tag.end(parser.getName()); + return tag; + } else if (parser.getEventType() == XmlPullParser.TEXT) { + Tag tag = Tag.no(parser.getText()); + return tag; + } + } + if (wakeLock.isHeld()) { + try { + wakeLock.release(); + } catch (RuntimeException re) { + } + } + } catch (ArrayIndexOutOfBoundsException e) { + throw new IOException( + "xml parser mishandled ArrayIndexOufOfBounds", e); + } catch (StringIndexOutOfBoundsException e) { + throw new IOException( + "xml parser mishandled StringIndexOufOfBounds", e); + } catch (NullPointerException e) { + throw new IOException("xml parser mishandled NullPointerException", + e); + } catch (IndexOutOfBoundsException e) { + throw new IOException("xml parser mishandled IndexOutOfBound", e); + } + return null; + } + + public Element readElement(Tag currentTag) throws XmlPullParserException, + IOException { + Element element = new Element(currentTag.getName()); + element.setAttributes(currentTag.getAttributes()); + Tag nextTag = this.readTag(); + if (nextTag == null) { + throw new IOException("unterupted mid tag"); + } + if (nextTag.isNo()) { + element.setContent(nextTag.getName()); + nextTag = this.readTag(); + if (nextTag == null) { + throw new IOException("unterupted mid tag"); + } + } + while (!nextTag.isEnd(element.getName())) { + if (!nextTag.isNo()) { + Element child = this.readElement(nextTag); + element.addChild(child); + } + nextTag = this.readTag(); + if (nextTag == null) { + throw new IOException("unterupted mid tag"); + } + } + return element; + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/OnBindListener.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/OnBindListener.java new file mode 100644 index 00000000..f09cf33d --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/OnBindListener.java @@ -0,0 +1,7 @@ +package eu.siacs.conversations.xmpp; + +import eu.siacs.conversations.entities.Account; + +public interface OnBindListener { + public void onBind(Account account); +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/OnContactStatusChanged.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/OnContactStatusChanged.java new file mode 100644 index 00000000..849e8e76 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/OnContactStatusChanged.java @@ -0,0 +1,7 @@ +package eu.siacs.conversations.xmpp; + +import eu.siacs.conversations.entities.Contact; + +public interface OnContactStatusChanged { + public void onContactStatusChanged(Contact contact, boolean online); +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/OnIqPacketReceived.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/OnIqPacketReceived.java new file mode 100644 index 00000000..a4cff986 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/OnIqPacketReceived.java @@ -0,0 +1,8 @@ +package eu.siacs.conversations.xmpp; + +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.xmpp.stanzas.IqPacket; + +public interface OnIqPacketReceived extends PacketReceived { + public void onIqPacketReceived(Account account, IqPacket packet); +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/OnMessageAcknowledged.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/OnMessageAcknowledged.java new file mode 100644 index 00000000..5f670d93 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/OnMessageAcknowledged.java @@ -0,0 +1,7 @@ +package eu.siacs.conversations.xmpp; + +import eu.siacs.conversations.entities.Account; + +public interface OnMessageAcknowledged { + public void onMessageAcknowledged(Account account, String id); +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/OnMessagePacketReceived.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/OnMessagePacketReceived.java new file mode 100644 index 00000000..325e945f --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/OnMessagePacketReceived.java @@ -0,0 +1,8 @@ +package eu.siacs.conversations.xmpp; + +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.xmpp.stanzas.MessagePacket; + +public interface OnMessagePacketReceived extends PacketReceived { + public void onMessagePacketReceived(Account account, MessagePacket packet); +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/OnPresencePacketReceived.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/OnPresencePacketReceived.java new file mode 100644 index 00000000..95c1acfc --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/OnPresencePacketReceived.java @@ -0,0 +1,8 @@ +package eu.siacs.conversations.xmpp; + +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.xmpp.stanzas.PresencePacket; + +public interface OnPresencePacketReceived extends PacketReceived { + public void onPresencePacketReceived(Account account, PresencePacket packet); +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/OnStatusChanged.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/OnStatusChanged.java new file mode 100644 index 00000000..ad1d98cb --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/OnStatusChanged.java @@ -0,0 +1,7 @@ +package eu.siacs.conversations.xmpp; + +import eu.siacs.conversations.entities.Account; + +public interface OnStatusChanged { + public void onStatusChanged(Account account); +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/PacketReceived.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/PacketReceived.java new file mode 100644 index 00000000..d4502d73 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/PacketReceived.java @@ -0,0 +1,5 @@ +package eu.siacs.conversations.xmpp; + +public abstract interface PacketReceived { + +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java new file mode 100644 index 00000000..903dc59d --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -0,0 +1,1130 @@ +package eu.siacs.conversations.xmpp; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.math.BigInteger; +import java.net.Socket; +import java.net.UnknownHostException; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.LinkedList; +import java.util.List; +import java.util.Map.Entry; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; + +import javax.net.ssl.X509TrustManager; + +import org.apache.http.conn.ssl.StrictHostnameVerifier; +import org.xmlpull.v1.XmlPullParserException; + +import de.duenndns.ssl.MemorizingTrustManager; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.os.PowerManager; +import android.os.PowerManager.WakeLock; +import android.os.SystemClock; +import android.preference.PreferenceManager; +import android.util.Log; +import android.util.SparseArray; +import eu.siacs.conversations.Config; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.utils.CryptoHelper; +import eu.siacs.conversations.utils.DNSHelper; +import eu.siacs.conversations.utils.zlib.ZLibOutputStream; +import eu.siacs.conversations.utils.zlib.ZLibInputStream; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xml.Tag; +import eu.siacs.conversations.xml.TagWriter; +import eu.siacs.conversations.xml.XmlReader; +import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived; +import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; +import eu.siacs.conversations.xmpp.stanzas.AbstractStanza; +import eu.siacs.conversations.xmpp.stanzas.IqPacket; +import eu.siacs.conversations.xmpp.stanzas.MessagePacket; +import eu.siacs.conversations.xmpp.stanzas.PresencePacket; +import eu.siacs.conversations.xmpp.stanzas.csi.ActivePacket; +import eu.siacs.conversations.xmpp.stanzas.csi.InactivePacket; +import eu.siacs.conversations.xmpp.stanzas.streammgmt.AckPacket; +import eu.siacs.conversations.xmpp.stanzas.streammgmt.EnablePacket; +import eu.siacs.conversations.xmpp.stanzas.streammgmt.RequestPacket; +import eu.siacs.conversations.xmpp.stanzas.streammgmt.ResumePacket; + +public class XmppConnection implements Runnable { + + protected Account account; + + private WakeLock wakeLock; + + private SecureRandom mRandom; + + private Socket socket; + private XmlReader tagReader; + private TagWriter tagWriter; + + private Features features = new Features(this); + + private boolean shouldBind = true; + private boolean shouldAuthenticate = true; + private Element streamFeatures; + private HashMap> disco = new HashMap>(); + + private String streamId = null; + private int smVersion = 3; + private SparseArray messageReceipts = new SparseArray(); + + private boolean usingCompression = false; + private boolean usingEncryption = false; + + private int stanzasReceived = 0; + private int stanzasSent = 0; + + private long lastPaketReceived = 0; + private long lastPingSent = 0; + private long lastConnect = 0; + private long lastSessionStarted = 0; + + private int attempt = 0; + + private static final int PACKET_IQ = 0; + private static final int PACKET_MESSAGE = 1; + private static final int PACKET_PRESENCE = 2; + + private Hashtable packetCallbacks = new Hashtable(); + private OnPresencePacketReceived presenceListener = null; + private OnJinglePacketReceived jingleListener = null; + private OnIqPacketReceived unregisteredIqListener = null; + private OnMessagePacketReceived messageListener = null; + private OnStatusChanged statusListener = null; + private OnBindListener bindListener = null; + private OnMessageAcknowledged acknowledgedListener = null; + private MemorizingTrustManager mMemorizingTrustManager; + private final Context applicationContext; + + public XmppConnection(Account account, XmppConnectionService service) { + this.mRandom = service.getRNG(); + this.mMemorizingTrustManager = service.getMemorizingTrustManager(); + this.account = account; + this.wakeLock = service.getPowerManager().newWakeLock( + PowerManager.PARTIAL_WAKE_LOCK, account.getJid()); + tagWriter = new TagWriter(); + applicationContext = service.getApplicationContext(); + } + + protected void changeStatus(int nextStatus) { + if (account.getStatus() != nextStatus) { + if ((nextStatus == Account.STATUS_OFFLINE) + && (account.getStatus() != Account.STATUS_CONNECTING) + && (account.getStatus() != Account.STATUS_ONLINE) + && (account.getStatus() != Account.STATUS_DISABLED)) { + return; + } + if (nextStatus == Account.STATUS_ONLINE) { + this.attempt = 0; + } + account.setStatus(nextStatus); + if (statusListener != null) { + statusListener.onStatusChanged(account); + } + } + } + + protected void connect() { + Log.d(Config.LOGTAG, account.getJid() + ": connecting"); + usingCompression = false; + usingEncryption = false; + lastConnect = SystemClock.elapsedRealtime(); + lastPingSent = SystemClock.elapsedRealtime(); + this.attempt++; + try { + shouldAuthenticate = shouldBind = !account + .isOptionSet(Account.OPTION_REGISTER); + tagReader = new XmlReader(wakeLock); + tagWriter = new TagWriter(); + packetCallbacks.clear(); + this.changeStatus(Account.STATUS_CONNECTING); + Bundle namePort = DNSHelper.getSRVRecord(account.getServer()); + if ("timeout".equals(namePort.getString("error"))) { + Log.d(Config.LOGTAG, account.getJid() + ": dns timeout"); + this.changeStatus(Account.STATUS_OFFLINE); + return; + } + String srvRecordServer = namePort.getString("name"); + String srvIpServer = namePort.getString("ipv4"); + int srvRecordPort = namePort.getInt("port"); + if (srvRecordServer != null) { + if (srvIpServer != null) { + Log.d(Config.LOGTAG, account.getJid() + + ": using values from dns " + srvRecordServer + + "[" + srvIpServer + "]:" + srvRecordPort); + socket = new Socket(srvIpServer, srvRecordPort); + } else { + boolean socketError = true; + int srvIndex = 0; + while (socketError + && namePort.containsKey("name" + srvIndex)) { + try { + srvRecordServer = namePort.getString("name" + + srvIndex); + srvRecordPort = namePort.getInt("port" + srvIndex); + Log.d(Config.LOGTAG, account.getJid() + + ": using values from dns " + + srvRecordServer + ":" + srvRecordPort); + socket = new Socket(srvRecordServer, srvRecordPort); + socketError = false; + } catch (UnknownHostException e) { + srvIndex++; + if (!namePort.containsKey("name" + srvIndex)) { + throw e; + } + } catch (IOException e) { + srvIndex++; + if (!namePort.containsKey("name" + srvIndex)) { + throw e; + } + } + } + } + } else if (namePort.containsKey("error") + && "nosrv".equals(namePort.getString("error", null))) { + socket = new Socket(account.getServer(), 5222); + } else { + Log.d(Config.LOGTAG, account.getJid() + + ": timeout in DNS resolution"); + changeStatus(Account.STATUS_OFFLINE); + return; + } + OutputStream out = socket.getOutputStream(); + tagWriter.setOutputStream(out); + InputStream in = socket.getInputStream(); + tagReader.setInputStream(in); + tagWriter.beginDocument(); + sendStartStream(); + Tag nextTag; + while ((nextTag = tagReader.readTag()) != null) { + if (nextTag.isStart("stream")) { + processStream(nextTag); + break; + } else { + Log.d(Config.LOGTAG, + "found unexpected tag: " + nextTag.getName()); + return; + } + } + if (socket.isConnected()) { + socket.close(); + } + } catch (UnknownHostException e) { + this.changeStatus(Account.STATUS_SERVER_NOT_FOUND); + if (wakeLock.isHeld()) { + try { + wakeLock.release(); + } catch (RuntimeException re) { + } + } + return; + } catch (IOException e) { + this.changeStatus(Account.STATUS_OFFLINE); + if (wakeLock.isHeld()) { + try { + wakeLock.release(); + } catch (RuntimeException re) { + } + } + return; + } catch (NoSuchAlgorithmException e) { + this.changeStatus(Account.STATUS_OFFLINE); + Log.d(Config.LOGTAG, "compression exception " + e.getMessage()); + if (wakeLock.isHeld()) { + try { + wakeLock.release(); + } catch (RuntimeException re) { + } + } + return; + } catch (XmlPullParserException e) { + this.changeStatus(Account.STATUS_OFFLINE); + Log.d(Config.LOGTAG, "xml exception " + e.getMessage()); + if (wakeLock.isHeld()) { + try { + wakeLock.release(); + } catch (RuntimeException re) { + } + } + return; + } + + } + + @Override + public void run() { + connect(); + } + + private void processStream(Tag currentTag) throws XmlPullParserException, + IOException, NoSuchAlgorithmException { + Tag nextTag = tagReader.readTag(); + while ((nextTag != null) && (!nextTag.isEnd("stream"))) { + if (nextTag.isStart("error")) { + processStreamError(nextTag); + } else if (nextTag.isStart("features")) { + processStreamFeatures(nextTag); + } else if (nextTag.isStart("proceed")) { + switchOverToTls(nextTag); + } else if (nextTag.isStart("compressed")) { + switchOverToZLib(nextTag); + } else if (nextTag.isStart("success")) { + Log.d(Config.LOGTAG, account.getJid() + ": logged in"); + tagReader.readTag(); + tagReader.reset(); + sendStartStream(); + processStream(tagReader.readTag()); + break; + } else if (nextTag.isStart("failure")) { + tagReader.readElement(nextTag); + changeStatus(Account.STATUS_UNAUTHORIZED); + } else if (nextTag.isStart("challenge")) { + String challange = tagReader.readElement(nextTag).getContent(); + Element response = new Element("response"); + response.setAttribute("xmlns", + "urn:ietf:params:xml:ns:xmpp-sasl"); + response.setContent(CryptoHelper.saslDigestMd5(account, + challange, mRandom)); + tagWriter.writeElement(response); + } else if (nextTag.isStart("enabled")) { + Element enabled = tagReader.readElement(nextTag); + if ("true".equals(enabled.getAttribute("resume"))) { + this.streamId = enabled.getAttribute("id"); + Log.d(Config.LOGTAG, account.getJid() + + ": stream managment(" + smVersion + + ") enabled (resumable)"); + } else { + Log.d(Config.LOGTAG, account.getJid() + + ": stream managment(" + smVersion + ") enabled"); + } + this.lastSessionStarted = SystemClock.elapsedRealtime(); + this.stanzasReceived = 0; + RequestPacket r = new RequestPacket(smVersion); + tagWriter.writeStanzaAsync(r); + } else if (nextTag.isStart("resumed")) { + lastPaketReceived = SystemClock.elapsedRealtime(); + Element resumed = tagReader.readElement(nextTag); + String h = resumed.getAttribute("h"); + try { + int serverCount = Integer.parseInt(h); + if (serverCount != stanzasSent) { + Log.d(Config.LOGTAG, account.getJid() + + ": session resumed with lost packages"); + stanzasSent = serverCount; + } else { + Log.d(Config.LOGTAG, account.getJid() + + ": session resumed"); + } + if (acknowledgedListener != null) { + for (int i = 0; i < messageReceipts.size(); ++i) { + if (serverCount >= messageReceipts.keyAt(i)) { + acknowledgedListener.onMessageAcknowledged( + account, messageReceipts.valueAt(i)); + } + } + } + messageReceipts.clear(); + } catch (NumberFormatException e) { + + } + sendInitialPing(); + + } else if (nextTag.isStart("r")) { + tagReader.readElement(nextTag); + AckPacket ack = new AckPacket(this.stanzasReceived, smVersion); + tagWriter.writeStanzaAsync(ack); + } else if (nextTag.isStart("a")) { + Element ack = tagReader.readElement(nextTag); + lastPaketReceived = SystemClock.elapsedRealtime(); + int serverSequence = Integer.parseInt(ack.getAttribute("h")); + String msgId = this.messageReceipts.get(serverSequence); + if (msgId != null) { + if (this.acknowledgedListener != null) { + this.acknowledgedListener.onMessageAcknowledged( + account, msgId); + } + this.messageReceipts.remove(serverSequence); + } + } else if (nextTag.isStart("failed")) { + tagReader.readElement(nextTag); + Log.d(Config.LOGTAG, account.getJid() + ": resumption failed"); + streamId = null; + if (account.getStatus() != Account.STATUS_ONLINE) { + sendBindRequest(); + } + } else if (nextTag.isStart("iq")) { + processIq(nextTag); + } else if (nextTag.isStart("message")) { + processMessage(nextTag); + } else if (nextTag.isStart("presence")) { + processPresence(nextTag); + } + nextTag = tagReader.readTag(); + } + if (account.getStatus() == Account.STATUS_ONLINE) { + account.setStatus(Account.STATUS_OFFLINE); + if (statusListener != null) { + statusListener.onStatusChanged(account); + } + } + } + + private void sendInitialPing() { + Log.d(Config.LOGTAG, account.getJid() + ": sending intial ping"); + IqPacket iq = new IqPacket(IqPacket.TYPE_GET); + iq.setFrom(account.getFullJid()); + iq.addChild("ping", "urn:xmpp:ping"); + this.sendIqPacket(iq, new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + Log.d(Config.LOGTAG, account.getJid() + + ": online with resource " + account.getResource()); + changeStatus(Account.STATUS_ONLINE); + } + }); + } + + private Element processPacket(Tag currentTag, int packetType) + throws XmlPullParserException, IOException { + Element element; + switch (packetType) { + case PACKET_IQ: + element = new IqPacket(); + break; + case PACKET_MESSAGE: + element = new MessagePacket(); + break; + case PACKET_PRESENCE: + element = new PresencePacket(); + break; + default: + return null; + } + element.setAttributes(currentTag.getAttributes()); + Tag nextTag = tagReader.readTag(); + if (nextTag == null) { + throw new IOException("interrupted mid tag"); + } + while (!nextTag.isEnd(element.getName())) { + if (!nextTag.isNo()) { + Element child = tagReader.readElement(nextTag); + String type = currentTag.getAttribute("type"); + if (packetType == PACKET_IQ + && "jingle".equals(child.getName()) + && ("set".equalsIgnoreCase(type) || "get" + .equalsIgnoreCase(type))) { + element = new JinglePacket(); + element.setAttributes(currentTag.getAttributes()); + } + element.addChild(child); + } + nextTag = tagReader.readTag(); + if (nextTag == null) { + throw new IOException("interrupted mid tag"); + } + } + ++stanzasReceived; + lastPaketReceived = SystemClock.elapsedRealtime(); + return element; + } + + private void processIq(Tag currentTag) throws XmlPullParserException, + IOException { + IqPacket packet = (IqPacket) processPacket(currentTag, PACKET_IQ); + + if (packet.getId() == null) { + return; // an iq packet without id is definitely invalid + } + + if (packet instanceof JinglePacket) { + if (this.jingleListener != null) { + this.jingleListener.onJinglePacketReceived(account, + (JinglePacket) packet); + } + } else { + if (packetCallbacks.containsKey(packet.getId())) { + if (packetCallbacks.get(packet.getId()) instanceof OnIqPacketReceived) { + ((OnIqPacketReceived) packetCallbacks.get(packet.getId())) + .onIqPacketReceived(account, packet); + } + + packetCallbacks.remove(packet.getId()); + } else if ((packet.getType() == IqPacket.TYPE_GET || packet + .getType() == IqPacket.TYPE_SET) + && this.unregisteredIqListener != null) { + this.unregisteredIqListener.onIqPacketReceived(account, packet); + } + } + } + + private void processMessage(Tag currentTag) throws XmlPullParserException, + IOException { + MessagePacket packet = (MessagePacket) processPacket(currentTag, + PACKET_MESSAGE); + String id = packet.getAttribute("id"); + if ((id != null) && (packetCallbacks.containsKey(id))) { + if (packetCallbacks.get(id) instanceof OnMessagePacketReceived) { + ((OnMessagePacketReceived) packetCallbacks.get(id)) + .onMessagePacketReceived(account, packet); + } + packetCallbacks.remove(id); + } else if (this.messageListener != null) { + this.messageListener.onMessagePacketReceived(account, packet); + } + } + + private void processPresence(Tag currentTag) throws XmlPullParserException, + IOException { + PresencePacket packet = (PresencePacket) processPacket(currentTag, + PACKET_PRESENCE); + String id = packet.getAttribute("id"); + if ((id != null) && (packetCallbacks.containsKey(id))) { + if (packetCallbacks.get(id) instanceof OnPresencePacketReceived) { + ((OnPresencePacketReceived) packetCallbacks.get(id)) + .onPresencePacketReceived(account, packet); + } + packetCallbacks.remove(id); + } else if (this.presenceListener != null) { + this.presenceListener.onPresencePacketReceived(account, packet); + } + } + + private void sendCompressionZlib() throws IOException { + Element compress = new Element("compress"); + compress.setAttribute("xmlns", "http://jabber.org/protocol/compress"); + compress.addChild("method").setContent("zlib"); + tagWriter.writeElement(compress); + } + + private void switchOverToZLib(Tag currentTag) + throws XmlPullParserException, IOException, + NoSuchAlgorithmException { + tagReader.readTag(); // read tag close + tagWriter.setOutputStream(new ZLibOutputStream(tagWriter + .getOutputStream())); + tagReader + .setInputStream(new ZLibInputStream(tagReader.getInputStream())); + + sendStartStream(); + Log.d(Config.LOGTAG, account.getJid() + ": compression enabled"); + usingCompression = true; + processStream(tagReader.readTag()); + } + + private void sendStartTLS() throws IOException { + Tag startTLS = Tag.empty("starttls"); + startTLS.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-tls"); + tagWriter.writeTag(startTLS); + } + + private SharedPreferences getPreferences() { + return PreferenceManager + .getDefaultSharedPreferences(applicationContext); + } + + private boolean enableLegacySSL() { + return getPreferences().getBoolean("enable_legacy_ssl", false); + } + + private void switchOverToTls(Tag currentTag) throws XmlPullParserException, + IOException { + tagReader.readTag(); + try { + SSLContext sc = SSLContext.getInstance("TLS"); + sc.init(null, + new X509TrustManager[] { this.mMemorizingTrustManager }, + mRandom); + SSLSocketFactory factory = sc.getSocketFactory(); + + HostnameVerifier verifier = this.mMemorizingTrustManager + .wrapHostnameVerifier(new StrictHostnameVerifier()); + SSLSocket sslSocket = (SSLSocket) factory.createSocket(socket, + socket.getInetAddress().getHostAddress(), socket.getPort(), + true); + + // Support all protocols except legacy SSL. + // The min SDK version prevents us having to worry about SSLv2. In + // future, this may be + // true of SSLv3 as well. + final String[] supportProtocols; + if (enableLegacySSL()) { + supportProtocols = sslSocket.getSupportedProtocols(); + } else { + final List supportedProtocols = new LinkedList( + Arrays.asList(sslSocket.getSupportedProtocols())); + supportedProtocols.remove("SSLv3"); + supportProtocols = new String[supportedProtocols.size()]; + supportedProtocols.toArray(supportProtocols); + } + sslSocket.setEnabledProtocols(supportProtocols); + + if (verifier != null + && !verifier.verify(account.getServer(), + sslSocket.getSession())) { + Log.d(Config.LOGTAG, account.getJid() + + ": host mismatch in TLS connection"); + sslSocket.close(); + throw new IOException(); + } + tagReader.setInputStream(sslSocket.getInputStream()); + tagWriter.setOutputStream(sslSocket.getOutputStream()); + sendStartStream(); + Log.d(Config.LOGTAG, account.getJid() + + ": TLS connection established"); + usingEncryption = true; + processStream(tagReader.readTag()); + sslSocket.close(); + } catch (NoSuchAlgorithmException e1) { + e1.printStackTrace(); + } catch (KeyManagementException e) { + e.printStackTrace(); + } + } + + private void sendSaslAuthPlain() throws IOException { + String saslString = CryptoHelper.saslPlain(account.getUsername(), + account.getPassword()); + Element auth = new Element("auth"); + auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl"); + auth.setAttribute("mechanism", "PLAIN"); + auth.setContent(saslString); + tagWriter.writeElement(auth); + } + + private void sendSaslAuthDigestMd5() throws IOException { + Element auth = new Element("auth"); + auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl"); + auth.setAttribute("mechanism", "DIGEST-MD5"); + tagWriter.writeElement(auth); + } + + private void processStreamFeatures(Tag currentTag) + throws XmlPullParserException, IOException { + this.streamFeatures = tagReader.readElement(currentTag); + if (this.streamFeatures.hasChild("starttls") && !usingEncryption) { + sendStartTLS(); + } else if (compressionAvailable()) { + sendCompressionZlib(); + } else if (this.streamFeatures.hasChild("register") + && account.isOptionSet(Account.OPTION_REGISTER) + && usingEncryption) { + sendRegistryRequest(); + } else if (!this.streamFeatures.hasChild("register") + && account.isOptionSet(Account.OPTION_REGISTER)) { + changeStatus(Account.STATUS_REGISTRATION_NOT_SUPPORTED); + disconnect(true); + } else if (this.streamFeatures.hasChild("mechanisms") + && shouldAuthenticate && usingEncryption) { + List mechanisms = extractMechanisms(streamFeatures + .findChild("mechanisms")); + if (mechanisms.contains("PLAIN")) { + sendSaslAuthPlain(); + } else if (mechanisms.contains("DIGEST-MD5")) { + sendSaslAuthDigestMd5(); + } + } else if (this.streamFeatures.hasChild("sm", "urn:xmpp:sm:" + + smVersion) + && streamId != null) { + ResumePacket resume = new ResumePacket(this.streamId, + stanzasReceived, smVersion); + this.tagWriter.writeStanzaAsync(resume); + } else if (this.streamFeatures.hasChild("bind") && shouldBind) { + sendBindRequest(); + } else { + Log.d(Config.LOGTAG, account.getJid() + + ": incompatible server. disconnecting"); + disconnect(true); + } + } + + private boolean compressionAvailable() { + if (!this.streamFeatures.hasChild("compression", + "http://jabber.org/features/compress")) + return false; + if (!ZLibOutputStream.SUPPORTED) + return false; + if (!account.isOptionSet(Account.OPTION_USECOMPRESSION)) + return false; + + Element compression = this.streamFeatures.findChild("compression", + "http://jabber.org/features/compress"); + for (Element child : compression.getChildren()) { + if (!"method".equals(child.getName())) + continue; + + if ("zlib".equalsIgnoreCase(child.getContent())) { + return true; + } + } + return false; + } + + private List extractMechanisms(Element stream) { + ArrayList mechanisms = new ArrayList(stream + .getChildren().size()); + for (Element child : stream.getChildren()) { + mechanisms.add(child.getContent()); + } + return mechanisms; + } + + private void sendRegistryRequest() { + IqPacket register = new IqPacket(IqPacket.TYPE_GET); + register.query("jabber:iq:register"); + register.setTo(account.getServer()); + sendIqPacket(register, new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + Element instructions = packet.query().findChild("instructions"); + if (packet.query().hasChild("username") + && (packet.query().hasChild("password"))) { + IqPacket register = new IqPacket(IqPacket.TYPE_SET); + Element username = new Element("username") + .setContent(account.getUsername()); + Element password = new Element("password") + .setContent(account.getPassword()); + register.query("jabber:iq:register").addChild(username); + register.query().addChild(password); + sendIqPacket(register, new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, + IqPacket packet) { + if (packet.getType() == IqPacket.TYPE_RESULT) { + account.setOption(Account.OPTION_REGISTER, + false); + changeStatus(Account.STATUS_REGISTRATION_SUCCESSFULL); + } else if (packet.hasChild("error") + && (packet.findChild("error") + .hasChild("conflict"))) { + changeStatus(Account.STATUS_REGISTRATION_CONFLICT); + } else { + changeStatus(Account.STATUS_REGISTRATION_FAILED); + Log.d(Config.LOGTAG, packet.toString()); + } + disconnect(true); + } + }); + } else { + changeStatus(Account.STATUS_REGISTRATION_FAILED); + disconnect(true); + Log.d(Config.LOGTAG, account.getJid() + + ": could not register. instructions are" + + instructions.getContent()); + } + } + }); + } + + private void sendBindRequest() throws IOException { + IqPacket iq = new IqPacket(IqPacket.TYPE_SET); + iq.addChild("bind", "urn:ietf:params:xml:ns:xmpp-bind") + .addChild("resource").setContent(account.getResource()); + this.sendUnboundIqPacket(iq, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + Element bind = packet.findChild("bind"); + if (bind != null) { + Element jid = bind.findChild("jid"); + if (jid != null && jid.getContent() != null) { + account.setResource(jid.getContent().split("/", 2)[1]); + if (streamFeatures.hasChild("sm", "urn:xmpp:sm:3")) { + smVersion = 3; + EnablePacket enable = new EnablePacket(smVersion); + tagWriter.writeStanzaAsync(enable); + stanzasSent = 0; + messageReceipts.clear(); + } else if (streamFeatures.hasChild("sm", + "urn:xmpp:sm:2")) { + smVersion = 2; + EnablePacket enable = new EnablePacket(smVersion); + tagWriter.writeStanzaAsync(enable); + stanzasSent = 0; + messageReceipts.clear(); + } + sendServiceDiscoveryInfo(account.getServer()); + sendServiceDiscoveryItems(account.getServer()); + if (bindListener != null) { + bindListener.onBind(account); + } + sendInitialPing(); + } else { + disconnect(true); + } + } else { + disconnect(true); + } + } + }); + if (this.streamFeatures.hasChild("session")) { + Log.d(Config.LOGTAG, account.getJid() + + ": sending deprecated session"); + IqPacket startSession = new IqPacket(IqPacket.TYPE_SET); + startSession.addChild("session", + "urn:ietf:params:xml:ns:xmpp-session"); + this.sendUnboundIqPacket(startSession, null); + } + } + + private void sendServiceDiscoveryInfo(final String server) { + IqPacket iq = new IqPacket(IqPacket.TYPE_GET); + iq.setTo(server); + iq.query("http://jabber.org/protocol/disco#info"); + this.sendIqPacket(iq, new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + List elements = packet.query().getChildren(); + List features = new ArrayList(); + for (int i = 0; i < elements.size(); ++i) { + if (elements.get(i).getName().equals("feature")) { + features.add(elements.get(i).getAttribute("var")); + } + } + disco.put(server, features); + + if (account.getServer().equals(server)) { + enableAdvancedStreamFeatures(); + } + } + }); + } + + private void enableAdvancedStreamFeatures() { + if (getFeatures().carbons()) { + sendEnableCarbons(); + } + } + + private void sendServiceDiscoveryItems(final String server) { + IqPacket iq = new IqPacket(IqPacket.TYPE_GET); + iq.setTo(server); + iq.query("http://jabber.org/protocol/disco#items"); + this.sendIqPacket(iq, new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + List elements = packet.query().getChildren(); + for (int i = 0; i < elements.size(); ++i) { + if (elements.get(i).getName().equals("item")) { + String jid = elements.get(i).getAttribute("jid"); + sendServiceDiscoveryInfo(jid); + } + } + } + }); + } + + private void sendEnableCarbons() { + IqPacket iq = new IqPacket(IqPacket.TYPE_SET); + iq.addChild("enable", "urn:xmpp:carbons:2"); + this.sendIqPacket(iq, new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (!packet.hasChild("error")) { + Log.d(Config.LOGTAG, account.getJid() + + ": successfully enabled carbons"); + } else { + Log.d(Config.LOGTAG, account.getJid() + + ": error enableing carbons " + packet.toString()); + } + } + }); + } + + private void processStreamError(Tag currentTag) + throws XmlPullParserException, IOException { + Element streamError = tagReader.readElement(currentTag); + if (streamError != null && streamError.hasChild("conflict")) { + String resource = account.getResource().split("\\.")[0]; + account.setResource(resource + "." + nextRandomId()); + Log.d(Config.LOGTAG, + account.getJid() + ": switching resource due to conflict (" + + account.getResource() + ")"); + } + } + + private void sendStartStream() throws IOException { + Tag stream = Tag.start("stream:stream"); + stream.setAttribute("from", account.getJid()); + stream.setAttribute("to", account.getServer()); + stream.setAttribute("version", "1.0"); + stream.setAttribute("xml:lang", "en"); + stream.setAttribute("xmlns", "jabber:client"); + stream.setAttribute("xmlns:stream", "http://etherx.jabber.org/streams"); + tagWriter.writeTag(stream); + } + + private String nextRandomId() { + return new BigInteger(50, mRandom).toString(32); + } + + public void sendIqPacket(IqPacket packet, OnIqPacketReceived callback) { + if (packet.getId() == null) { + String id = nextRandomId(); + packet.setAttribute("id", id); + } + packet.setFrom(account.getFullJid()); + this.sendPacket(packet, callback); + } + + public void sendUnboundIqPacket(IqPacket packet, OnIqPacketReceived callback) { + if (packet.getId() == null) { + String id = nextRandomId(); + packet.setAttribute("id", id); + } + this.sendPacket(packet, callback); + } + + public void sendMessagePacket(MessagePacket packet) { + this.sendPacket(packet, null); + } + + public void sendPresencePacket(PresencePacket packet) { + this.sendPacket(packet, null); + } + + private synchronized void sendPacket(final AbstractStanza packet, + PacketReceived callback) { + if (packet.getName().equals("iq") || packet.getName().equals("message") + || packet.getName().equals("presence")) { + ++stanzasSent; + } + tagWriter.writeStanzaAsync(packet); + if (packet instanceof MessagePacket && packet.getId() != null + && this.streamId != null) { + Log.d(Config.LOGTAG, "request delivery report for stanza " + + stanzasSent); + this.messageReceipts.put(stanzasSent, packet.getId()); + tagWriter.writeStanzaAsync(new RequestPacket(this.smVersion)); + } + if (callback != null) { + if (packet.getId() == null) { + packet.setId(nextRandomId()); + } + packetCallbacks.put(packet.getId(), callback); + } + } + + public void sendPing() { + if (streamFeatures.hasChild("sm")) { + tagWriter.writeStanzaAsync(new RequestPacket(smVersion)); + } else { + IqPacket iq = new IqPacket(IqPacket.TYPE_GET); + iq.setFrom(account.getFullJid()); + iq.addChild("ping", "urn:xmpp:ping"); + this.sendIqPacket(iq, null); + } + this.lastPingSent = SystemClock.elapsedRealtime(); + } + + public void setOnMessagePacketReceivedListener( + OnMessagePacketReceived listener) { + this.messageListener = listener; + } + + public void setOnUnregisteredIqPacketReceivedListener( + OnIqPacketReceived listener) { + this.unregisteredIqListener = listener; + } + + public void setOnPresencePacketReceivedListener( + OnPresencePacketReceived listener) { + this.presenceListener = listener; + } + + public void setOnJinglePacketReceivedListener( + OnJinglePacketReceived listener) { + this.jingleListener = listener; + } + + public void setOnStatusChangedListener(OnStatusChanged listener) { + this.statusListener = listener; + } + + public void setOnBindListener(OnBindListener listener) { + this.bindListener = listener; + } + + public void setOnMessageAcknowledgeListener(OnMessageAcknowledged listener) { + this.acknowledgedListener = listener; + } + + public void disconnect(boolean force) { + Log.d(Config.LOGTAG, account.getJid() + ": disconnecting"); + try { + if (force) { + socket.close(); + return; + } + new Thread(new Runnable() { + + @Override + public void run() { + if (tagWriter.isActive()) { + tagWriter.finish(); + try { + while (!tagWriter.finished()) { + Log.d(Config.LOGTAG, "not yet finished"); + Thread.sleep(100); + } + tagWriter.writeTag(Tag.end("stream:stream")); + socket.close(); + } catch (IOException e) { + Log.d(Config.LOGTAG, + "io exception during disconnect"); + } catch (InterruptedException e) { + Log.d(Config.LOGTAG, "interrupted"); + } + } + } + }).start(); + } catch (IOException e) { + Log.d(Config.LOGTAG, "io exception during disconnect"); + } + } + + public List findDiscoItemsByFeature(String feature) { + List items = new ArrayList(); + for (Entry> cursor : disco.entrySet()) { + if (cursor.getValue().contains(feature)) { + items.add(cursor.getKey()); + } + } + return items; + } + + public String findDiscoItemByFeature(String feature) { + List items = findDiscoItemsByFeature(feature); + if (items.size() >= 1) { + return items.get(0); + } + return null; + } + + public void r() { + this.tagWriter.writeStanzaAsync(new RequestPacket(smVersion)); + } + + public String getMucServer() { + return findDiscoItemByFeature("http://jabber.org/protocol/muc"); + } + + public int getTimeToNextAttempt() { + int interval = (int) (25 * Math.pow(1.5, attempt)); + int secondsSinceLast = (int) ((SystemClock.elapsedRealtime() - this.lastConnect) / 1000); + return interval - secondsSinceLast; + } + + public int getAttempt() { + return this.attempt; + } + + public Features getFeatures() { + return this.features; + } + + public class Features { + XmppConnection connection; + + public Features(XmppConnection connection) { + this.connection = connection; + } + + private boolean hasDiscoFeature(String server, String feature) { + if (!connection.disco.containsKey(server)) { + return false; + } + return connection.disco.get(server).contains(feature); + } + + public boolean carbons() { + return hasDiscoFeature(account.getServer(), "urn:xmpp:carbons:2"); + } + + public boolean sm() { + return streamId != null; + } + + public boolean csi() { + if (connection.streamFeatures == null) { + return false; + } else { + return connection.streamFeatures.hasChild("csi", + "urn:xmpp:csi:0"); + } + } + + public boolean pubsub() { + return hasDiscoFeature(account.getServer(), + "http://jabber.org/protocol/pubsub#publish"); + } + + public boolean rosterVersioning() { + if (connection.streamFeatures == null) { + return false; + } else { + return connection.streamFeatures.hasChild("ver"); + } + } + + public boolean streamhost() { + return connection + .findDiscoItemByFeature("http://jabber.org/protocol/bytestreams") != null; + } + + public boolean compression() { + return connection.usingCompression; + } + } + + public long getLastSessionEstablished() { + long diff; + if (this.lastSessionStarted == 0) { + diff = SystemClock.elapsedRealtime() - this.lastConnect; + } else { + diff = SystemClock.elapsedRealtime() - this.lastSessionStarted; + } + return System.currentTimeMillis() - diff; + } + + public long getLastConnect() { + return this.lastConnect; + } + + public long getLastPingSent() { + return this.lastPingSent; + } + + public long getLastPacketReceived() { + return this.lastPaketReceived; + } + + public void sendActive() { + this.sendPacket(new ActivePacket(), null); + } + + public void sendInactive() { + this.sendPacket(new InactivePacket(), null); + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java new file mode 100644 index 00000000..3e7c7b68 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java @@ -0,0 +1,143 @@ +package eu.siacs.conversations.xmpp.jingle; + +import java.util.ArrayList; +import java.util.List; + +import eu.siacs.conversations.xml.Element; + +public class JingleCandidate { + + public static int TYPE_UNKNOWN; + public static int TYPE_DIRECT = 0; + public static int TYPE_PROXY = 1; + + private boolean ours; + private boolean usedByCounterpart = false; + private String cid; + private String host; + private int port; + private int type; + private String jid; + private int priority; + + public JingleCandidate(String cid, boolean ours) { + this.ours = ours; + this.cid = cid; + } + + public String getCid() { + return cid; + } + + public void setHost(String host) { + this.host = host; + } + + public String getHost() { + return this.host; + } + + public void setJid(String jid) { + this.jid = jid; + } + + public String getJid() { + return this.jid; + } + + public void setPort(int port) { + this.port = port; + } + + public int getPort() { + return this.port; + } + + public void setType(int type) { + this.type = type; + } + + public void setType(String type) { + if ("proxy".equals(type)) { + this.type = TYPE_PROXY; + } else if ("direct".equals(type)) { + this.type = TYPE_DIRECT; + } else { + this.type = TYPE_UNKNOWN; + } + } + + public void setPriority(int i) { + this.priority = i; + } + + public int getPriority() { + return this.priority; + } + + public boolean equals(JingleCandidate other) { + return this.getCid().equals(other.getCid()); + } + + public boolean equalValues(JingleCandidate other) { + return other.getHost().equals(this.getHost()) + && (other.getPort() == this.getPort()); + } + + public boolean isOurs() { + return ours; + } + + public int getType() { + return this.type; + } + + public static List parse(List canditates) { + List parsedCandidates = new ArrayList(); + for (Element c : canditates) { + parsedCandidates.add(JingleCandidate.parse(c)); + } + return parsedCandidates; + } + + public static JingleCandidate parse(Element candidate) { + JingleCandidate parsedCandidate = new JingleCandidate( + candidate.getAttribute("cid"), false); + parsedCandidate.setHost(candidate.getAttribute("host")); + parsedCandidate.setJid(candidate.getAttribute("jid")); + parsedCandidate.setType(candidate.getAttribute("type")); + parsedCandidate.setPriority(Integer.parseInt(candidate + .getAttribute("priority"))); + parsedCandidate + .setPort(Integer.parseInt(candidate.getAttribute("port"))); + return parsedCandidate; + } + + public Element toElement() { + Element element = new Element("candidate"); + element.setAttribute("cid", this.getCid()); + element.setAttribute("host", this.getHost()); + element.setAttribute("port", Integer.toString(this.getPort())); + element.setAttribute("jid", this.getJid()); + element.setAttribute("priority", Integer.toString(this.getPriority())); + if (this.getType() == TYPE_DIRECT) { + element.setAttribute("type", "direct"); + } else if (this.getType() == TYPE_PROXY) { + element.setAttribute("type", "proxy"); + } + return element; + } + + public void flagAsUsedByCounterpart() { + this.usedByCounterpart = true; + } + + public boolean isUsedByCounterpart() { + return this.usedByCounterpart; + } + + public String toString() { + return this.getHost() + ":" + this.getPort() + " (prio=" + + this.getPriority() + ")"; + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java new file mode 100644 index 00000000..a0b2feb2 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java @@ -0,0 +1,910 @@ +package eu.siacs.conversations.xmpp.jingle; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; + +import android.content.Intent; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.util.Log; +import eu.siacs.conversations.Config; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Downloadable; +import eu.siacs.conversations.entities.DownloadableFile; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.OnIqPacketReceived; +import eu.siacs.conversations.xmpp.jingle.stanzas.Content; +import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; +import eu.siacs.conversations.xmpp.jingle.stanzas.Reason; +import eu.siacs.conversations.xmpp.stanzas.IqPacket; + +public class JingleConnection implements Downloadable { + + private final String[] extensions = { "webp", "jpeg", "jpg", "png" }; + private final String[] cryptoExtensions = { "pgp", "gpg", "otr" }; + + private JingleConnectionManager mJingleConnectionManager; + private XmppConnectionService mXmppConnectionService; + + protected static final int JINGLE_STATUS_INITIATED = 0; + protected static final int JINGLE_STATUS_ACCEPTED = 1; + protected static final int JINGLE_STATUS_TERMINATED = 2; + protected static final int JINGLE_STATUS_CANCELED = 3; + protected static final int JINGLE_STATUS_FINISHED = 4; + protected static final int JINGLE_STATUS_TRANSMITTING = 5; + protected static final int JINGLE_STATUS_FAILED = 99; + + private int ibbBlockSize = 4096; + + private int mJingleStatus = -1; + private int mStatus = -1; + private Message message; + private String sessionId; + private Account account; + private String initiator; + private String responder; + private List candidates = new ArrayList(); + private ConcurrentHashMap connections = new ConcurrentHashMap(); + + private String transportId; + private Element fileOffer; + private DownloadableFile file = null; + + private String contentName; + private String contentCreator; + + private boolean receivedCandidate = false; + private boolean sentCandidate = false; + + private boolean acceptedAutomatically = false; + + private JingleTransport transport = null; + + private OnIqPacketReceived responseListener = new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (packet.getType() == IqPacket.TYPE_ERROR) { + if (initiator.equals(account.getFullJid())) { + mXmppConnectionService.markMessage(message, + Message.STATUS_SEND_FAILED); + } + mJingleStatus = JINGLE_STATUS_FAILED; + } + } + }; + + final OnFileTransmissionStatusChanged onFileTransmissionSatusChanged = new OnFileTransmissionStatusChanged() { + + @Override + public void onFileTransmitted(DownloadableFile file) { + if (responder.equals(account.getFullJid())) { + sendSuccess(); + if (acceptedAutomatically) { + message.markUnread(); + JingleConnection.this.mXmppConnectionService + .getNotificationService().push(message); + } + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFile(file.getAbsolutePath(), options); + int imageHeight = options.outHeight; + int imageWidth = options.outWidth; + message.setBody(Long.toString(file.getSize()) + ',' + + imageWidth + ',' + imageHeight); + mXmppConnectionService.databaseBackend.createMessage(message); + mXmppConnectionService.markMessage(message, + Message.STATUS_RECEIVED); + } + Log.d(Config.LOGTAG, + "sucessfully transmitted file:" + file.getAbsolutePath()); + if (message.getEncryption() != Message.ENCRYPTION_PGP) { + Intent intent = new Intent( + Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); + intent.setData(Uri.fromFile(file)); + mXmppConnectionService.sendBroadcast(intent); + } + } + + @Override + public void onFileTransferAborted() { + JingleConnection.this.sendCancel(); + JingleConnection.this.cancel(); + } + }; + + private OnProxyActivated onProxyActivated = new OnProxyActivated() { + + @Override + public void success() { + if (initiator.equals(account.getFullJid())) { + Log.d(Config.LOGTAG, "we were initiating. sending file"); + transport.send(file, onFileTransmissionSatusChanged); + } else { + transport.receive(file, onFileTransmissionSatusChanged); + Log.d(Config.LOGTAG, "we were responding. receiving file"); + } + } + + @Override + public void failed() { + Log.d(Config.LOGTAG, "proxy activation failed"); + } + }; + + public JingleConnection(JingleConnectionManager mJingleConnectionManager) { + this.mJingleConnectionManager = mJingleConnectionManager; + this.mXmppConnectionService = mJingleConnectionManager + .getXmppConnectionService(); + } + + public String getSessionId() { + return this.sessionId; + } + + public Account getAccount() { + return this.account; + } + + public String getCounterPart() { + return this.message.getCounterpart(); + } + + public void deliverPacket(JinglePacket packet) { + boolean returnResult = true; + if (packet.isAction("session-terminate")) { + Reason reason = packet.getReason(); + if (reason != null) { + if (reason.hasChild("cancel")) { + this.cancel(); + } else if (reason.hasChild("success")) { + this.receiveSuccess(); + } else { + this.cancel(); + } + } else { + this.cancel(); + } + } else if (packet.isAction("session-accept")) { + returnResult = receiveAccept(packet); + } else if (packet.isAction("transport-info")) { + returnResult = receiveTransportInfo(packet); + } else if (packet.isAction("transport-replace")) { + if (packet.getJingleContent().hasIbbTransport()) { + returnResult = this.receiveFallbackToIbb(packet); + } else { + returnResult = false; + Log.d(Config.LOGTAG, "trying to fallback to something unknown" + + packet.toString()); + } + } else if (packet.isAction("transport-accept")) { + returnResult = this.receiveTransportAccept(packet); + } else { + Log.d(Config.LOGTAG, "packet arrived in connection. action was " + + packet.getAction()); + returnResult = false; + } + IqPacket response; + if (returnResult) { + response = packet.generateRespone(IqPacket.TYPE_RESULT); + + } else { + response = packet.generateRespone(IqPacket.TYPE_ERROR); + } + account.getXmppConnection().sendIqPacket(response, null); + } + + public void init(Message message) { + this.contentCreator = "initiator"; + this.contentName = this.mJingleConnectionManager.nextRandomId(); + this.message = message; + this.account = message.getConversation().getAccount(); + this.initiator = this.account.getFullJid(); + this.responder = this.message.getCounterpart(); + this.sessionId = this.mJingleConnectionManager.nextRandomId(); + if (this.candidates.size() > 0) { + this.sendInitRequest(); + } else { + this.mJingleConnectionManager.getPrimaryCandidate(account, + new OnPrimaryCandidateFound() { + + @Override + public void onPrimaryCandidateFound(boolean success, + final JingleCandidate candidate) { + if (success) { + final JingleSocks5Transport socksConnection = new JingleSocks5Transport( + JingleConnection.this, candidate); + connections.put(candidate.getCid(), + socksConnection); + socksConnection + .connect(new OnTransportConnected() { + + @Override + public void failed() { + Log.d(Config.LOGTAG, + "connection to our own primary candidete failed"); + sendInitRequest(); + } + + @Override + public void established() { + Log.d(Config.LOGTAG, + "succesfully connected to our own primary candidate"); + mergeCandidate(candidate); + sendInitRequest(); + } + }); + mergeCandidate(candidate); + } else { + Log.d(Config.LOGTAG, + "no primary candidate of our own was found"); + sendInitRequest(); + } + } + }); + } + + } + + public void init(Account account, JinglePacket packet) { + this.mJingleStatus = JINGLE_STATUS_INITIATED; + Conversation conversation = this.mXmppConnectionService + .findOrCreateConversation(account, + packet.getFrom().split("/", 2)[0], false); + this.message = new Message(conversation, "", Message.ENCRYPTION_NONE); + this.message.setStatus(Message.STATUS_RECEIVED); + this.message.setType(Message.TYPE_IMAGE); + this.mStatus = Downloadable.STATUS_OFFER; + this.message.setDownloadable(this); + String[] fromParts = packet.getFrom().split("/", 2); + this.message.setPresence(fromParts[1]); + this.account = account; + this.initiator = packet.getFrom(); + this.responder = this.account.getFullJid(); + this.sessionId = packet.getSessionId(); + Content content = packet.getJingleContent(); + this.contentCreator = content.getAttribute("creator"); + this.contentName = content.getAttribute("name"); + this.transportId = content.getTransportId(); + this.mergeCandidates(JingleCandidate.parse(content.socks5transport() + .getChildren())); + this.fileOffer = packet.getJingleContent().getFileOffer(); + if (fileOffer != null) { + Element fileSize = fileOffer.findChild("size"); + Element fileNameElement = fileOffer.findChild("name"); + if (fileNameElement != null) { + boolean supportedFile = false; + String[] filename = fileNameElement.getContent() + .toLowerCase(Locale.US).split("\\."); + if (Arrays.asList(this.extensions).contains( + filename[filename.length - 1])) { + supportedFile = true; + } else if (Arrays.asList(this.cryptoExtensions).contains( + filename[filename.length - 1])) { + if (filename.length == 3) { + if (Arrays.asList(this.extensions).contains( + filename[filename.length - 2])) { + supportedFile = true; + if (filename[filename.length - 1].equals("otr")) { + Log.d(Config.LOGTAG, "receiving otr file"); + this.message + .setEncryption(Message.ENCRYPTION_OTR); + } else { + this.message + .setEncryption(Message.ENCRYPTION_PGP); + } + } + } + } + if (supportedFile) { + long size = Long.parseLong(fileSize.getContent()); + message.setBody(Long.toString(size)); + conversation.add(message); + mXmppConnectionService.updateConversationUi(); + if (size <= this.mJingleConnectionManager + .getAutoAcceptFileSize()) { + Log.d(Config.LOGTAG, "auto accepting file from " + + packet.getFrom()); + this.acceptedAutomatically = true; + this.sendAccept(); + } else { + message.markUnread(); + Log.d(Config.LOGTAG, + "not auto accepting new file offer with size: " + + size + + " allowed size:" + + this.mJingleConnectionManager + .getAutoAcceptFileSize()); + this.mXmppConnectionService.getNotificationService() + .push(message); + } + this.file = this.mXmppConnectionService.getFileBackend() + .getFile(message, false); + if (message.getEncryption() == Message.ENCRYPTION_OTR) { + byte[] key = conversation.getSymmetricKey(); + if (key == null) { + this.sendCancel(); + this.cancel(); + return; + } else { + this.file.setKey(key); + } + } + this.file.setExpectedSize(size); + } else { + this.sendCancel(); + this.cancel(); + } + } else { + this.sendCancel(); + this.cancel(); + } + } else { + this.sendCancel(); + this.cancel(); + } + } + + private void sendInitRequest() { + JinglePacket packet = this.bootstrapPacket("session-initiate"); + Content content = new Content(this.contentCreator, this.contentName); + if (message.getType() == Message.TYPE_IMAGE) { + content.setTransportId(this.transportId); + this.file = this.mXmppConnectionService.getFileBackend().getFile( + message, false); + if (message.getEncryption() == Message.ENCRYPTION_OTR) { + Conversation conversation = this.message.getConversation(); + this.mXmppConnectionService.renewSymmetricKey(conversation); + content.setFileOffer(this.file, true); + this.file.setKey(conversation.getSymmetricKey()); + } else { + content.setFileOffer(this.file, false); + } + this.transportId = this.mJingleConnectionManager.nextRandomId(); + content.setTransportId(this.transportId); + content.socks5transport().setChildren(getCandidatesAsElements()); + packet.setContent(content); + this.sendJinglePacket(packet); + this.mJingleStatus = JINGLE_STATUS_INITIATED; + } + } + + private List getCandidatesAsElements() { + List elements = new ArrayList(); + for (JingleCandidate c : this.candidates) { + elements.add(c.toElement()); + } + return elements; + } + + private void sendAccept() { + mJingleStatus = JINGLE_STATUS_ACCEPTED; + this.mStatus = Downloadable.STATUS_DOWNLOADING; + mXmppConnectionService.updateConversationUi(); + this.mJingleConnectionManager.getPrimaryCandidate(this.account, + new OnPrimaryCandidateFound() { + + @Override + public void onPrimaryCandidateFound(boolean success, + final JingleCandidate candidate) { + final JinglePacket packet = bootstrapPacket("session-accept"); + final Content content = new Content(contentCreator, + contentName); + content.setFileOffer(fileOffer); + content.setTransportId(transportId); + if ((success) && (!equalCandidateExists(candidate))) { + final JingleSocks5Transport socksConnection = new JingleSocks5Transport( + JingleConnection.this, candidate); + connections.put(candidate.getCid(), socksConnection); + socksConnection.connect(new OnTransportConnected() { + + @Override + public void failed() { + Log.d(Config.LOGTAG, + "connection to our own primary candidate failed"); + content.socks5transport().setChildren( + getCandidatesAsElements()); + packet.setContent(content); + sendJinglePacket(packet); + connectNextCandidate(); + } + + @Override + public void established() { + Log.d(Config.LOGTAG, + "connected to primary candidate"); + mergeCandidate(candidate); + content.socks5transport().setChildren( + getCandidatesAsElements()); + packet.setContent(content); + sendJinglePacket(packet); + connectNextCandidate(); + } + }); + } else { + Log.d(Config.LOGTAG, + "did not find a primary candidate for ourself"); + content.socks5transport().setChildren( + getCandidatesAsElements()); + packet.setContent(content); + sendJinglePacket(packet); + connectNextCandidate(); + } + } + }); + + } + + private JinglePacket bootstrapPacket(String action) { + JinglePacket packet = new JinglePacket(); + packet.setAction(action); + packet.setFrom(account.getFullJid()); + packet.setTo(this.message.getCounterpart()); + packet.setSessionId(this.sessionId); + packet.setInitiator(this.initiator); + return packet; + } + + private void sendJinglePacket(JinglePacket packet) { + // Log.d(Config.LOGTAG,packet.toString()); + account.getXmppConnection().sendIqPacket(packet, responseListener); + } + + private boolean receiveAccept(JinglePacket packet) { + Content content = packet.getJingleContent(); + mergeCandidates(JingleCandidate.parse(content.socks5transport() + .getChildren())); + this.mJingleStatus = JINGLE_STATUS_ACCEPTED; + mXmppConnectionService.markMessage(message, Message.STATUS_UNSEND); + this.connectNextCandidate(); + return true; + } + + private boolean receiveTransportInfo(JinglePacket packet) { + Content content = packet.getJingleContent(); + if (content.hasSocks5Transport()) { + if (content.socks5transport().hasChild("activated")) { + if ((this.transport != null) + && (this.transport instanceof JingleSocks5Transport)) { + onProxyActivated.success(); + } else { + String cid = content.socks5transport() + .findChild("activated").getAttribute("cid"); + Log.d(Config.LOGTAG, "received proxy activated (" + cid + + ")prior to choosing our own transport"); + JingleSocks5Transport connection = this.connections + .get(cid); + if (connection != null) { + connection.setActivated(true); + } else { + Log.d(Config.LOGTAG, "activated connection not found"); + this.sendCancel(); + this.cancel(); + } + } + return true; + } else if (content.socks5transport().hasChild("proxy-error")) { + onProxyActivated.failed(); + return true; + } else if (content.socks5transport().hasChild("candidate-error")) { + Log.d(Config.LOGTAG, "received candidate error"); + this.receivedCandidate = true; + if ((mJingleStatus == JINGLE_STATUS_ACCEPTED) + && (this.sentCandidate)) { + this.connect(); + } + return true; + } else if (content.socks5transport().hasChild("candidate-used")) { + String cid = content.socks5transport() + .findChild("candidate-used").getAttribute("cid"); + if (cid != null) { + Log.d(Config.LOGTAG, "candidate used by counterpart:" + cid); + JingleCandidate candidate = getCandidate(cid); + candidate.flagAsUsedByCounterpart(); + this.receivedCandidate = true; + if ((mJingleStatus == JINGLE_STATUS_ACCEPTED) + && (this.sentCandidate)) { + this.connect(); + } else { + Log.d(Config.LOGTAG, + "ignoring because file is already in transmission or we havent sent our candidate yet"); + } + return true; + } else { + return false; + } + } else { + return false; + } + } else { + return true; + } + } + + private void connect() { + final JingleSocks5Transport connection = chooseConnection(); + this.transport = connection; + if (connection == null) { + Log.d(Config.LOGTAG, "could not find suitable candidate"); + this.disconnect(); + if (this.initiator.equals(account.getFullJid())) { + this.sendFallbackToIbb(); + } + } else { + this.mJingleStatus = JINGLE_STATUS_TRANSMITTING; + if (connection.needsActivation()) { + if (connection.getCandidate().isOurs()) { + Log.d(Config.LOGTAG, "candidate " + + connection.getCandidate().getCid() + + " was our proxy. going to activate"); + IqPacket activation = new IqPacket(IqPacket.TYPE_SET); + activation.setTo(connection.getCandidate().getJid()); + activation.query("http://jabber.org/protocol/bytestreams") + .setAttribute("sid", this.getSessionId()); + activation.query().addChild("activate") + .setContent(this.getCounterPart()); + this.account.getXmppConnection().sendIqPacket(activation, + new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, + IqPacket packet) { + if (packet.getType() == IqPacket.TYPE_ERROR) { + onProxyActivated.failed(); + } else { + onProxyActivated.success(); + sendProxyActivated(connection + .getCandidate().getCid()); + } + } + }); + } else { + Log.d(Config.LOGTAG, + "candidate " + + connection.getCandidate().getCid() + + " was a proxy. waiting for other party to activate"); + } + } else { + if (initiator.equals(account.getFullJid())) { + Log.d(Config.LOGTAG, "we were initiating. sending file"); + connection.send(file, onFileTransmissionSatusChanged); + } else { + Log.d(Config.LOGTAG, "we were responding. receiving file"); + connection.receive(file, onFileTransmissionSatusChanged); + } + } + } + } + + private JingleSocks5Transport chooseConnection() { + JingleSocks5Transport connection = null; + for (Entry cursor : connections + .entrySet()) { + JingleSocks5Transport currentConnection = cursor.getValue(); + // Log.d(Config.LOGTAG,"comparing candidate: "+currentConnection.getCandidate().toString()); + if (currentConnection.isEstablished() + && (currentConnection.getCandidate().isUsedByCounterpart() || (!currentConnection + .getCandidate().isOurs()))) { + // Log.d(Config.LOGTAG,"is usable"); + if (connection == null) { + connection = currentConnection; + } else { + if (connection.getCandidate().getPriority() < currentConnection + .getCandidate().getPriority()) { + connection = currentConnection; + } else if (connection.getCandidate().getPriority() == currentConnection + .getCandidate().getPriority()) { + // Log.d(Config.LOGTAG,"found two candidates with same priority"); + if (initiator.equals(account.getFullJid())) { + if (currentConnection.getCandidate().isOurs()) { + connection = currentConnection; + } + } else { + if (!currentConnection.getCandidate().isOurs()) { + connection = currentConnection; + } + } + } + } + } + } + return connection; + } + + private void sendSuccess() { + JinglePacket packet = bootstrapPacket("session-terminate"); + Reason reason = new Reason(); + reason.addChild("success"); + packet.setReason(reason); + this.sendJinglePacket(packet); + this.disconnect(); + this.mJingleStatus = JINGLE_STATUS_FINISHED; + this.message.setStatus(Message.STATUS_RECEIVED); + this.message.setDownloadable(null); + this.mXmppConnectionService.updateMessage(message); + this.mJingleConnectionManager.finishConnection(this); + } + + private void sendFallbackToIbb() { + Log.d(Config.LOGTAG, "sending fallback to ibb"); + JinglePacket packet = this.bootstrapPacket("transport-replace"); + Content content = new Content(this.contentCreator, this.contentName); + this.transportId = this.mJingleConnectionManager.nextRandomId(); + content.setTransportId(this.transportId); + content.ibbTransport().setAttribute("block-size", + Integer.toString(this.ibbBlockSize)); + packet.setContent(content); + this.sendJinglePacket(packet); + } + + private boolean receiveFallbackToIbb(JinglePacket packet) { + Log.d(Config.LOGTAG, "receiving fallack to ibb"); + String receivedBlockSize = packet.getJingleContent().ibbTransport() + .getAttribute("block-size"); + if (receivedBlockSize != null) { + int bs = Integer.parseInt(receivedBlockSize); + if (bs > this.ibbBlockSize) { + this.ibbBlockSize = bs; + } + } + this.transportId = packet.getJingleContent().getTransportId(); + this.transport = new JingleInbandTransport(this.account, + this.responder, this.transportId, this.ibbBlockSize); + this.transport.receive(file, onFileTransmissionSatusChanged); + JinglePacket answer = bootstrapPacket("transport-accept"); + Content content = new Content("initiator", "a-file-offer"); + content.setTransportId(this.transportId); + content.ibbTransport().setAttribute("block-size", + Integer.toString(this.ibbBlockSize)); + answer.setContent(content); + this.sendJinglePacket(answer); + return true; + } + + private boolean receiveTransportAccept(JinglePacket packet) { + if (packet.getJingleContent().hasIbbTransport()) { + String receivedBlockSize = packet.getJingleContent().ibbTransport() + .getAttribute("block-size"); + if (receivedBlockSize != null) { + int bs = Integer.parseInt(receivedBlockSize); + if (bs > this.ibbBlockSize) { + this.ibbBlockSize = bs; + } + } + this.transport = new JingleInbandTransport(this.account, + this.responder, this.transportId, this.ibbBlockSize); + this.transport.connect(new OnTransportConnected() { + + @Override + public void failed() { + Log.d(Config.LOGTAG, "ibb open failed"); + } + + @Override + public void established() { + JingleConnection.this.transport.send(file, + onFileTransmissionSatusChanged); + } + }); + return true; + } else { + return false; + } + } + + private void receiveSuccess() { + this.mJingleStatus = JINGLE_STATUS_FINISHED; + this.mXmppConnectionService.markMessage(this.message, + Message.STATUS_SEND); + this.disconnect(); + this.mJingleConnectionManager.finishConnection(this); + } + + public void cancel() { + this.mJingleStatus = JINGLE_STATUS_CANCELED; + this.disconnect(); + if (this.message != null) { + if (this.responder.equals(account.getFullJid())) { + this.mStatus = Downloadable.STATUS_FAILED; + this.mXmppConnectionService.updateConversationUi(); + } else { + if (this.mJingleStatus == JINGLE_STATUS_INITIATED) { + this.mXmppConnectionService.markMessage(this.message, + Message.STATUS_SEND_REJECTED); + } else { + this.mXmppConnectionService.markMessage(this.message, + Message.STATUS_SEND_FAILED); + } + } + } + this.mJingleConnectionManager.finishConnection(this); + } + + private void sendCancel() { + JinglePacket packet = bootstrapPacket("session-terminate"); + Reason reason = new Reason(); + reason.addChild("cancel"); + packet.setReason(reason); + this.sendJinglePacket(packet); + } + + private void connectNextCandidate() { + for (JingleCandidate candidate : this.candidates) { + if ((!connections.containsKey(candidate.getCid()) && (!candidate + .isOurs()))) { + this.connectWithCandidate(candidate); + return; + } + } + this.sendCandidateError(); + } + + private void connectWithCandidate(final JingleCandidate candidate) { + final JingleSocks5Transport socksConnection = new JingleSocks5Transport( + this, candidate); + connections.put(candidate.getCid(), socksConnection); + socksConnection.connect(new OnTransportConnected() { + + @Override + public void failed() { + Log.d(Config.LOGTAG, + "connection failed with " + candidate.getHost() + ":" + + candidate.getPort()); + connectNextCandidate(); + } + + @Override + public void established() { + Log.d(Config.LOGTAG, + "established connection with " + candidate.getHost() + + ":" + candidate.getPort()); + sendCandidateUsed(candidate.getCid()); + } + }); + } + + private void disconnect() { + Iterator> it = this.connections + .entrySet().iterator(); + while (it.hasNext()) { + Entry pairs = it.next(); + pairs.getValue().disconnect(); + it.remove(); + } + } + + private void sendProxyActivated(String cid) { + JinglePacket packet = bootstrapPacket("transport-info"); + Content content = new Content(this.contentCreator, this.contentName); + content.setTransportId(this.transportId); + content.socks5transport().addChild("activated") + .setAttribute("cid", cid); + packet.setContent(content); + this.sendJinglePacket(packet); + } + + private void sendCandidateUsed(final String cid) { + JinglePacket packet = bootstrapPacket("transport-info"); + Content content = new Content(this.contentCreator, this.contentName); + content.setTransportId(this.transportId); + content.socks5transport().addChild("candidate-used") + .setAttribute("cid", cid); + packet.setContent(content); + this.sentCandidate = true; + if ((receivedCandidate) && (mJingleStatus == JINGLE_STATUS_ACCEPTED)) { + connect(); + } + this.sendJinglePacket(packet); + } + + private void sendCandidateError() { + JinglePacket packet = bootstrapPacket("transport-info"); + Content content = new Content(this.contentCreator, this.contentName); + content.setTransportId(this.transportId); + content.socks5transport().addChild("candidate-error"); + packet.setContent(content); + this.sentCandidate = true; + if ((receivedCandidate) && (mJingleStatus == JINGLE_STATUS_ACCEPTED)) { + connect(); + } + this.sendJinglePacket(packet); + } + + public String getInitiator() { + return this.initiator; + } + + public String getResponder() { + return this.responder; + } + + public int getJingleStatus() { + return this.mJingleStatus; + } + + private boolean equalCandidateExists(JingleCandidate candidate) { + for (JingleCandidate c : this.candidates) { + if (c.equalValues(candidate)) { + return true; + } + } + return false; + } + + private void mergeCandidate(JingleCandidate candidate) { + for (JingleCandidate c : this.candidates) { + if (c.equals(candidate)) { + return; + } + } + this.candidates.add(candidate); + } + + private void mergeCandidates(List candidates) { + for (JingleCandidate c : candidates) { + mergeCandidate(c); + } + } + + private JingleCandidate getCandidate(String cid) { + for (JingleCandidate c : this.candidates) { + if (c.getCid().equals(cid)) { + return c; + } + } + return null; + } + + interface OnProxyActivated { + public void success(); + + public void failed(); + } + + public boolean hasTransportId(String sid) { + return sid.equals(this.transportId); + } + + public JingleTransport getTransport() { + return this.transport; + } + + public boolean start() { + if (account.getStatus() == Account.STATUS_ONLINE) { + if (mJingleStatus == JINGLE_STATUS_INITIATED) { + new Thread(new Runnable() { + + @Override + public void run() { + sendAccept(); + } + }).start(); + } + return true; + } else { + return false; + } + } + + @Override + public int getStatus() { + return this.mStatus; + } + + @Override + public long getFileSize() { + if (this.file != null) { + return this.file.getExpectedSize(); + } else { + return 0; + } + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java new file mode 100644 index 00000000..1e7c84d4 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -0,0 +1,163 @@ +package eu.siacs.conversations.xmpp.jingle; + +import java.math.BigInteger; +import java.security.SecureRandom; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import android.annotation.SuppressLint; +import android.util.Log; +import eu.siacs.conversations.Config; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.services.AbstractConnectionManager; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.OnIqPacketReceived; +import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; +import eu.siacs.conversations.xmpp.stanzas.IqPacket; + +public class JingleConnectionManager extends AbstractConnectionManager { + private List connections = new CopyOnWriteArrayList(); + + private HashMap primaryCandidates = new HashMap(); + + @SuppressLint("TrulyRandom") + private SecureRandom random = new SecureRandom(); + + public JingleConnectionManager(XmppConnectionService service) { + super(service); + } + + public void deliverPacket(Account account, JinglePacket packet) { + if (packet.isAction("session-initiate")) { + JingleConnection connection = new JingleConnection(this); + connection.init(account, packet); + connections.add(connection); + } else { + for (JingleConnection connection : connections) { + if (connection.getAccount() == account + && connection.getSessionId().equals( + packet.getSessionId()) + && connection.getCounterPart().equals(packet.getFrom())) { + connection.deliverPacket(packet); + return; + } + } + account.getXmppConnection().sendIqPacket( + packet.generateRespone(IqPacket.TYPE_ERROR), null); + } + } + + public JingleConnection createNewConnection(Message message) { + JingleConnection connection = new JingleConnection(this); + connection.init(message); + this.connections.add(connection); + return connection; + } + + public JingleConnection createNewConnection(JinglePacket packet) { + JingleConnection connection = new JingleConnection(this); + this.connections.add(connection); + return connection; + } + + public void finishConnection(JingleConnection connection) { + this.connections.remove(connection); + } + + public void getPrimaryCandidate(Account account, + final OnPrimaryCandidateFound listener) { + if (!this.primaryCandidates.containsKey(account.getJid())) { + String xmlns = "http://jabber.org/protocol/bytestreams"; + final String proxy = account.getXmppConnection() + .findDiscoItemByFeature(xmlns); + if (proxy != null) { + IqPacket iq = new IqPacket(IqPacket.TYPE_GET); + iq.setTo(proxy); + iq.query(xmlns); + account.getXmppConnection().sendIqPacket(iq, + new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, + IqPacket packet) { + Element streamhost = packet + .query() + .findChild("streamhost", + "http://jabber.org/protocol/bytestreams"); + if (streamhost != null) { + JingleCandidate candidate = new JingleCandidate( + nextRandomId(), true); + candidate.setHost(streamhost + .getAttribute("host")); + candidate.setPort(Integer + .parseInt(streamhost + .getAttribute("port"))); + candidate + .setType(JingleCandidate.TYPE_PROXY); + candidate.setJid(proxy); + candidate.setPriority(655360 + 65535); + primaryCandidates.put(account.getJid(), + candidate); + listener.onPrimaryCandidateFound(true, + candidate); + } else { + listener.onPrimaryCandidateFound(false, + null); + } + } + }); + } else { + listener.onPrimaryCandidateFound(false, null); + } + + } else { + listener.onPrimaryCandidateFound(true, + this.primaryCandidates.get(account.getJid())); + } + } + + public String nextRandomId() { + return new BigInteger(50, random).toString(32); + } + + public void deliverIbbPacket(Account account, IqPacket packet) { + String sid = null; + Element payload = null; + if (packet.hasChild("open", "http://jabber.org/protocol/ibb")) { + payload = packet + .findChild("open", "http://jabber.org/protocol/ibb"); + sid = payload.getAttribute("sid"); + } else if (packet.hasChild("data", "http://jabber.org/protocol/ibb")) { + payload = packet + .findChild("data", "http://jabber.org/protocol/ibb"); + sid = payload.getAttribute("sid"); + } + if (sid != null) { + for (JingleConnection connection : connections) { + if (connection.getAccount() == account + && connection.hasTransportId(sid)) { + JingleTransport transport = connection.getTransport(); + if (transport instanceof JingleInbandTransport) { + JingleInbandTransport inbandTransport = (JingleInbandTransport) transport; + inbandTransport.deliverPayload(packet, payload); + return; + } + } + } + Log.d(Config.LOGTAG, + "couldnt deliver payload: " + payload.toString()); + } else { + Log.d(Config.LOGTAG, "no sid found in incomming ibb packet"); + } + } + + public void cancelInTransmission() { + for (JingleConnection connection : this.connections) { + if (connection.getJingleStatus() == JingleConnection.JINGLE_STATUS_TRANSMITTING) { + connection.cancel(); + } + } + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java new file mode 100644 index 00000000..cc1e92f6 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java @@ -0,0 +1,191 @@ +package eu.siacs.conversations.xmpp.jingle; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; + +import android.util.Base64; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.DownloadableFile; +import eu.siacs.conversations.utils.CryptoHelper; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.OnIqPacketReceived; +import eu.siacs.conversations.xmpp.stanzas.IqPacket; + +public class JingleInbandTransport extends JingleTransport { + + private Account account; + private String counterpart; + private int blockSize; + private int bufferSize; + private int seq = 0; + private String sessionId; + + private boolean established = false; + + private DownloadableFile file; + + private InputStream fileInputStream = null; + private OutputStream fileOutputStream; + private long remainingSize; + private MessageDigest digest; + + private OnFileTransmissionStatusChanged onFileTransmissionStatusChanged; + + private OnIqPacketReceived onAckReceived = new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (packet.getType() == IqPacket.TYPE_RESULT) { + sendNextBlock(); + } + } + }; + + public JingleInbandTransport(Account account, String counterpart, + String sid, int blocksize) { + this.account = account; + this.counterpart = counterpart; + this.blockSize = blocksize; + this.bufferSize = blocksize / 4; + this.sessionId = sid; + } + + public void connect(final OnTransportConnected callback) { + IqPacket iq = new IqPacket(IqPacket.TYPE_SET); + iq.setTo(this.counterpart); + Element open = iq.addChild("open", "http://jabber.org/protocol/ibb"); + open.setAttribute("sid", this.sessionId); + open.setAttribute("stanza", "iq"); + open.setAttribute("block-size", Integer.toString(this.blockSize)); + + this.account.getXmppConnection().sendIqPacket(iq, + new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, + IqPacket packet) { + if (packet.getType() == IqPacket.TYPE_ERROR) { + callback.failed(); + } else { + callback.established(); + } + } + }); + } + + @Override + public void receive(DownloadableFile file, + OnFileTransmissionStatusChanged callback) { + this.onFileTransmissionStatusChanged = callback; + this.file = file; + try { + this.digest = MessageDigest.getInstance("SHA-1"); + digest.reset(); + file.getParentFile().mkdirs(); + file.createNewFile(); + this.fileOutputStream = file.createOutputStream(); + if (this.fileOutputStream == null) { + callback.onFileTransferAborted(); + return; + } + this.remainingSize = file.getExpectedSize(); + } catch (NoSuchAlgorithmException e) { + callback.onFileTransferAborted(); + } catch (IOException e) { + callback.onFileTransferAborted(); + } + } + + @Override + public void send(DownloadableFile file, + OnFileTransmissionStatusChanged callback) { + this.onFileTransmissionStatusChanged = callback; + this.file = file; + try { + this.digest = MessageDigest.getInstance("SHA-1"); + this.digest.reset(); + fileInputStream = this.file.createInputStream(); + if (fileInputStream == null) { + callback.onFileTransferAborted(); + return; + } + this.sendNextBlock(); + } catch (NoSuchAlgorithmException e) { + callback.onFileTransferAborted(); + } + } + + private void sendNextBlock() { + byte[] buffer = new byte[this.bufferSize]; + try { + int count = fileInputStream.read(buffer); + if (count == -1) { + file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest())); + fileInputStream.close(); + this.onFileTransmissionStatusChanged.onFileTransmitted(file); + } else { + this.digest.update(buffer); + String base64 = Base64.encodeToString(buffer, Base64.NO_WRAP); + IqPacket iq = new IqPacket(IqPacket.TYPE_SET); + iq.setTo(this.counterpart); + Element data = iq.addChild("data", + "http://jabber.org/protocol/ibb"); + data.setAttribute("seq", Integer.toString(this.seq)); + data.setAttribute("block-size", + Integer.toString(this.blockSize)); + data.setAttribute("sid", this.sessionId); + data.setContent(base64); + this.account.getXmppConnection().sendIqPacket(iq, + this.onAckReceived); + this.seq++; + } + } catch (IOException e) { + this.onFileTransmissionStatusChanged.onFileTransferAborted(); + } + } + + private void receiveNextBlock(String data) { + try { + byte[] buffer = Base64.decode(data, Base64.NO_WRAP); + if (this.remainingSize < buffer.length) { + buffer = Arrays + .copyOfRange(buffer, 0, (int) this.remainingSize); + } + this.remainingSize -= buffer.length; + + this.fileOutputStream.write(buffer); + + this.digest.update(buffer); + if (this.remainingSize <= 0) { + file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest())); + fileOutputStream.flush(); + fileOutputStream.close(); + this.onFileTransmissionStatusChanged.onFileTransmitted(file); + } + } catch (IOException e) { + this.onFileTransmissionStatusChanged.onFileTransferAborted(); + } + } + + public void deliverPayload(IqPacket packet, Element payload) { + if (payload.getName().equals("open")) { + if (!established) { + established = true; + this.account.getXmppConnection().sendIqPacket( + packet.generateRespone(IqPacket.TYPE_RESULT), null); + } else { + this.account.getXmppConnection().sendIqPacket( + packet.generateRespone(IqPacket.TYPE_ERROR), null); + } + } else if (payload.getName().equals("data")) { + this.receiveNextBlock(payload.getContent()); + this.account.getXmppConnection().sendIqPacket( + packet.generateRespone(IqPacket.TYPE_RESULT), null); + } else { + // TODO some sort of exception + } + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java new file mode 100644 index 00000000..1da2f0cd --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java @@ -0,0 +1,212 @@ +package eu.siacs.conversations.xmpp.jingle; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; +import java.net.UnknownHostException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; + +import eu.siacs.conversations.entities.DownloadableFile; +import eu.siacs.conversations.utils.CryptoHelper; + +public class JingleSocks5Transport extends JingleTransport { + private JingleCandidate candidate; + private String destination; + private OutputStream outputStream; + private InputStream inputStream; + private boolean isEstablished = false; + private boolean activated = false; + protected Socket socket; + + public JingleSocks5Transport(JingleConnection jingleConnection, + JingleCandidate candidate) { + this.candidate = candidate; + try { + MessageDigest mDigest = MessageDigest.getInstance("SHA-1"); + StringBuilder destBuilder = new StringBuilder(); + destBuilder.append(jingleConnection.getSessionId()); + if (candidate.isOurs()) { + destBuilder.append(jingleConnection.getAccount().getFullJid()); + destBuilder.append(jingleConnection.getCounterPart()); + } else { + destBuilder.append(jingleConnection.getCounterPart()); + destBuilder.append(jingleConnection.getAccount().getFullJid()); + } + mDigest.reset(); + this.destination = CryptoHelper.bytesToHex(mDigest + .digest(destBuilder.toString().getBytes())); + } catch (NoSuchAlgorithmException e) { + + } + } + + public void connect(final OnTransportConnected callback) { + new Thread(new Runnable() { + + @Override + public void run() { + try { + socket = new Socket(candidate.getHost(), + candidate.getPort()); + inputStream = socket.getInputStream(); + outputStream = socket.getOutputStream(); + byte[] login = { 0x05, 0x01, 0x00 }; + byte[] expectedReply = { 0x05, 0x00 }; + byte[] reply = new byte[2]; + outputStream.write(login); + inputStream.read(reply); + final String connect = Character.toString('\u0005') + + '\u0001' + '\u0000' + '\u0003' + '\u0028' + + destination + '\u0000' + '\u0000'; + if (Arrays.equals(reply, expectedReply)) { + outputStream.write(connect.getBytes()); + byte[] result = new byte[2]; + inputStream.read(result); + int status = result[1]; + if (status == 0) { + isEstablished = true; + callback.established(); + } else { + callback.failed(); + } + } else { + socket.close(); + callback.failed(); + } + } catch (UnknownHostException e) { + callback.failed(); + } catch (IOException e) { + callback.failed(); + } + } + }).start(); + + } + + public void send(final DownloadableFile file, + final OnFileTransmissionStatusChanged callback) { + new Thread(new Runnable() { + + @Override + public void run() { + InputStream fileInputStream = null; + try { + MessageDigest digest = MessageDigest.getInstance("SHA-1"); + digest.reset(); + fileInputStream = file.createInputStream(); + if (fileInputStream == null) { + callback.onFileTransferAborted(); + return; + } + int count; + byte[] buffer = new byte[8192]; + while ((count = fileInputStream.read(buffer)) > 0) { + outputStream.write(buffer, 0, count); + digest.update(buffer, 0, count); + } + outputStream.flush(); + file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest())); + if (callback != null) { + callback.onFileTransmitted(file); + } + } catch (FileNotFoundException e) { + callback.onFileTransferAborted(); + } catch (IOException e) { + callback.onFileTransferAborted(); + } catch (NoSuchAlgorithmException e) { + callback.onFileTransferAborted(); + } finally { + try { + if (fileInputStream != null) { + fileInputStream.close(); + } + } catch (IOException e) { + callback.onFileTransferAborted(); + } + } + } + }).start(); + + } + + public void receive(final DownloadableFile file, + final OnFileTransmissionStatusChanged callback) { + new Thread(new Runnable() { + + @Override + public void run() { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-1"); + digest.reset(); + inputStream.skip(45); + socket.setSoTimeout(30000); + file.getParentFile().mkdirs(); + file.createNewFile(); + OutputStream fileOutputStream = file.createOutputStream(); + if (fileOutputStream == null) { + callback.onFileTransferAborted(); + return; + } + long remainingSize = file.getExpectedSize(); + byte[] buffer = new byte[8192]; + int count = buffer.length; + while (remainingSize > 0) { + count = inputStream.read(buffer); + if (count == -1) { + callback.onFileTransferAborted(); + return; + } else { + fileOutputStream.write(buffer, 0, count); + digest.update(buffer, 0, count); + remainingSize -= count; + } + } + fileOutputStream.flush(); + fileOutputStream.close(); + file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest())); + callback.onFileTransmitted(file); + } catch (FileNotFoundException e) { + callback.onFileTransferAborted(); + } catch (IOException e) { + callback.onFileTransferAborted(); + } catch (NoSuchAlgorithmException e) { + callback.onFileTransferAborted(); + } + } + }).start(); + } + + public boolean isProxy() { + return this.candidate.getType() == JingleCandidate.TYPE_PROXY; + } + + public boolean needsActivation() { + return (this.isProxy() && !this.activated); + } + + public void disconnect() { + if (this.socket != null) { + try { + this.socket.close(); + } catch (IOException e) { + + } + } + } + + public boolean isEstablished() { + return this.isEstablished; + } + + public JingleCandidate getCandidate() { + return this.candidate; + } + + public void setActivated(boolean activated) { + this.activated = activated; + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java new file mode 100644 index 00000000..1374e61c --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java @@ -0,0 +1,13 @@ +package eu.siacs.conversations.xmpp.jingle; + +import eu.siacs.conversations.entities.DownloadableFile; + +public abstract class JingleTransport { + public abstract void connect(final OnTransportConnected callback); + + public abstract void receive(final DownloadableFile file, + final OnFileTransmissionStatusChanged callback); + + public abstract void send(final DownloadableFile file, + final OnFileTransmissionStatusChanged callback); +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/OnFileTransmissionStatusChanged.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/OnFileTransmissionStatusChanged.java new file mode 100644 index 00000000..e45e7441 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/OnFileTransmissionStatusChanged.java @@ -0,0 +1,9 @@ +package eu.siacs.conversations.xmpp.jingle; + +import eu.siacs.conversations.entities.DownloadableFile; + +public interface OnFileTransmissionStatusChanged { + public void onFileTransmitted(DownloadableFile file); + + public void onFileTransferAborted(); +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/OnJinglePacketReceived.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/OnJinglePacketReceived.java new file mode 100644 index 00000000..2aaf62a1 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/OnJinglePacketReceived.java @@ -0,0 +1,9 @@ +package eu.siacs.conversations.xmpp.jingle; + +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.xmpp.PacketReceived; +import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; + +public interface OnJinglePacketReceived extends PacketReceived { + public void onJinglePacketReceived(Account account, JinglePacket packet); +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/OnPrimaryCandidateFound.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/OnPrimaryCandidateFound.java new file mode 100644 index 00000000..03a437b2 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/OnPrimaryCandidateFound.java @@ -0,0 +1,6 @@ +package eu.siacs.conversations.xmpp.jingle; + +public interface OnPrimaryCandidateFound { + public void onPrimaryCandidateFound(boolean success, + JingleCandidate canditate); +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/OnTransportConnected.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/OnTransportConnected.java new file mode 100644 index 00000000..38f03c5d --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/OnTransportConnected.java @@ -0,0 +1,7 @@ +package eu.siacs.conversations.xmpp.jingle; + +public interface OnTransportConnected { + public void failed(); + + public void established(); +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java new file mode 100644 index 00000000..bcadbe77 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java @@ -0,0 +1,102 @@ +package eu.siacs.conversations.xmpp.jingle.stanzas; + +import eu.siacs.conversations.entities.DownloadableFile; +import eu.siacs.conversations.xml.Element; + +public class Content extends Element { + + private String transportId; + + private Content(String name) { + super(name); + } + + public Content() { + super("content"); + } + + public Content(String creator, String name) { + super("content"); + this.setAttribute("creator", creator); + this.setAttribute("name", name); + } + + public void setTransportId(String sid) { + this.transportId = sid; + } + + public void setFileOffer(DownloadableFile actualFile, boolean otr) { + Element description = this.addChild("description", + "urn:xmpp:jingle:apps:file-transfer:3"); + Element offer = description.addChild("offer"); + Element file = offer.addChild("file"); + file.addChild("size").setContent(Long.toString(actualFile.getSize())); + if (otr) { + file.addChild("name").setContent(actualFile.getName() + ".otr"); + } else { + file.addChild("name").setContent(actualFile.getName()); + } + } + + public Element getFileOffer() { + Element description = this.findChild("description", + "urn:xmpp:jingle:apps:file-transfer:3"); + if (description == null) { + return null; + } + Element offer = description.findChild("offer"); + if (offer == null) { + return null; + } + return offer.findChild("file"); + } + + public void setFileOffer(Element fileOffer) { + Element description = this.findChild("description", + "urn:xmpp:jingle:apps:file-transfer:3"); + if (description == null) { + description = this.addChild("description", + "urn:xmpp:jingle:apps:file-transfer:3"); + } + description.addChild(fileOffer); + } + + public String getTransportId() { + if (hasSocks5Transport()) { + this.transportId = socks5transport().getAttribute("sid"); + } else if (hasIbbTransport()) { + this.transportId = ibbTransport().getAttribute("sid"); + } + return this.transportId; + } + + public Element socks5transport() { + Element transport = this.findChild("transport", + "urn:xmpp:jingle:transports:s5b:1"); + if (transport == null) { + transport = this.addChild("transport", + "urn:xmpp:jingle:transports:s5b:1"); + transport.setAttribute("sid", this.transportId); + } + return transport; + } + + public Element ibbTransport() { + Element transport = this.findChild("transport", + "urn:xmpp:jingle:transports:ibb:1"); + if (transport == null) { + transport = this.addChild("transport", + "urn:xmpp:jingle:transports:ibb:1"); + transport.setAttribute("sid", this.transportId); + } + return transport; + } + + public boolean hasSocks5Transport() { + return this.hasChild("transport", "urn:xmpp:jingle:transports:s5b:1"); + } + + public boolean hasIbbTransport() { + return this.hasChild("transport", "urn:xmpp:jingle:transports:ibb:1"); + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java new file mode 100644 index 00000000..77a73643 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java @@ -0,0 +1,95 @@ +package eu.siacs.conversations.xmpp.jingle.stanzas; + +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.stanzas.IqPacket; + +public class JinglePacket extends IqPacket { + Content content = null; + Reason reason = null; + Element jingle = new Element("jingle"); + + @Override + public Element addChild(Element child) { + if ("jingle".equals(child.getName())) { + Element contentElement = child.findChild("content"); + if (contentElement != null) { + this.content = new Content(); + this.content.setChildren(contentElement.getChildren()); + this.content.setAttributes(contentElement.getAttributes()); + } + Element reasonElement = child.findChild("reason"); + if (reasonElement != null) { + this.reason = new Reason(); + this.reason.setChildren(reasonElement.getChildren()); + this.reason.setAttributes(reasonElement.getAttributes()); + } + this.jingle.setAttributes(child.getAttributes()); + } + return child; + } + + public JinglePacket setContent(Content content) { + this.content = content; + return this; + } + + public Content getJingleContent() { + if (this.content == null) { + this.content = new Content(); + } + return this.content; + } + + public JinglePacket setReason(Reason reason) { + this.reason = reason; + return this; + } + + public Reason getReason() { + return this.reason; + } + + private void build() { + this.children.clear(); + this.jingle.clearChildren(); + this.jingle.setAttribute("xmlns", "urn:xmpp:jingle:1"); + if (this.content != null) { + jingle.addChild(this.content); + } + if (this.reason != null) { + jingle.addChild(this.reason); + } + this.children.add(jingle); + this.setAttribute("type", "set"); + } + + public String getSessionId() { + return this.jingle.getAttribute("sid"); + } + + public void setSessionId(String sid) { + this.jingle.setAttribute("sid", sid); + } + + @Override + public String toString() { + this.build(); + return super.toString(); + } + + public void setAction(String action) { + this.jingle.setAttribute("action", action); + } + + public String getAction() { + return this.jingle.getAttribute("action"); + } + + public void setInitiator(String initiator) { + this.jingle.setAttribute("initiator", initiator); + } + + public boolean isAction(String action) { + return action.equalsIgnoreCase(this.getAction()); + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java new file mode 100644 index 00000000..610d5e76 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java @@ -0,0 +1,13 @@ +package eu.siacs.conversations.xmpp.jingle.stanzas; + +import eu.siacs.conversations.xml.Element; + +public class Reason extends Element { + private Reason(String name) { + super(name); + } + + public Reason() { + super("reason"); + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java new file mode 100644 index 00000000..154fadf6 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java @@ -0,0 +1,71 @@ +package eu.siacs.conversations.xmpp.pep; + +import eu.siacs.conversations.xml.Element; +import android.util.Base64; + +public class Avatar { + public String type; + public String sha1sum; + public String image; + public int height; + public int width; + public long size; + public String owner; + + public byte[] getImageAsBytes() { + return Base64.decode(image, Base64.DEFAULT); + } + + public String getFilename() { + if (type == null) { + return sha1sum; + } else if (type.equalsIgnoreCase("image/webp")) { + return sha1sum + ".webp"; + } else if (type.equalsIgnoreCase("image/png")) { + return sha1sum + ".png"; + } else { + return sha1sum; + } + } + + public static Avatar parseMetadata(Element items) { + Element item = items.findChild("item"); + if (item == null) { + return null; + } + Element metadata = item.findChild("metadata"); + if (metadata == null) { + return null; + } + String primaryId = item.getAttribute("id"); + if (primaryId == null) { + return null; + } + for (Element child : metadata.getChildren()) { + if (child.getName().equals("info") + && primaryId.equals(child.getAttribute("id"))) { + Avatar avatar = new Avatar(); + String height = child.getAttribute("height"); + String width = child.getAttribute("width"); + String size = child.getAttribute("bytes"); + try { + if (height != null) { + avatar.height = Integer.parseInt(height); + } + if (width != null) { + avatar.width = Integer.parseInt(width); + } + if (size != null) { + avatar.size = Long.parseLong(size); + } + } catch (NumberFormatException e) { + return null; + } + avatar.type = child.getAttribute("type"); + avatar.sha1sum = child.getAttribute("id"); + return avatar; + } + } + return null; + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractStanza.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractStanza.java new file mode 100644 index 00000000..eef41c79 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractStanza.java @@ -0,0 +1,34 @@ +package eu.siacs.conversations.xmpp.stanzas; + +import eu.siacs.conversations.xml.Element; + +public class AbstractStanza extends Element { + + protected AbstractStanza(String name) { + super(name); + } + + public String getTo() { + return getAttribute("to"); + } + + public String getFrom() { + return getAttribute("from"); + } + + public String getId() { + return this.getAttribute("id"); + } + + public void setTo(String to) { + setAttribute("to", to); + } + + public void setFrom(String from) { + setAttribute("from", from); + } + + public void setId(String id) { + setAttribute("id", id); + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/IqPacket.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/IqPacket.java new file mode 100644 index 00000000..9df05e67 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/IqPacket.java @@ -0,0 +1,76 @@ +package eu.siacs.conversations.xmpp.stanzas; + +import eu.siacs.conversations.xml.Element; + +public class IqPacket extends AbstractStanza { + + public static final int TYPE_ERROR = -1; + public static final int TYPE_SET = 0; + public static final int TYPE_RESULT = 1; + public static final int TYPE_GET = 2; + + private IqPacket(String name) { + super(name); + } + + public IqPacket(int type) { + super("iq"); + switch (type) { + case TYPE_SET: + this.setAttribute("type", "set"); + break; + case TYPE_GET: + this.setAttribute("type", "get"); + break; + case TYPE_RESULT: + this.setAttribute("type", "result"); + break; + case TYPE_ERROR: + this.setAttribute("type", "error"); + break; + default: + break; + } + } + + public IqPacket() { + super("iq"); + } + + public Element query() { + Element query = findChild("query"); + if (query == null) { + query = addChild("query"); + } + return query; + } + + public Element query(String xmlns) { + Element query = query(); + query.setAttribute("xmlns", xmlns); + return query(); + } + + public int getType() { + String type = getAttribute("type"); + if ("error".equals(type)) { + return TYPE_ERROR; + } else if ("result".equals(type)) { + return TYPE_RESULT; + } else if ("set".equals(type)) { + return TYPE_SET; + } else if ("get".equals(type)) { + return TYPE_GET; + } else { + return 1000; + } + } + + public IqPacket generateRespone(int type) { + IqPacket packet = new IqPacket(type); + packet.setTo(this.getFrom()); + packet.setId(this.getId()); + return packet; + } + +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java new file mode 100644 index 00000000..4e7b532b --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java @@ -0,0 +1,66 @@ +package eu.siacs.conversations.xmpp.stanzas; + +import eu.siacs.conversations.xml.Element; + +public class MessagePacket extends AbstractStanza { + public static final int TYPE_CHAT = 0; + public static final int TYPE_NORMAL = 2; + public static final int TYPE_GROUPCHAT = 3; + public static final int TYPE_ERROR = 4; + public static final int TYPE_HEADLINE = 5; + + public MessagePacket() { + super("message"); + } + + public String getBody() { + Element body = this.findChild("body"); + if (body != null) { + return body.getContent(); + } else { + return null; + } + } + + public void setBody(String text) { + this.children.remove(findChild("body")); + Element body = new Element("body"); + body.setContent(text); + this.children.add(body); + } + + public void setType(int type) { + switch (type) { + case TYPE_CHAT: + this.setAttribute("type", "chat"); + break; + case TYPE_GROUPCHAT: + this.setAttribute("type", "groupchat"); + break; + case TYPE_NORMAL: + break; + default: + this.setAttribute("type", "chat"); + break; + } + } + + public int getType() { + String type = getAttribute("type"); + if (type == null) { + return TYPE_NORMAL; + } else if (type.equals("normal")) { + return TYPE_NORMAL; + } else if (type.equals("chat")) { + return TYPE_CHAT; + } else if (type.equals("groupchat")) { + return TYPE_GROUPCHAT; + } else if (type.equals("error")) { + return TYPE_ERROR; + } else if (type.equals("headline")) { + return TYPE_HEADLINE; + } else { + return TYPE_NORMAL; + } + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/PresencePacket.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/PresencePacket.java new file mode 100644 index 00000000..7ea32099 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/PresencePacket.java @@ -0,0 +1,8 @@ +package eu.siacs.conversations.xmpp.stanzas; + +public class PresencePacket extends AbstractStanza { + + public PresencePacket() { + super("presence"); + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/ActivePacket.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/ActivePacket.java new file mode 100644 index 00000000..78ab66d8 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/ActivePacket.java @@ -0,0 +1,10 @@ +package eu.siacs.conversations.xmpp.stanzas.csi; + +import eu.siacs.conversations.xmpp.stanzas.AbstractStanza; + +public class ActivePacket extends AbstractStanza { + public ActivePacket() { + super("active"); + setAttribute("xmlns", "urn:xmpp:csi:0"); + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/InactivePacket.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/InactivePacket.java new file mode 100644 index 00000000..f109280f --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/InactivePacket.java @@ -0,0 +1,10 @@ +package eu.siacs.conversations.xmpp.stanzas.csi; + +import eu.siacs.conversations.xmpp.stanzas.AbstractStanza; + +public class InactivePacket extends AbstractStanza { + public InactivePacket() { + super("inactive"); + setAttribute("xmlns", "urn:xmpp:csi:0"); + } +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/AckPacket.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/AckPacket.java new file mode 100644 index 00000000..f93b5d87 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/AckPacket.java @@ -0,0 +1,13 @@ +package eu.siacs.conversations.xmpp.stanzas.streammgmt; + +import eu.siacs.conversations.xmpp.stanzas.AbstractStanza; + +public class AckPacket extends AbstractStanza { + + public AckPacket(int sequence, int smVersion) { + super("a"); + this.setAttribute("xmlns", "urn:xmpp:sm:" + smVersion); + this.setAttribute("h", Integer.toString(sequence)); + } + +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/EnablePacket.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/EnablePacket.java new file mode 100644 index 00000000..78cd81ed --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/EnablePacket.java @@ -0,0 +1,13 @@ +package eu.siacs.conversations.xmpp.stanzas.streammgmt; + +import eu.siacs.conversations.xmpp.stanzas.AbstractStanza; + +public class EnablePacket extends AbstractStanza { + + public EnablePacket(int smVersion) { + super("enable"); + this.setAttribute("xmlns", "urn:xmpp:sm:" + smVersion); + this.setAttribute("resume", "true"); + } + +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/RequestPacket.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/RequestPacket.java new file mode 100644 index 00000000..98cfc748 --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/RequestPacket.java @@ -0,0 +1,12 @@ +package eu.siacs.conversations.xmpp.stanzas.streammgmt; + +import eu.siacs.conversations.xmpp.stanzas.AbstractStanza; + +public class RequestPacket extends AbstractStanza { + + public RequestPacket(int smVersion) { + super("r"); + this.setAttribute("xmlns", "urn:xmpp:sm:" + smVersion); + } + +} diff --git a/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/ResumePacket.java b/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/ResumePacket.java new file mode 100644 index 00000000..9cdcfa5e --- /dev/null +++ b/conversations/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/ResumePacket.java @@ -0,0 +1,14 @@ +package eu.siacs.conversations.xmpp.stanzas.streammgmt; + +import eu.siacs.conversations.xmpp.stanzas.AbstractStanza; + +public class ResumePacket extends AbstractStanza { + + public ResumePacket(String id, int sequence, int smVersion) { + super("resume"); + this.setAttribute("xmlns", "urn:xmpp:sm:" + smVersion); + this.setAttribute("previd", id); + this.setAttribute("h", Integer.toString(sequence)); + } + +} diff --git a/conversations/src/main/res/drawable-hdpi/ic_action_add_group.png b/conversations/src/main/res/drawable-hdpi/ic_action_add_group.png new file mode 100644 index 00000000..97640355 Binary files /dev/null and b/conversations/src/main/res/drawable-hdpi/ic_action_add_group.png differ diff --git a/conversations/src/main/res/drawable-hdpi/ic_action_add_person.png b/conversations/src/main/res/drawable-hdpi/ic_action_add_person.png new file mode 100644 index 00000000..9d88d0f4 Binary files /dev/null and b/conversations/src/main/res/drawable-hdpi/ic_action_add_person.png differ diff --git a/conversations/src/main/res/drawable-hdpi/ic_action_chat.png b/conversations/src/main/res/drawable-hdpi/ic_action_chat.png new file mode 100644 index 00000000..0847ac46 Binary files /dev/null and b/conversations/src/main/res/drawable-hdpi/ic_action_chat.png differ diff --git a/conversations/src/main/res/drawable-hdpi/ic_action_copy.png b/conversations/src/main/res/drawable-hdpi/ic_action_copy.png new file mode 100644 index 00000000..22327391 Binary files /dev/null and b/conversations/src/main/res/drawable-hdpi/ic_action_copy.png differ diff --git a/conversations/src/main/res/drawable-hdpi/ic_action_discard.png b/conversations/src/main/res/drawable-hdpi/ic_action_discard.png new file mode 100644 index 00000000..703b31f8 Binary files /dev/null and b/conversations/src/main/res/drawable-hdpi/ic_action_discard.png differ diff --git a/conversations/src/main/res/drawable-hdpi/ic_action_edit.png b/conversations/src/main/res/drawable-hdpi/ic_action_edit.png new file mode 100644 index 00000000..756db316 Binary files /dev/null and b/conversations/src/main/res/drawable-hdpi/ic_action_edit.png differ diff --git a/conversations/src/main/res/drawable-hdpi/ic_action_edit_dark.png b/conversations/src/main/res/drawable-hdpi/ic_action_edit_dark.png new file mode 100644 index 00000000..5f7c6eff Binary files /dev/null and b/conversations/src/main/res/drawable-hdpi/ic_action_edit_dark.png differ diff --git a/conversations/src/main/res/drawable-hdpi/ic_action_group.png b/conversations/src/main/res/drawable-hdpi/ic_action_group.png new file mode 100644 index 00000000..3e7f16d5 Binary files /dev/null and b/conversations/src/main/res/drawable-hdpi/ic_action_group.png differ diff --git a/conversations/src/main/res/drawable-hdpi/ic_action_new.png b/conversations/src/main/res/drawable-hdpi/ic_action_new.png new file mode 100644 index 00000000..d866d616 Binary files /dev/null and b/conversations/src/main/res/drawable-hdpi/ic_action_new.png differ diff --git a/conversations/src/main/res/drawable-hdpi/ic_action_new_attachment.png b/conversations/src/main/res/drawable-hdpi/ic_action_new_attachment.png new file mode 100644 index 00000000..c01c2b38 Binary files /dev/null and b/conversations/src/main/res/drawable-hdpi/ic_action_new_attachment.png differ diff --git a/conversations/src/main/res/drawable-hdpi/ic_action_not_secure.png b/conversations/src/main/res/drawable-hdpi/ic_action_not_secure.png new file mode 100644 index 00000000..2c917615 Binary files /dev/null and b/conversations/src/main/res/drawable-hdpi/ic_action_not_secure.png differ diff --git a/conversations/src/main/res/drawable-hdpi/ic_action_refresh.png b/conversations/src/main/res/drawable-hdpi/ic_action_refresh.png new file mode 100644 index 00000000..45b22282 Binary files /dev/null and b/conversations/src/main/res/drawable-hdpi/ic_action_refresh.png differ diff --git a/conversations/src/main/res/drawable-hdpi/ic_action_remove.png b/conversations/src/main/res/drawable-hdpi/ic_action_remove.png new file mode 100644 index 00000000..58a56e45 Binary files /dev/null and b/conversations/src/main/res/drawable-hdpi/ic_action_remove.png differ diff --git a/conversations/src/main/res/drawable-hdpi/ic_action_search.png b/conversations/src/main/res/drawable-hdpi/ic_action_search.png new file mode 100644 index 00000000..772e3598 Binary files /dev/null and b/conversations/src/main/res/drawable-hdpi/ic_action_search.png differ diff --git a/conversations/src/main/res/drawable-hdpi/ic_action_secure.png b/conversations/src/main/res/drawable-hdpi/ic_action_secure.png new file mode 100644 index 00000000..4439d1ae Binary files /dev/null and b/conversations/src/main/res/drawable-hdpi/ic_action_secure.png differ diff --git a/conversations/src/main/res/drawable-hdpi/ic_action_send_now_away.png b/conversations/src/main/res/drawable-hdpi/ic_action_send_now_away.png new file mode 100644 index 00000000..505cbe63 Binary files /dev/null and b/conversations/src/main/res/drawable-hdpi/ic_action_send_now_away.png differ diff --git a/conversations/src/main/res/drawable-hdpi/ic_action_send_now_dnd.png b/conversations/src/main/res/drawable-hdpi/ic_action_send_now_dnd.png new file mode 100644 index 00000000..a376524d Binary files /dev/null and b/conversations/src/main/res/drawable-hdpi/ic_action_send_now_dnd.png differ diff --git a/conversations/src/main/res/drawable-hdpi/ic_action_send_now_offline.png b/conversations/src/main/res/drawable-hdpi/ic_action_send_now_offline.png new file mode 100644 index 00000000..d4d2d510 Binary files /dev/null and b/conversations/src/main/res/drawable-hdpi/ic_action_send_now_offline.png differ diff --git a/conversations/src/main/res/drawable-hdpi/ic_action_send_now_online.png b/conversations/src/main/res/drawable-hdpi/ic_action_send_now_online.png new file mode 100644 index 00000000..48676f7b Binary files /dev/null and b/conversations/src/main/res/drawable-hdpi/ic_action_send_now_online.png differ diff --git a/conversations/src/main/res/drawable-hdpi/ic_activity.png b/conversations/src/main/res/drawable-hdpi/ic_activity.png new file mode 100644 index 00000000..613da683 Binary files /dev/null and b/conversations/src/main/res/drawable-hdpi/ic_activity.png differ diff --git a/conversations/src/main/res/drawable-hdpi/ic_indicator.png b/conversations/src/main/res/drawable-hdpi/ic_indicator.png new file mode 100644 index 00000000..6de8969f Binary files /dev/null and b/conversations/src/main/res/drawable-hdpi/ic_indicator.png differ diff --git a/conversations/src/main/res/drawable-hdpi/ic_launcher.png b/conversations/src/main/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 00000000..d48df2c3 Binary files /dev/null and b/conversations/src/main/res/drawable-hdpi/ic_launcher.png differ diff --git a/conversations/src/main/res/drawable-hdpi/ic_notification.png b/conversations/src/main/res/drawable-hdpi/ic_notification.png new file mode 100644 index 00000000..664ba535 Binary files /dev/null and b/conversations/src/main/res/drawable-hdpi/ic_notification.png differ diff --git a/conversations/src/main/res/drawable-hdpi/ic_profile.png b/conversations/src/main/res/drawable-hdpi/ic_profile.png new file mode 100644 index 00000000..3f071dec Binary files /dev/null and b/conversations/src/main/res/drawable-hdpi/ic_profile.png differ diff --git a/conversations/src/main/res/drawable-hdpi/ic_received_indicator.png b/conversations/src/main/res/drawable-hdpi/ic_received_indicator.png new file mode 100644 index 00000000..b1e3f274 Binary files /dev/null and b/conversations/src/main/res/drawable-hdpi/ic_received_indicator.png differ diff --git a/conversations/src/main/res/drawable-hdpi/ic_secure_indicator.png b/conversations/src/main/res/drawable-hdpi/ic_secure_indicator.png new file mode 100644 index 00000000..2a2934fb Binary files /dev/null and b/conversations/src/main/res/drawable-hdpi/ic_secure_indicator.png differ diff --git a/conversations/src/main/res/drawable-hdpi/tab_selected_conversations.9.png b/conversations/src/main/res/drawable-hdpi/tab_selected_conversations.9.png new file mode 100644 index 00000000..b8f44c21 Binary files /dev/null and b/conversations/src/main/res/drawable-hdpi/tab_selected_conversations.9.png differ diff --git a/conversations/src/main/res/drawable-hdpi/tab_selected_focused_conversations.9.png b/conversations/src/main/res/drawable-hdpi/tab_selected_focused_conversations.9.png new file mode 100644 index 00000000..5512dbd3 Binary files /dev/null and b/conversations/src/main/res/drawable-hdpi/tab_selected_focused_conversations.9.png differ diff --git a/conversations/src/main/res/drawable-hdpi/tab_selected_pressed_conversations.9.png b/conversations/src/main/res/drawable-hdpi/tab_selected_pressed_conversations.9.png new file mode 100644 index 00000000..e5f1df22 Binary files /dev/null and b/conversations/src/main/res/drawable-hdpi/tab_selected_pressed_conversations.9.png differ diff --git a/conversations/src/main/res/drawable-hdpi/tab_unselected_conversations.9.png b/conversations/src/main/res/drawable-hdpi/tab_unselected_conversations.9.png new file mode 100644 index 00000000..7cd46d63 Binary files /dev/null and b/conversations/src/main/res/drawable-hdpi/tab_unselected_conversations.9.png differ diff --git a/conversations/src/main/res/drawable-hdpi/tab_unselected_focused_conversations.9.png b/conversations/src/main/res/drawable-hdpi/tab_unselected_focused_conversations.9.png new file mode 100644 index 00000000..438ecdd8 Binary files /dev/null and b/conversations/src/main/res/drawable-hdpi/tab_unselected_focused_conversations.9.png differ diff --git a/conversations/src/main/res/drawable-hdpi/tab_unselected_pressed_conversations.9.png b/conversations/src/main/res/drawable-hdpi/tab_unselected_pressed_conversations.9.png new file mode 100644 index 00000000..4f18a95a Binary files /dev/null and b/conversations/src/main/res/drawable-hdpi/tab_unselected_pressed_conversations.9.png differ diff --git a/conversations/src/main/res/drawable-mdpi/ic_action_add_group.png b/conversations/src/main/res/drawable-mdpi/ic_action_add_group.png new file mode 100644 index 00000000..9a655899 Binary files /dev/null and b/conversations/src/main/res/drawable-mdpi/ic_action_add_group.png differ diff --git a/conversations/src/main/res/drawable-mdpi/ic_action_add_person.png b/conversations/src/main/res/drawable-mdpi/ic_action_add_person.png new file mode 100644 index 00000000..b7d8f46a Binary files /dev/null and b/conversations/src/main/res/drawable-mdpi/ic_action_add_person.png differ diff --git a/conversations/src/main/res/drawable-mdpi/ic_action_chat.png b/conversations/src/main/res/drawable-mdpi/ic_action_chat.png new file mode 100644 index 00000000..8fdb5d75 Binary files /dev/null and b/conversations/src/main/res/drawable-mdpi/ic_action_chat.png differ diff --git a/conversations/src/main/res/drawable-mdpi/ic_action_copy.png b/conversations/src/main/res/drawable-mdpi/ic_action_copy.png new file mode 100644 index 00000000..71348202 Binary files /dev/null and b/conversations/src/main/res/drawable-mdpi/ic_action_copy.png differ diff --git a/conversations/src/main/res/drawable-mdpi/ic_action_discard.png b/conversations/src/main/res/drawable-mdpi/ic_action_discard.png new file mode 100644 index 00000000..248fb09c Binary files /dev/null and b/conversations/src/main/res/drawable-mdpi/ic_action_discard.png differ diff --git a/conversations/src/main/res/drawable-mdpi/ic_action_edit.png b/conversations/src/main/res/drawable-mdpi/ic_action_edit.png new file mode 100644 index 00000000..68a45320 Binary files /dev/null and b/conversations/src/main/res/drawable-mdpi/ic_action_edit.png differ diff --git a/conversations/src/main/res/drawable-mdpi/ic_action_edit_dark.png b/conversations/src/main/res/drawable-mdpi/ic_action_edit_dark.png new file mode 100644 index 00000000..650b4d89 Binary files /dev/null and b/conversations/src/main/res/drawable-mdpi/ic_action_edit_dark.png differ diff --git a/conversations/src/main/res/drawable-mdpi/ic_action_group.png b/conversations/src/main/res/drawable-mdpi/ic_action_group.png new file mode 100644 index 00000000..1ee3cccd Binary files /dev/null and b/conversations/src/main/res/drawable-mdpi/ic_action_group.png differ diff --git a/conversations/src/main/res/drawable-mdpi/ic_action_new.png b/conversations/src/main/res/drawable-mdpi/ic_action_new.png new file mode 100644 index 00000000..f17e7980 Binary files /dev/null and b/conversations/src/main/res/drawable-mdpi/ic_action_new.png differ diff --git a/conversations/src/main/res/drawable-mdpi/ic_action_new_attachment.png b/conversations/src/main/res/drawable-mdpi/ic_action_new_attachment.png new file mode 100644 index 00000000..1d265aac Binary files /dev/null and b/conversations/src/main/res/drawable-mdpi/ic_action_new_attachment.png differ diff --git a/conversations/src/main/res/drawable-mdpi/ic_action_not_secure.png b/conversations/src/main/res/drawable-mdpi/ic_action_not_secure.png new file mode 100644 index 00000000..faffa233 Binary files /dev/null and b/conversations/src/main/res/drawable-mdpi/ic_action_not_secure.png differ diff --git a/conversations/src/main/res/drawable-mdpi/ic_action_refresh.png b/conversations/src/main/res/drawable-mdpi/ic_action_refresh.png new file mode 100644 index 00000000..de008e51 Binary files /dev/null and b/conversations/src/main/res/drawable-mdpi/ic_action_refresh.png differ diff --git a/conversations/src/main/res/drawable-mdpi/ic_action_remove.png b/conversations/src/main/res/drawable-mdpi/ic_action_remove.png new file mode 100644 index 00000000..342a79de Binary files /dev/null and b/conversations/src/main/res/drawable-mdpi/ic_action_remove.png differ diff --git a/conversations/src/main/res/drawable-mdpi/ic_action_search.png b/conversations/src/main/res/drawable-mdpi/ic_action_search.png new file mode 100644 index 00000000..4edb1ff9 Binary files /dev/null and b/conversations/src/main/res/drawable-mdpi/ic_action_search.png differ diff --git a/conversations/src/main/res/drawable-mdpi/ic_action_secure.png b/conversations/src/main/res/drawable-mdpi/ic_action_secure.png new file mode 100644 index 00000000..05332ebf Binary files /dev/null and b/conversations/src/main/res/drawable-mdpi/ic_action_secure.png differ diff --git a/conversations/src/main/res/drawable-mdpi/ic_action_send_now_away.png b/conversations/src/main/res/drawable-mdpi/ic_action_send_now_away.png new file mode 100644 index 00000000..0fdca901 Binary files /dev/null and b/conversations/src/main/res/drawable-mdpi/ic_action_send_now_away.png differ diff --git a/conversations/src/main/res/drawable-mdpi/ic_action_send_now_dnd.png b/conversations/src/main/res/drawable-mdpi/ic_action_send_now_dnd.png new file mode 100644 index 00000000..c0aef36c Binary files /dev/null and b/conversations/src/main/res/drawable-mdpi/ic_action_send_now_dnd.png differ diff --git a/conversations/src/main/res/drawable-mdpi/ic_action_send_now_offline.png b/conversations/src/main/res/drawable-mdpi/ic_action_send_now_offline.png new file mode 100644 index 00000000..7723f4aa Binary files /dev/null and b/conversations/src/main/res/drawable-mdpi/ic_action_send_now_offline.png differ diff --git a/conversations/src/main/res/drawable-mdpi/ic_action_send_now_online.png b/conversations/src/main/res/drawable-mdpi/ic_action_send_now_online.png new file mode 100644 index 00000000..39d00ee4 Binary files /dev/null and b/conversations/src/main/res/drawable-mdpi/ic_action_send_now_online.png differ diff --git a/conversations/src/main/res/drawable-mdpi/ic_activity.png b/conversations/src/main/res/drawable-mdpi/ic_activity.png new file mode 100644 index 00000000..c8727f57 Binary files /dev/null and b/conversations/src/main/res/drawable-mdpi/ic_activity.png differ diff --git a/conversations/src/main/res/drawable-mdpi/ic_indicator.png b/conversations/src/main/res/drawable-mdpi/ic_indicator.png new file mode 100644 index 00000000..bb4fee10 Binary files /dev/null and b/conversations/src/main/res/drawable-mdpi/ic_indicator.png differ diff --git a/conversations/src/main/res/drawable-mdpi/ic_launcher.png b/conversations/src/main/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 00000000..200daf4c Binary files /dev/null and b/conversations/src/main/res/drawable-mdpi/ic_launcher.png differ diff --git a/conversations/src/main/res/drawable-mdpi/ic_notification.png b/conversations/src/main/res/drawable-mdpi/ic_notification.png new file mode 100644 index 00000000..5d1aca10 Binary files /dev/null and b/conversations/src/main/res/drawable-mdpi/ic_notification.png differ diff --git a/conversations/src/main/res/drawable-mdpi/ic_profile.png b/conversations/src/main/res/drawable-mdpi/ic_profile.png new file mode 100644 index 00000000..0d056c7c Binary files /dev/null and b/conversations/src/main/res/drawable-mdpi/ic_profile.png differ diff --git a/conversations/src/main/res/drawable-mdpi/ic_received_indicator.png b/conversations/src/main/res/drawable-mdpi/ic_received_indicator.png new file mode 100644 index 00000000..88ff1efb Binary files /dev/null and b/conversations/src/main/res/drawable-mdpi/ic_received_indicator.png differ diff --git a/conversations/src/main/res/drawable-mdpi/ic_secure_indicator.png b/conversations/src/main/res/drawable-mdpi/ic_secure_indicator.png new file mode 100644 index 00000000..5a73aef4 Binary files /dev/null and b/conversations/src/main/res/drawable-mdpi/ic_secure_indicator.png differ diff --git a/conversations/src/main/res/drawable-mdpi/tab_selected_conversations.9.png b/conversations/src/main/res/drawable-mdpi/tab_selected_conversations.9.png new file mode 100644 index 00000000..09d42dc8 Binary files /dev/null and b/conversations/src/main/res/drawable-mdpi/tab_selected_conversations.9.png differ diff --git a/conversations/src/main/res/drawable-mdpi/tab_selected_focused_conversations.9.png b/conversations/src/main/res/drawable-mdpi/tab_selected_focused_conversations.9.png new file mode 100644 index 00000000..20af01de Binary files /dev/null and b/conversations/src/main/res/drawable-mdpi/tab_selected_focused_conversations.9.png differ diff --git a/conversations/src/main/res/drawable-mdpi/tab_selected_pressed_conversations.9.png b/conversations/src/main/res/drawable-mdpi/tab_selected_pressed_conversations.9.png new file mode 100644 index 00000000..13a878be Binary files /dev/null and b/conversations/src/main/res/drawable-mdpi/tab_selected_pressed_conversations.9.png differ diff --git a/conversations/src/main/res/drawable-mdpi/tab_unselected_conversations.9.png b/conversations/src/main/res/drawable-mdpi/tab_unselected_conversations.9.png new file mode 100644 index 00000000..ad2dbae9 Binary files /dev/null and b/conversations/src/main/res/drawable-mdpi/tab_unselected_conversations.9.png differ diff --git a/conversations/src/main/res/drawable-mdpi/tab_unselected_focused_conversations.9.png b/conversations/src/main/res/drawable-mdpi/tab_unselected_focused_conversations.9.png new file mode 100644 index 00000000..dfff5ac8 Binary files /dev/null and b/conversations/src/main/res/drawable-mdpi/tab_unselected_focused_conversations.9.png differ diff --git a/conversations/src/main/res/drawable-mdpi/tab_unselected_pressed_conversations.9.png b/conversations/src/main/res/drawable-mdpi/tab_unselected_pressed_conversations.9.png new file mode 100644 index 00000000..4365d178 Binary files /dev/null and b/conversations/src/main/res/drawable-mdpi/tab_unselected_pressed_conversations.9.png differ diff --git a/conversations/src/main/res/drawable-xhdpi/ic_action_add_group.png b/conversations/src/main/res/drawable-xhdpi/ic_action_add_group.png new file mode 100644 index 00000000..c493aa5a Binary files /dev/null and b/conversations/src/main/res/drawable-xhdpi/ic_action_add_group.png differ diff --git a/conversations/src/main/res/drawable-xhdpi/ic_action_add_person.png b/conversations/src/main/res/drawable-xhdpi/ic_action_add_person.png new file mode 100644 index 00000000..4e8de1b6 Binary files /dev/null and b/conversations/src/main/res/drawable-xhdpi/ic_action_add_person.png differ diff --git a/conversations/src/main/res/drawable-xhdpi/ic_action_chat.png b/conversations/src/main/res/drawable-xhdpi/ic_action_chat.png new file mode 100644 index 00000000..8a9a4314 Binary files /dev/null and b/conversations/src/main/res/drawable-xhdpi/ic_action_chat.png differ diff --git a/conversations/src/main/res/drawable-xhdpi/ic_action_copy.png b/conversations/src/main/res/drawable-xhdpi/ic_action_copy.png new file mode 100644 index 00000000..5ddf1513 Binary files /dev/null and b/conversations/src/main/res/drawable-xhdpi/ic_action_copy.png differ diff --git a/conversations/src/main/res/drawable-xhdpi/ic_action_discard.png b/conversations/src/main/res/drawable-xhdpi/ic_action_discard.png new file mode 100644 index 00000000..9eeeed12 Binary files /dev/null and b/conversations/src/main/res/drawable-xhdpi/ic_action_discard.png differ diff --git a/conversations/src/main/res/drawable-xhdpi/ic_action_edit.png b/conversations/src/main/res/drawable-xhdpi/ic_action_edit.png new file mode 100644 index 00000000..67e056fe Binary files /dev/null and b/conversations/src/main/res/drawable-xhdpi/ic_action_edit.png differ diff --git a/conversations/src/main/res/drawable-xhdpi/ic_action_edit_dark.png b/conversations/src/main/res/drawable-xhdpi/ic_action_edit_dark.png new file mode 100644 index 00000000..8ab436d8 Binary files /dev/null and b/conversations/src/main/res/drawable-xhdpi/ic_action_edit_dark.png differ diff --git a/conversations/src/main/res/drawable-xhdpi/ic_action_group.png b/conversations/src/main/res/drawable-xhdpi/ic_action_group.png new file mode 100644 index 00000000..fa2af497 Binary files /dev/null and b/conversations/src/main/res/drawable-xhdpi/ic_action_group.png differ diff --git a/conversations/src/main/res/drawable-xhdpi/ic_action_new.png b/conversations/src/main/res/drawable-xhdpi/ic_action_new.png new file mode 100644 index 00000000..dde2141f Binary files /dev/null and b/conversations/src/main/res/drawable-xhdpi/ic_action_new.png differ diff --git a/conversations/src/main/res/drawable-xhdpi/ic_action_new_attachment.png b/conversations/src/main/res/drawable-xhdpi/ic_action_new_attachment.png new file mode 100644 index 00000000..41cbab20 Binary files /dev/null and b/conversations/src/main/res/drawable-xhdpi/ic_action_new_attachment.png differ diff --git a/conversations/src/main/res/drawable-xhdpi/ic_action_not_secure.png b/conversations/src/main/res/drawable-xhdpi/ic_action_not_secure.png new file mode 100644 index 00000000..c0902a03 Binary files /dev/null and b/conversations/src/main/res/drawable-xhdpi/ic_action_not_secure.png differ diff --git a/conversations/src/main/res/drawable-xhdpi/ic_action_refresh.png b/conversations/src/main/res/drawable-xhdpi/ic_action_refresh.png new file mode 100644 index 00000000..cdc160d4 Binary files /dev/null and b/conversations/src/main/res/drawable-xhdpi/ic_action_refresh.png differ diff --git a/conversations/src/main/res/drawable-xhdpi/ic_action_remove.png b/conversations/src/main/res/drawable-xhdpi/ic_action_remove.png new file mode 100644 index 00000000..58e2e3b4 Binary files /dev/null and b/conversations/src/main/res/drawable-xhdpi/ic_action_remove.png differ diff --git a/conversations/src/main/res/drawable-xhdpi/ic_action_search.png b/conversations/src/main/res/drawable-xhdpi/ic_action_search.png new file mode 100644 index 00000000..19658e4a Binary files /dev/null and b/conversations/src/main/res/drawable-xhdpi/ic_action_search.png differ diff --git a/conversations/src/main/res/drawable-xhdpi/ic_action_secure.png b/conversations/src/main/res/drawable-xhdpi/ic_action_secure.png new file mode 100644 index 00000000..4e08b95a Binary files /dev/null and b/conversations/src/main/res/drawable-xhdpi/ic_action_secure.png differ diff --git a/conversations/src/main/res/drawable-xhdpi/ic_action_send_now_away.png b/conversations/src/main/res/drawable-xhdpi/ic_action_send_now_away.png new file mode 100644 index 00000000..bb999d85 Binary files /dev/null and b/conversations/src/main/res/drawable-xhdpi/ic_action_send_now_away.png differ diff --git a/conversations/src/main/res/drawable-xhdpi/ic_action_send_now_dnd.png b/conversations/src/main/res/drawable-xhdpi/ic_action_send_now_dnd.png new file mode 100644 index 00000000..a0bf5561 Binary files /dev/null and b/conversations/src/main/res/drawable-xhdpi/ic_action_send_now_dnd.png differ diff --git a/conversations/src/main/res/drawable-xhdpi/ic_action_send_now_offline.png b/conversations/src/main/res/drawable-xhdpi/ic_action_send_now_offline.png new file mode 100644 index 00000000..6da9ff7b Binary files /dev/null and b/conversations/src/main/res/drawable-xhdpi/ic_action_send_now_offline.png differ diff --git a/conversations/src/main/res/drawable-xhdpi/ic_action_send_now_online.png b/conversations/src/main/res/drawable-xhdpi/ic_action_send_now_online.png new file mode 100644 index 00000000..348ba657 Binary files /dev/null and b/conversations/src/main/res/drawable-xhdpi/ic_action_send_now_online.png differ diff --git a/conversations/src/main/res/drawable-xhdpi/ic_activity.png b/conversations/src/main/res/drawable-xhdpi/ic_activity.png new file mode 100644 index 00000000..95ffbecf Binary files /dev/null and b/conversations/src/main/res/drawable-xhdpi/ic_activity.png differ diff --git a/conversations/src/main/res/drawable-xhdpi/ic_indicator.png b/conversations/src/main/res/drawable-xhdpi/ic_indicator.png new file mode 100644 index 00000000..3e5141c2 Binary files /dev/null and b/conversations/src/main/res/drawable-xhdpi/ic_indicator.png differ diff --git a/conversations/src/main/res/drawable-xhdpi/ic_launcher.png b/conversations/src/main/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 00000000..927a2d2a Binary files /dev/null and b/conversations/src/main/res/drawable-xhdpi/ic_launcher.png differ diff --git a/conversations/src/main/res/drawable-xhdpi/ic_notification.png b/conversations/src/main/res/drawable-xhdpi/ic_notification.png new file mode 100644 index 00000000..dfa643d0 Binary files /dev/null and b/conversations/src/main/res/drawable-xhdpi/ic_notification.png differ diff --git a/conversations/src/main/res/drawable-xhdpi/ic_profile.png b/conversations/src/main/res/drawable-xhdpi/ic_profile.png new file mode 100644 index 00000000..88a82cf0 Binary files /dev/null and b/conversations/src/main/res/drawable-xhdpi/ic_profile.png differ diff --git a/conversations/src/main/res/drawable-xhdpi/ic_received_indicator.png b/conversations/src/main/res/drawable-xhdpi/ic_received_indicator.png new file mode 100644 index 00000000..2c871933 Binary files /dev/null and b/conversations/src/main/res/drawable-xhdpi/ic_received_indicator.png differ diff --git a/conversations/src/main/res/drawable-xhdpi/ic_secure_indicator.png b/conversations/src/main/res/drawable-xhdpi/ic_secure_indicator.png new file mode 100644 index 00000000..1f4c9a32 Binary files /dev/null and b/conversations/src/main/res/drawable-xhdpi/ic_secure_indicator.png differ diff --git a/conversations/src/main/res/drawable-xhdpi/tab_selected_conversations.9.png b/conversations/src/main/res/drawable-xhdpi/tab_selected_conversations.9.png new file mode 100644 index 00000000..34eb4ec0 Binary files /dev/null and b/conversations/src/main/res/drawable-xhdpi/tab_selected_conversations.9.png differ diff --git a/conversations/src/main/res/drawable-xhdpi/tab_selected_focused_conversations.9.png b/conversations/src/main/res/drawable-xhdpi/tab_selected_focused_conversations.9.png new file mode 100644 index 00000000..3155ef69 Binary files /dev/null and b/conversations/src/main/res/drawable-xhdpi/tab_selected_focused_conversations.9.png differ diff --git a/conversations/src/main/res/drawable-xhdpi/tab_selected_pressed_conversations.9.png b/conversations/src/main/res/drawable-xhdpi/tab_selected_pressed_conversations.9.png new file mode 100644 index 00000000..5c2440e4 Binary files /dev/null and b/conversations/src/main/res/drawable-xhdpi/tab_selected_pressed_conversations.9.png differ diff --git a/conversations/src/main/res/drawable-xhdpi/tab_unselected_conversations.9.png b/conversations/src/main/res/drawable-xhdpi/tab_unselected_conversations.9.png new file mode 100644 index 00000000..e9ab742e Binary files /dev/null and b/conversations/src/main/res/drawable-xhdpi/tab_unselected_conversations.9.png differ diff --git a/conversations/src/main/res/drawable-xhdpi/tab_unselected_focused_conversations.9.png b/conversations/src/main/res/drawable-xhdpi/tab_unselected_focused_conversations.9.png new file mode 100644 index 00000000..42a2191e Binary files /dev/null and b/conversations/src/main/res/drawable-xhdpi/tab_unselected_focused_conversations.9.png differ diff --git a/conversations/src/main/res/drawable-xhdpi/tab_unselected_pressed_conversations.9.png b/conversations/src/main/res/drawable-xhdpi/tab_unselected_pressed_conversations.9.png new file mode 100644 index 00000000..a5a2c25e Binary files /dev/null and b/conversations/src/main/res/drawable-xhdpi/tab_unselected_pressed_conversations.9.png differ diff --git a/conversations/src/main/res/drawable-xxhdpi/ic_action_add_group.png b/conversations/src/main/res/drawable-xxhdpi/ic_action_add_group.png new file mode 100644 index 00000000..2b46dbb9 Binary files /dev/null and b/conversations/src/main/res/drawable-xxhdpi/ic_action_add_group.png differ diff --git a/conversations/src/main/res/drawable-xxhdpi/ic_action_add_person.png b/conversations/src/main/res/drawable-xxhdpi/ic_action_add_person.png new file mode 100644 index 00000000..e9a58eaf Binary files /dev/null and b/conversations/src/main/res/drawable-xxhdpi/ic_action_add_person.png differ diff --git a/conversations/src/main/res/drawable-xxhdpi/ic_action_chat.png b/conversations/src/main/res/drawable-xxhdpi/ic_action_chat.png new file mode 100644 index 00000000..04000fd0 Binary files /dev/null and b/conversations/src/main/res/drawable-xxhdpi/ic_action_chat.png differ diff --git a/conversations/src/main/res/drawable-xxhdpi/ic_action_copy.png b/conversations/src/main/res/drawable-xxhdpi/ic_action_copy.png new file mode 100644 index 00000000..a0508df8 Binary files /dev/null and b/conversations/src/main/res/drawable-xxhdpi/ic_action_copy.png differ diff --git a/conversations/src/main/res/drawable-xxhdpi/ic_action_discard.png b/conversations/src/main/res/drawable-xxhdpi/ic_action_discard.png new file mode 100644 index 00000000..cb1260a4 Binary files /dev/null and b/conversations/src/main/res/drawable-xxhdpi/ic_action_discard.png differ diff --git a/conversations/src/main/res/drawable-xxhdpi/ic_action_edit.png b/conversations/src/main/res/drawable-xxhdpi/ic_action_edit.png new file mode 100644 index 00000000..3a241ea4 Binary files /dev/null and b/conversations/src/main/res/drawable-xxhdpi/ic_action_edit.png differ diff --git a/conversations/src/main/res/drawable-xxhdpi/ic_action_edit_dark.png b/conversations/src/main/res/drawable-xxhdpi/ic_action_edit_dark.png new file mode 100644 index 00000000..f2b2078b Binary files /dev/null and b/conversations/src/main/res/drawable-xxhdpi/ic_action_edit_dark.png differ diff --git a/conversations/src/main/res/drawable-xxhdpi/ic_action_group.png b/conversations/src/main/res/drawable-xxhdpi/ic_action_group.png new file mode 100644 index 00000000..9289b1c8 Binary files /dev/null and b/conversations/src/main/res/drawable-xxhdpi/ic_action_group.png differ diff --git a/conversations/src/main/res/drawable-xxhdpi/ic_action_new.png b/conversations/src/main/res/drawable-xxhdpi/ic_action_new.png new file mode 100644 index 00000000..c42c2bfb Binary files /dev/null and b/conversations/src/main/res/drawable-xxhdpi/ic_action_new.png differ diff --git a/conversations/src/main/res/drawable-xxhdpi/ic_action_new_attachment.png b/conversations/src/main/res/drawable-xxhdpi/ic_action_new_attachment.png new file mode 100644 index 00000000..ce7536cb Binary files /dev/null and b/conversations/src/main/res/drawable-xxhdpi/ic_action_new_attachment.png differ diff --git a/conversations/src/main/res/drawable-xxhdpi/ic_action_not_secure.png b/conversations/src/main/res/drawable-xxhdpi/ic_action_not_secure.png new file mode 100644 index 00000000..a186f1fb Binary files /dev/null and b/conversations/src/main/res/drawable-xxhdpi/ic_action_not_secure.png differ diff --git a/conversations/src/main/res/drawable-xxhdpi/ic_action_refresh.png b/conversations/src/main/res/drawable-xxhdpi/ic_action_refresh.png new file mode 100644 index 00000000..cb847f37 Binary files /dev/null and b/conversations/src/main/res/drawable-xxhdpi/ic_action_refresh.png differ diff --git a/conversations/src/main/res/drawable-xxhdpi/ic_action_remove.png b/conversations/src/main/res/drawable-xxhdpi/ic_action_remove.png new file mode 100644 index 00000000..331c545b Binary files /dev/null and b/conversations/src/main/res/drawable-xxhdpi/ic_action_remove.png differ diff --git a/conversations/src/main/res/drawable-xxhdpi/ic_action_search.png b/conversations/src/main/res/drawable-xxhdpi/ic_action_search.png new file mode 100644 index 00000000..a1086388 Binary files /dev/null and b/conversations/src/main/res/drawable-xxhdpi/ic_action_search.png differ diff --git a/conversations/src/main/res/drawable-xxhdpi/ic_action_secure.png b/conversations/src/main/res/drawable-xxhdpi/ic_action_secure.png new file mode 100644 index 00000000..ccf1fb00 Binary files /dev/null and b/conversations/src/main/res/drawable-xxhdpi/ic_action_secure.png differ diff --git a/conversations/src/main/res/drawable-xxhdpi/ic_action_send_now_away.png b/conversations/src/main/res/drawable-xxhdpi/ic_action_send_now_away.png new file mode 100644 index 00000000..12ec4d33 Binary files /dev/null and b/conversations/src/main/res/drawable-xxhdpi/ic_action_send_now_away.png differ diff --git a/conversations/src/main/res/drawable-xxhdpi/ic_action_send_now_dnd.png b/conversations/src/main/res/drawable-xxhdpi/ic_action_send_now_dnd.png new file mode 100644 index 00000000..7719f81a Binary files /dev/null and b/conversations/src/main/res/drawable-xxhdpi/ic_action_send_now_dnd.png differ diff --git a/conversations/src/main/res/drawable-xxhdpi/ic_action_send_now_offline.png b/conversations/src/main/res/drawable-xxhdpi/ic_action_send_now_offline.png new file mode 100644 index 00000000..18895813 Binary files /dev/null and b/conversations/src/main/res/drawable-xxhdpi/ic_action_send_now_offline.png differ diff --git a/conversations/src/main/res/drawable-xxhdpi/ic_action_send_now_online.png b/conversations/src/main/res/drawable-xxhdpi/ic_action_send_now_online.png new file mode 100644 index 00000000..29bde36e Binary files /dev/null and b/conversations/src/main/res/drawable-xxhdpi/ic_action_send_now_online.png differ diff --git a/conversations/src/main/res/drawable-xxhdpi/ic_activity.png b/conversations/src/main/res/drawable-xxhdpi/ic_activity.png new file mode 100644 index 00000000..0b642d9b Binary files /dev/null and b/conversations/src/main/res/drawable-xxhdpi/ic_activity.png differ diff --git a/conversations/src/main/res/drawable-xxhdpi/ic_indicator.png b/conversations/src/main/res/drawable-xxhdpi/ic_indicator.png new file mode 100644 index 00000000..2c51b8b7 Binary files /dev/null and b/conversations/src/main/res/drawable-xxhdpi/ic_indicator.png differ diff --git a/conversations/src/main/res/drawable-xxhdpi/ic_launcher.png b/conversations/src/main/res/drawable-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..65c1af34 Binary files /dev/null and b/conversations/src/main/res/drawable-xxhdpi/ic_launcher.png differ diff --git a/conversations/src/main/res/drawable-xxhdpi/ic_notification.png b/conversations/src/main/res/drawable-xxhdpi/ic_notification.png new file mode 100644 index 00000000..ee1e9534 Binary files /dev/null and b/conversations/src/main/res/drawable-xxhdpi/ic_notification.png differ diff --git a/conversations/src/main/res/drawable-xxhdpi/ic_profile.png b/conversations/src/main/res/drawable-xxhdpi/ic_profile.png new file mode 100644 index 00000000..309dc513 Binary files /dev/null and b/conversations/src/main/res/drawable-xxhdpi/ic_profile.png differ diff --git a/conversations/src/main/res/drawable-xxhdpi/ic_received_indicator.png b/conversations/src/main/res/drawable-xxhdpi/ic_received_indicator.png new file mode 100644 index 00000000..039a9ef9 Binary files /dev/null and b/conversations/src/main/res/drawable-xxhdpi/ic_received_indicator.png differ diff --git a/conversations/src/main/res/drawable-xxhdpi/ic_secure_indicator.png b/conversations/src/main/res/drawable-xxhdpi/ic_secure_indicator.png new file mode 100644 index 00000000..1ee9b67d Binary files /dev/null and b/conversations/src/main/res/drawable-xxhdpi/ic_secure_indicator.png differ diff --git a/conversations/src/main/res/drawable-xxhdpi/tab_selected_conversations.9.png b/conversations/src/main/res/drawable-xxhdpi/tab_selected_conversations.9.png new file mode 100644 index 00000000..e4439e7c Binary files /dev/null and b/conversations/src/main/res/drawable-xxhdpi/tab_selected_conversations.9.png differ diff --git a/conversations/src/main/res/drawable-xxhdpi/tab_selected_focused_conversations.9.png b/conversations/src/main/res/drawable-xxhdpi/tab_selected_focused_conversations.9.png new file mode 100644 index 00000000..dd2ded89 Binary files /dev/null and b/conversations/src/main/res/drawable-xxhdpi/tab_selected_focused_conversations.9.png differ diff --git a/conversations/src/main/res/drawable-xxhdpi/tab_selected_pressed_conversations.9.png b/conversations/src/main/res/drawable-xxhdpi/tab_selected_pressed_conversations.9.png new file mode 100644 index 00000000..58c8a576 Binary files /dev/null and b/conversations/src/main/res/drawable-xxhdpi/tab_selected_pressed_conversations.9.png differ diff --git a/conversations/src/main/res/drawable-xxhdpi/tab_unselected_conversations.9.png b/conversations/src/main/res/drawable-xxhdpi/tab_unselected_conversations.9.png new file mode 100644 index 00000000..566062f0 Binary files /dev/null and b/conversations/src/main/res/drawable-xxhdpi/tab_unselected_conversations.9.png differ diff --git a/conversations/src/main/res/drawable-xxhdpi/tab_unselected_focused_conversations.9.png b/conversations/src/main/res/drawable-xxhdpi/tab_unselected_focused_conversations.9.png new file mode 100644 index 00000000..432e68c4 Binary files /dev/null and b/conversations/src/main/res/drawable-xxhdpi/tab_unselected_focused_conversations.9.png differ diff --git a/conversations/src/main/res/drawable-xxhdpi/tab_unselected_pressed_conversations.9.png b/conversations/src/main/res/drawable-xxhdpi/tab_unselected_pressed_conversations.9.png new file mode 100644 index 00000000..8dd01d5c Binary files /dev/null and b/conversations/src/main/res/drawable-xxhdpi/tab_unselected_pressed_conversations.9.png differ diff --git a/conversations/src/main/res/drawable/actionbar_tab_indicator.xml b/conversations/src/main/res/drawable/actionbar_tab_indicator.xml new file mode 100644 index 00000000..5598ee42 --- /dev/null +++ b/conversations/src/main/res/drawable/actionbar_tab_indicator.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/conversations/src/main/res/drawable/es_slidingpane_shadow.xml b/conversations/src/main/res/drawable/es_slidingpane_shadow.xml new file mode 100644 index 00000000..44ffd4ea --- /dev/null +++ b/conversations/src/main/res/drawable/es_slidingpane_shadow.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/conversations/src/main/res/drawable/grey.xml b/conversations/src/main/res/drawable/grey.xml new file mode 100644 index 00000000..2e90d96d --- /dev/null +++ b/conversations/src/main/res/drawable/grey.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/conversations/src/main/res/drawable/greybackground.xml b/conversations/src/main/res/drawable/greybackground.xml new file mode 100644 index 00000000..bedc4b17 --- /dev/null +++ b/conversations/src/main/res/drawable/greybackground.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/conversations/src/main/res/drawable/infocard_border.xml b/conversations/src/main/res/drawable/infocard_border.xml new file mode 100644 index 00000000..af7d5d22 --- /dev/null +++ b/conversations/src/main/res/drawable/infocard_border.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/conversations/src/main/res/drawable/message_border.xml b/conversations/src/main/res/drawable/message_border.xml new file mode 100644 index 00000000..b35693d5 --- /dev/null +++ b/conversations/src/main/res/drawable/message_border.xml @@ -0,0 +1,15 @@ + + + + + + + + + + \ No newline at end of file diff --git a/conversations/src/main/res/drawable/snackbar.xml b/conversations/src/main/res/drawable/snackbar.xml new file mode 100644 index 00000000..13818618 --- /dev/null +++ b/conversations/src/main/res/drawable/snackbar.xml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/conversations/src/main/res/layout-w360dp/fragment_conversations_overview.xml b/conversations/src/main/res/layout-w360dp/fragment_conversations_overview.xml new file mode 100644 index 00000000..a600118d --- /dev/null +++ b/conversations/src/main/res/layout-w360dp/fragment_conversations_overview.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/conversations/src/main/res/layout-w384dp/fragment_conversations_overview.xml b/conversations/src/main/res/layout-w384dp/fragment_conversations_overview.xml new file mode 100644 index 00000000..c3aa67ae --- /dev/null +++ b/conversations/src/main/res/layout-w384dp/fragment_conversations_overview.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/conversations/src/main/res/layout-w600dp/fragment_conversations_overview.xml b/conversations/src/main/res/layout-w600dp/fragment_conversations_overview.xml new file mode 100644 index 00000000..331fb1f0 --- /dev/null +++ b/conversations/src/main/res/layout-w600dp/fragment_conversations_overview.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/conversations/src/main/res/layout-w960dp/fragment_conversations_overview.xml b/conversations/src/main/res/layout-w960dp/fragment_conversations_overview.xml new file mode 100644 index 00000000..2744f38e --- /dev/null +++ b/conversations/src/main/res/layout-w960dp/fragment_conversations_overview.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/conversations/src/main/res/layout/account_row.xml b/conversations/src/main/res/layout/account_row.xml new file mode 100644 index 00000000..2d1190a3 --- /dev/null +++ b/conversations/src/main/res/layout/account_row.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/conversations/src/main/res/layout/actionview_search.xml b/conversations/src/main/res/layout/actionview_search.xml new file mode 100644 index 00000000..64b75f0e --- /dev/null +++ b/conversations/src/main/res/layout/actionview_search.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/conversations/src/main/res/layout/activity_choose_contact.xml b/conversations/src/main/res/layout/activity_choose_contact.xml new file mode 100644 index 00000000..248a7822 --- /dev/null +++ b/conversations/src/main/res/layout/activity_choose_contact.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/conversations/src/main/res/layout/activity_contact_details.xml b/conversations/src/main/res/layout/activity_contact_details.xml new file mode 100644 index 00000000..f7cb2198 --- /dev/null +++ b/conversations/src/main/res/layout/activity_contact_details.xml @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/conversations/src/main/res/layout/activity_edit_account.xml b/conversations/src/main/res/layout/activity_edit_account.xml new file mode 100644 index 00000000..97289628 --- /dev/null +++ b/conversations/src/main/res/layout/activity_edit_account.xml @@ -0,0 +1,272 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +