From 869f01f31ca4fb49bac4c506953604dc65f04925 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Fri, 25 Sep 2009 13:00:41 -0500 Subject: [PATCH] Baseline for work is Dialcentral 1.0.6-10 --- LICENSE | 504 +++++++ Makefile | 102 ++ README | 37 + TODO | 5 + src/__init__.py | 1 + src/alarm_handler.py | 149 ++ src/alarm_notify.py | 140 ++ src/browser_emu.py | 166 +++ src/constants.py | 9 + src/dc_glade.py | 868 ++++++++++++ src/dialcentral.glade | 1292 +++++++++++++++++ src/dialcentral.py | 28 + src/example_custom_notifier.py | 24 + src/file_backend.py | 171 +++ src/gtk_toolbox.py | 741 ++++++++++ src/gv_backend.py | 645 +++++++++ src/gv_views.py | 1483 ++++++++++++++++++++ src/hildonize.py | 646 +++++++++ src/led_handler.py | 24 + src/null_backend.py | 134 ++ src/null_views.py | 206 +++ support/builddeb.py | 282 ++++ support/dialcentral.desktop | 7 + support/fake_py2deb.py | 56 + support/icons/hicolor/26x26/hildon/dialcentral.png | Bin 0 -> 1671 bytes support/icons/hicolor/64x64/hildon/dialcentral.png | Bin 0 -> 6411 bytes .../icons/hicolor/scalable/hildon/dialcentral.png | Bin 0 -> 32182 bytes support/py2deb.py | 955 +++++++++++++ support/pylint.rc | 305 ++++ support/test_syntax.py | 45 + support/todo.py | 104 ++ tests/basic_data/basic.csv | 2 + tests/basic_data/google.csv | 7 + tests/basic_data/grandcentral.csv | 3 + tests/basic_data/settings.ini | 37 + tests/dummy_hildon/hildon.py | 35 + tests/gc_samples/dump_cookies.py | 52 + tests/gc_samples/generate_gc_samples.py | 74 + tests/gv_samples/dump_cookies.py | 52 + tests/gv_samples/generate_gv_samples.py | 81 ++ tests/test_file_backend.py | 155 ++ tests/test_gc_backend.py | 53 + tests/test_gv_backend.py | 55 + tests/test_startup.py | 108 ++ tests/test_utils.py | 130 ++ 45 files changed, 9973 insertions(+) create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README create mode 100644 TODO create mode 100644 src/__init__.py create mode 100644 src/alarm_handler.py create mode 100755 src/alarm_notify.py create mode 100644 src/browser_emu.py create mode 100644 src/constants.py create mode 100755 src/dc_glade.py create mode 100644 src/dialcentral.glade create mode 100755 src/dialcentral.py create mode 100755 src/example_custom_notifier.py create mode 100644 src/file_backend.py create mode 100644 src/gtk_toolbox.py create mode 100644 src/gv_backend.py create mode 100644 src/gv_views.py create mode 100755 src/hildonize.py create mode 100755 src/led_handler.py create mode 100644 src/null_backend.py create mode 100644 src/null_views.py create mode 100755 support/builddeb.py create mode 100644 support/dialcentral.desktop create mode 100644 support/fake_py2deb.py create mode 100644 support/icons/hicolor/26x26/hildon/dialcentral.png create mode 100644 support/icons/hicolor/64x64/hildon/dialcentral.png create mode 100644 support/icons/hicolor/scalable/hildon/dialcentral.png create mode 100644 support/py2deb.py create mode 100644 support/pylint.rc create mode 100755 support/test_syntax.py create mode 100755 support/todo.py create mode 100644 tests/basic_data/basic.csv create mode 100644 tests/basic_data/empty.csv create mode 100644 tests/basic_data/google.csv create mode 100644 tests/basic_data/grandcentral.csv create mode 100644 tests/basic_data/settings.ini create mode 100644 tests/dummy_hildon/__init__.py create mode 100644 tests/dummy_hildon/hildon.py create mode 100644 tests/gc_samples/__init__.py create mode 100755 tests/gc_samples/dump_cookies.py create mode 100755 tests/gc_samples/generate_gc_samples.py create mode 100644 tests/gv_samples/__init__.py create mode 100755 tests/gv_samples/dump_cookies.py create mode 100755 tests/gv_samples/generate_gv_samples.py create mode 100644 tests/test_file_backend.py create mode 100644 tests/test_gc_backend.py create mode 100644 tests/test_gv_backend.py create mode 100644 tests/test_startup.py create mode 100644 tests/test_utils.py diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5ab7695 --- /dev/null +++ b/LICENSE @@ -0,0 +1,504 @@ + 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/Makefile b/Makefile new file mode 100644 index 0000000..9721ca8 --- /dev/null +++ b/Makefile @@ -0,0 +1,102 @@ +PROJECT_NAME=dialcentral +SOURCE_PATH=src +SOURCE=$(shell find $(SOURCE_PATH) -iname "*.py") +PROGRAM=$(SOURCE_PATH)/$(PROJECT_NAME).py +DATA_TYPES=*.ini *.map *.glade *.png +DATA=$(foreach type, $(DATA_TYPES), $(shell find $(SOURCE_PATH) -iname "$(type)")) +OBJ=$(SOURCE:.py=.pyc) +BUILD_PATH=./build +TAG_FILE=~/.ctags/$(PROJECT_NAME).tags +TODO_FILE=./TODO + +DEBUGGER=winpdb +UNIT_TEST=nosetests --with-doctest -w . +SYNTAX_TEST=support/test_syntax.py +STYLE_TEST=../../Python/tools/pep8.py --ignore=W191,E501 +LINT_RC=./support/pylint.rc +LINT=pylint --rcfile=$(LINT_RC) +PROFILE_GEN=python -m cProfile -o .profile +PROFILE_VIEW=python -m pstats .profile +TODO_FINDER=support/todo.py +CTAGS=ctags-exuberant + +.PHONY: all run profile debug test build lint tags todo clean distclean + +all: test + +run: $(OBJ) + $(SOURCE_PATH)/dc_glade.py + +profile: $(OBJ) + $(PROFILE_GEN) $(PROGRAM) + $(PROFILE_VIEW) + +debug: $(OBJ) + $(DEBUGGER) $(PROGRAM) + +test: $(OBJ) + $(UNIT_TEST) + +package: $(OBJ) + rm -Rf $(BUILD_PATH) + mkdir -p $(BUILD_PATH)/generic + cp $(SOURCE_PATH)/constants.py $(BUILD_PATH)/generic + cp $(SOURCE_PATH)/$(PROJECT_NAME).py $(BUILD_PATH)/generic + $(foreach file, $(DATA), cp $(file) $(BUILD_PATH)/generic/$(subst /,-,$(file)) ; ) + $(foreach file, $(SOURCE), cp $(file) $(BUILD_PATH)/generic/$(subst /,-,$(file)) ; ) + #$(foreach file, $(OBJ), cp $(file) $(BUILD_PATH)/generic/$(subst /,-,$(file)) ; ) + cp support/$(PROJECT_NAME).desktop $(BUILD_PATH)/generic + cp support/icons/hicolor/26x26/hildon/$(PROJECT_NAME).png $(BUILD_PATH)/generic/26x26-$(PROJECT_NAME).png + cp support/icons/hicolor/64x64/hildon/$(PROJECT_NAME).png $(BUILD_PATH)/generic/64x64-$(PROJECT_NAME).png + cp support/icons/hicolor/scalable/hildon/$(PROJECT_NAME).png $(BUILD_PATH)/generic/scale-$(PROJECT_NAME).png + cp support/builddeb.py $(BUILD_PATH)/generic + cp support/py2deb.py $(BUILD_PATH)/generic + cp support/fake_py2deb.py $(BUILD_PATH)/generic + mkdir -p $(BUILD_PATH)/chinook + cp -R $(BUILD_PATH)/generic/* $(BUILD_PATH)/chinook + cd $(BUILD_PATH)/chinook ; python builddeb.py chinook + mkdir -p $(BUILD_PATH)/diablo + cp -R $(BUILD_PATH)/generic/* $(BUILD_PATH)/diablo + cd $(BUILD_PATH)/diablo ; python builddeb.py diablo + mkdir -p $(BUILD_PATH)/fremantle + cp -R $(BUILD_PATH)/generic/* $(BUILD_PATH)/fremantle + cd $(BUILD_PATH)/fremantle ; python builddeb.py fremantle + mkdir -p $(BUILD_PATH)/mer + cp -R $(BUILD_PATH)/generic/* $(BUILD_PATH)/mer + cd $(BUILD_PATH)/mer ; python builddeb.py mer + +lint: $(OBJ) + $(foreach file, $(SOURCE), $(LINT) $(file) ; ) + +tags: $(TAG_FILE) + +todo: $(TODO_FILE) + +clean: + rm -Rf $(OBJ) + rm -Rf $(BUILD_PATH) + rm -Rf $(TODO_FILE) + +distclean: + rm -Rf $(OBJ) + rm -Rf $(BUILD_PATH) + rm -Rf $(TAG_FILE) + find $(SOURCE_PATH) -name "*.*~" | xargs rm -f + find $(SOURCE_PATH) -name "*.swp" | xargs rm -f + find $(SOURCE_PATH) -name "*.bak" | xargs rm -f + find $(SOURCE_PATH) -name ".*.swp" | xargs rm -f + +$(TAG_FILE): $(OBJ) + mkdir -p $(dir $(TAG_FILE)) + $(CTAGS) -o $(TAG_FILE) $(SOURCE) + +$(TODO_FILE): $(SOURCE) + @- $(TODO_FINDER) $(SOURCE) > $(TODO_FILE) + +%.pyc: %.py + $(SYNTAX_TEST) $< + +#Makefile Debugging +#Target to print any variable, can be added to the dependencies of any other target +#Userfule flags for make, -d, -p, -n +print-%: ; @$(error $* is $($*) ($(value $*)) (from $(origin $*))) diff --git a/README b/README new file mode 100644 index 0000000..b87ce9c --- /dev/null +++ b/README @@ -0,0 +1,37 @@ +Building a package +=================== +Run + make PLATFORM=... package +which will create a "./pkg-.../..." heirarchy. Move this structure to somewhere on the tablet, then run pypackager. + +Supported PLATFORMs include + desktop + os2007 + os2008 + +SDK Enviroment +=================== + +Native + +Follow install instructions + Ubuntu: http://www.linuxuk.org/node/38 +Install Nokia stuff (for each target) + fakeroot apt-get install maemo-explicit + +Userful commands +Login + /scratchbox/login +Change targets + sb-conf select DIABLO_ARMEL + sb-conf select DIABLO_X86 +Fixing it + fakeroot apt-get -f install + +Starting scratchbox + Xephyr :2 -host-cursor -screen 800x480x16 -dpi 96 -ac -extension Composite + scratchbox + export DISPLAY=:2 + af-sb-init.sh start +Then running a command in the "Maemo" terminal will launch it in the Xephyr session + Tip: run with "run-standalone.sh" for niceness? diff --git a/TODO b/TODO new file mode 100644 index 0000000..25bc1a1 --- /dev/null +++ b/TODO @@ -0,0 +1,5 @@ +src/gv_views.py: +21: @todo Alternate UI for dialogs (stackables) +416: @todo Add multi-SMS messages like GoogleVoice + + diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..4265cc3 --- /dev/null +++ b/src/__init__.py @@ -0,0 +1 @@ +#!/usr/bin/env python diff --git a/src/alarm_handler.py b/src/alarm_handler.py new file mode 100644 index 0000000..d3a9bb9 --- /dev/null +++ b/src/alarm_handler.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python + +import os +import time +import datetime +import ConfigParser + +import dbus +import osso.alarmd as alarmd + + +class AlarmHandler(object): + + _INVALID_COOKIE = -1 + _TITLE = "Dialcentral Notifications" + _LAUNCHER = os.path.abspath(os.path.join(os.path.dirname(__file__), "alarm_notify.py")) + _REPEAT_FOREVER = -1 + _DEFAULT_FLAGS = ( + alarmd.ALARM_EVENT_NO_DIALOG | + alarmd.ALARM_EVENT_NO_SNOOZE | + alarmd.ALARM_EVENT_CONNECTED + ) + + def __init__(self): + self._recurrence = 5 + + bus = dbus.SystemBus() + self._alarmdDBus = bus.get_object("com.nokia.alarmd", "/com/nokia/alarmd"); + self._alarmCookie = self._INVALID_COOKIE + self._launcher = self._LAUNCHER + + def load_settings(self, config, sectionName): + try: + self._recurrence = config.getint(sectionName, "recurrence") + self._alarmCookie = config.getint(sectionName, "alarmCookie") + launcher = config.get(sectionName, "notifier") + if launcher: + self._launcher = launcher + except ConfigParser.NoOptionError: + pass + + def save_settings(self, config, sectionName): + config.set(sectionName, "recurrence", str(self._recurrence)) + config.set(sectionName, "alarmCookie", str(self._alarmCookie)) + launcher = self._launcher if self._launcher != self._LAUNCHER else "" + config.set(sectionName, "notifier", launcher) + + def apply_settings(self, enabled, recurrence): + if recurrence != self._recurrence or enabled != self.isEnabled: + if self.isEnabled: + self._clear_alarm() + if enabled: + self._set_alarm(recurrence) + self._recurrence = int(recurrence) + + @property + def recurrence(self): + return self._recurrence + + @property + def isEnabled(self): + return self._alarmCookie != self._INVALID_COOKIE + + def _get_start_time(self, recurrence): + now = datetime.datetime.now() + startTimeMinute = now.minute + max(recurrence, 5) # being safe + startTimeHour = now.hour + int(startTimeMinute / 60) + startTimeMinute = startTimeMinute % 59 + now.replace(minute=startTimeMinute) + timestamp = int(time.mktime(now.timetuple())) + return timestamp + + def _set_alarm(self, recurrence): + assert 1 <= recurrence, "Notifications set to occur too frequently: %d" % recurrence + alarmTime = self._get_start_time(recurrence) + + #Setup the alarm arguments so that they can be passed to the D-Bus add_event method + action = [] + action.extend(['flags', self._DEFAULT_FLAGS]) + action.extend(['title', self._TITLE]) + action.extend(['path', self._launcher]) + action.extend([ + 'arguments', + dbus.Array( + [alarmTime, int(27)], + signature=dbus.Signature('v') + ) + ]) #int(27) used in place of alarm_index + + event = [] + event.extend([dbus.ObjectPath('/AlarmdEventRecurring'), dbus.UInt32(4)]) + event.extend(['action', dbus.ObjectPath('/AlarmdActionExec')]) #use AlarmdActionExec instead of AlarmdActionDbus + event.append(dbus.UInt32(len(action) / 2)) + event.extend(action) + event.extend(['time', dbus.Int64(alarmTime)]) + event.extend(['recurr_interval', dbus.UInt32(recurrence)]) + event.extend(['recurr_count', dbus.Int32(self._REPEAT_FOREVER)]) + + self._alarmCookie = self._alarmdDBus.add_event(*event); + + def _clear_alarm(self): + if self._alarmCookie == self._INVALID_COOKIE: + return + deleteResult = self._alarmdDBus.del_event(dbus.Int32(self._alarmCookie)) + self._alarmCookie = self._INVALID_COOKIE + assert deleteResult != -1, "Deleting of alarm event failed" + + +def main(): + import ConfigParser + import constants + try: + import optparse + except ImportError: + return + + parser = optparse.OptionParser() + parser.add_option("-x", "--display", action="store_true", dest="display", help="Display data") + parser.add_option("-e", "--enable", action="store_true", dest="enabled", help="Whether the alarm should be enabled or not", default=False) + parser.add_option("-d", "--disable", action="store_false", dest="enabled", help="Whether the alarm should be enabled or not", default=False) + parser.add_option("-r", "--recurrence", action="store", type="int", dest="recurrence", help="How often the alarm occurs", default=5) + (commandOptions, commandArgs) = parser.parse_args() + + alarmHandler = AlarmHandler() + config = ConfigParser.SafeConfigParser() + config.read(constants._user_settings_) + alarmHandler.load_settings(config, "alarm") + + if commandOptions.display: + print "Alarm (%s) is %s for every %d minutes" % ( + alarmHandler._alarmCookie, + "enabled" if alarmHandler.isEnabled else "disabled", + alarmHandler.recurrence, + ) + else: + isEnabled = commandOptions.enabled + recurrence = commandOptions.recurrence + alarmHandler.apply_settings(isEnabled, recurrence) + + alarmHandler.save_settings(config, "alarm") + configFile = open(constants._user_settings_, "wb") + try: + config.write(configFile) + finally: + configFile.close() + + +if __name__ == "__main__": + main() diff --git a/src/alarm_notify.py b/src/alarm_notify.py new file mode 100755 index 0000000..9cb42b8 --- /dev/null +++ b/src/alarm_notify.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python + +import os +import filecmp +import ConfigParser +import pprint + +import constants +import gv_backend + + +def get_missed(backend): + missedPage = backend._browser.download(backend._missedCallsURL) + missedJson = backend._grab_json(missedPage) + return missedJson + + +def get_voicemail(backend): + voicemailPage = backend._browser.download(backend._voicemailURL) + voicemailJson = backend._grab_json(voicemailPage) + return voicemailJson + + +def get_sms(backend): + smsPage = backend._browser.download(backend._smsURL) + smsJson = backend._grab_json(smsPage) + return smsJson + + +def remove_reltime(data): + for messageData in data["messages"].itervalues(): + del messageData["relativeStartTime"] + + +def is_type_changed(backend, type, get_material): + jsonMaterial = get_material(backend) + unreadCount = jsonMaterial["unreadCounts"][type] + + previousSnapshotPath = os.path.join(constants._data_path_, "snapshot_%s.old.json" % type) + currentSnapshotPath = os.path.join(constants._data_path_, "snapshot_%s.json" % type) + + try: + os.remove(previousSnapshotPath) + except OSError, e: + # check if failed purely because the old file didn't exist, which is fine + if e.errno != 2: + raise + try: + os.rename(currentSnapshotPath, previousSnapshotPath) + previousExists = True + except OSError, e: + # check if failed purely because the new old file didn't exist, which is fine + if e.errno != 2: + raise + previousExists = False + + remove_reltime(jsonMaterial) + textMaterial = pprint.pformat(jsonMaterial) + currentSnapshot = file(currentSnapshotPath, "w") + try: + currentSnapshot.write(textMaterial) + finally: + currentSnapshot.close() + + if unreadCount == 0 or not previousExists: + return False + + seemEqual = filecmp.cmp(previousSnapshotPath, currentSnapshotPath) + return not seemEqual + + +def is_changed(): + gvCookiePath = os.path.join(constants._data_path_, "gv_cookies.txt") + backend = gv_backend.GVDialer(gvCookiePath) + + loggedIn = False + + if not loggedIn: + loggedIn = backend.is_authed() + + config = ConfigParser.SafeConfigParser() + config.read(constants._user_settings_) + if not loggedIn: + import base64 + try: + blobs = ( + config.get(constants.__pretty_app_name__, "bin_blob_%i" % i) + for i in xrange(2) + ) + creds = ( + base64.b64decode(blob) + for blob in blobs + ) + username, password = tuple(creds) + loggedIn = backend.login(username, password) + except ConfigParser.NoOptionError, e: + pass + except ConfigParser.NoSectionError, e: + pass + + try: + notifyOnMissed = config.getboolean("2 - Account Info", "notifyOnMissed") + notifyOnVoicemail = config.getboolean("2 - Account Info", "notifyOnVoicemail") + notifyOnSms = config.getboolean("2 - Account Info", "notifyOnSms") + except ConfigParser.NoOptionError, e: + notifyOnMissed = False + notifyOnVoicemail = False + notifyOnSms = False + except ConfigParser.NoSectionError, e: + notifyOnMissed = False + notifyOnVoicemail = False + notifyOnSms = False + + assert loggedIn + notifySources = [] + if notifyOnMissed: + notifySources.append(("missed", get_missed)) + if notifyOnVoicemail: + notifySources.append(("voicemail", get_voicemail)) + if notifyOnSms: + notifySources.append(("sms", get_sms)) + + notifyUser = False + for type, get_material in notifySources: + if is_type_changed(backend, type, get_material): + notifyUser = True + return notifyUser + + +def notify_on_change(): + notifyUser = is_changed() + + if notifyUser: + import led_handler + led = led_handler.LedHandler() + led.on() + + +if __name__ == "__main__": + notify_on_change() diff --git a/src/browser_emu.py b/src/browser_emu.py new file mode 100644 index 0000000..88a0b62 --- /dev/null +++ b/src/browser_emu.py @@ -0,0 +1,166 @@ +""" +@author: Laszlo Nagy +@copyright: (c) 2005 by Szoftver Messias Bt. +@licence: BSD style + +Objects of the MozillaEmulator class can emulate a browser that is capable of: + + - cookie management + - configurable user agent string + - GET and POST + - multipart POST (send files) + - receive content into file + +I have seen many requests on the python mailing list about how to emulate a browser. I'm using this class for years now, without any problems. This is how you can use it: + + 1. Use firefox + 2. Install and open the livehttpheaders plugin + 3. Use the website manually with firefox + 4. Check the GET and POST requests in the livehttpheaders capture window + 5. Create an instance of the above class and send the same GET and POST requests to the server. + +Optional steps: + + - You can change user agent string in the build_opened method + - The "encode_multipart_formdata" function can be used alone to create POST data from a list of field values and files +""" + +import urllib2 +import cookielib +import logging + +import socket + + +socket.setdefaulttimeout(10) + + +class MozillaEmulator(object): + + def __init__(self, trycount = 1): + """Create a new MozillaEmulator object. + + @param trycount: The download() method will retry the operation if it fails. You can specify -1 for infinite retrying. + A value of 0 means no retrying. A value of 1 means one retry. etc.""" + self.cookies = cookielib.LWPCookieJar() + self.debug = False + self.trycount = trycount + + def build_opener(self, url, postdata = None, extraheaders = None, forbid_redirect = False): + if extraheaders is None: + extraheaders = {} + + txheaders = { + 'Accept': 'text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png', + 'Accept-Language': 'en,en-us;q=0.5', + 'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.7', + } + for key, value in extraheaders.iteritems(): + txheaders[key] = value + req = urllib2.Request(url, postdata, txheaders) + self.cookies.add_cookie_header(req) + if forbid_redirect: + redirector = HTTPNoRedirector() + else: + redirector = urllib2.HTTPRedirectHandler() + + http_handler = urllib2.HTTPHandler(debuglevel=self.debug) + https_handler = urllib2.HTTPSHandler(debuglevel=self.debug) + + u = urllib2.build_opener( + http_handler, + https_handler, + urllib2.HTTPCookieProcessor(self.cookies), + redirector + ) + u.addheaders = [( + 'User-Agent', + 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7.8) Gecko/20050511 Firefox/1.0.4' + )] + if not postdata is None: + req.add_data(postdata) + return (req, u) + + def download(self, url, + postdata = None, extraheaders = None, forbid_redirect = False, + trycount = None, only_head = False, + ): + """Download an URL with GET or POST methods. + + @param postdata: It can be a string that will be POST-ed to the URL. + When None is given, the method will be GET instead. + @param extraheaders: You can add/modify HTTP headers with a dict here. + @param forbid_redirect: Set this flag if you do not want to handle + HTTP 301 and 302 redirects. + @param trycount: Specify the maximum number of retries here. + 0 means no retry on error. Using -1 means infinite retring. + None means the default value (that is self.trycount). + @param only_head: Create the openerdirector and return it. In other + words, this will not retrieve any content except HTTP headers. + + @return: The raw HTML page data + """ + logging.warning("Performing download of %s" % url) + + if extraheaders is None: + extraheaders = {} + if trycount is None: + trycount = self.trycount + cnt = 0 + + while True: + try: + req, u = self.build_opener(url, postdata, extraheaders, forbid_redirect) + openerdirector = u.open(req) + if self.debug: + print req.get_method(), url + print openerdirector.code, openerdirector.msg + print openerdirector.headers + self.cookies.extract_cookies(openerdirector, req) + if only_head: + return openerdirector + + return self._read(openerdirector, trycount) + except urllib2.URLError: + cnt += 1 + if (-1 < trycount) and (trycount < cnt): + raise + + # Retry :-) + if self.debug: + print "MozillaEmulator: urllib2.URLError, retryting ", cnt + + def _read(self, openerdirector, trycount): + chunks = [] + + chunk = openerdirector.read() + chunks.append(chunk) + #while chunk and cnt < trycount: + # time.sleep(1) + # cnt += 1 + # chunk = openerdirector.read() + # chunks.append(chunk) + + data = "".join(chunks) + + if "Content-Length" in openerdirector.info(): + assert len(data) == int(openerdirector.info()["Content-Length"]), "The packet header promised %s of data but only was able to read %s of data" % ( + openerdirector.info()["Content-Length"], + len(data), + ) + + return data + + +class HTTPNoRedirector(urllib2.HTTPRedirectHandler): + """This is a custom http redirect handler that FORBIDS redirection.""" + + def http_error_302(self, req, fp, code, msg, headers): + e = urllib2.HTTPError(req.get_full_url(), code, msg, headers, fp) + if e.code in (301, 302): + if 'location' in headers: + newurl = headers.getheaders('location')[0] + elif 'uri' in headers: + newurl = headers.getheaders('uri')[0] + e.newurl = newurl + raise e diff --git a/src/constants.py b/src/constants.py new file mode 100644 index 0000000..8d1359b --- /dev/null +++ b/src/constants.py @@ -0,0 +1,9 @@ +import os + +__pretty_app_name__ = "DialCentral" +__app_name__ = "dialcentral" +__version__ = "1.0.6" +__build__ = 10 +__app_magic__ = 0xdeadbeef +_data_path_ = os.path.join(os.path.expanduser("~"), ".dialcentral") +_user_settings_ = "%s/settings.ini" % _data_path_ diff --git a/src/dc_glade.py b/src/dc_glade.py new file mode 100755 index 0000000..b23521a --- /dev/null +++ b/src/dc_glade.py @@ -0,0 +1,868 @@ +#!/usr/bin/python2.5 + +""" +DialCentral - Front end for Google's GoogleVoice service. +Copyright (C) 2008 Mark Bergman bergman AT merctech DOT com + +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 + +@todo Add "login failed" and "attempting login" notifications +""" + + +from __future__ import with_statement + +import sys +import gc +import os +import threading +import base64 +import ConfigParser +import itertools +import logging + +import gtk +import gtk.glade + +import constants +import hildonize +import gtk_toolbox + + +def getmtime_nothrow(path): + try: + return os.path.getmtime(path) + except Exception: + return 0 + + +def display_error_message(msg): + error_dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg) + + def close(dialog, response): + dialog.destroy() + error_dialog.connect("response", close) + error_dialog.run() + + +class Dialcentral(object): + + _glade_files = [ + os.path.join(os.path.dirname(__file__), "dialcentral.glade"), + os.path.join(os.path.dirname(__file__), "../lib/dialcentral.glade"), + '/usr/lib/dialcentral/dialcentral.glade', + ] + + KEYPAD_TAB = 0 + RECENT_TAB = 1 + MESSAGES_TAB = 2 + CONTACTS_TAB = 3 + ACCOUNT_TAB = 4 + + NULL_BACKEND = 0 + GV_BACKEND = 2 + BACKENDS = (NULL_BACKEND, GV_BACKEND) + + def __init__(self): + self._initDone = False + self._connection = None + self._osso = None + self._clipboard = gtk.clipboard_get() + + self._credentials = ("", "") + self._selectedBackendId = self.NULL_BACKEND + self._defaultBackendId = self.GV_BACKEND + self._phoneBackends = None + self._dialpads = None + self._accountViews = None + self._messagesViews = None + self._recentViews = None + self._contactsViews = None + self._alarmHandler = None + self._ledHandler = None + self._originalCurrentLabels = [] + + for path in self._glade_files: + if os.path.isfile(path): + self._widgetTree = gtk.glade.XML(path) + break + else: + display_error_message("Cannot find dialcentral.glade") + gtk.main_quit() + return + + self._window = self._widgetTree.get_widget("mainWindow") + self._notebook = self._widgetTree.get_widget("notebook") + self._errorDisplay = gtk_toolbox.ErrorDisplay(self._widgetTree) + self._credentialsDialog = gtk_toolbox.LoginWindow(self._widgetTree) + + self._isFullScreen = False + self._app = hildonize.get_app_class()() + self._window = hildonize.hildonize_window(self._app, self._window) + hildonize.hildonize_text_entry(self._widgetTree.get_widget("usernameentry")) + hildonize.hildonize_password_entry(self._widgetTree.get_widget("passwordentry")) + + for scrollingWidget in ( + 'recent_scrolledwindow', + 'message_scrolledwindow', + 'contacts_scrolledwindow', + "phoneSelectionMessages_scrolledwindow", + "smsMessages_scrolledwindow", + ): + hildonize.hildonize_scrollwindow(self._widgetTree.get_widget(scrollingWidget)) + for scrollingWidget in ( + "phonetypes_scrolledwindow", + "smsMessage_scrolledEntry", + ): + hildonize.hildonize_scrollwindow_with_viewport(self._widgetTree.get_widget(scrollingWidget)) + + replacementButtons = [gtk.Button("Test")] + menu = hildonize.hildonize_menu( + self._window, + self._widgetTree.get_widget("dialpad_menubar"), + replacementButtons + ) + + self._window.connect("key-press-event", self._on_key_press) + self._window.connect("window-state-event", self._on_window_state_change) + if not hildonize.IS_HILDON_SUPPORTED: + logging.warning("No hildonization support") + + hildonize.set_application_title(self._window, "%s" % constants.__pretty_app_name__) + + self._window.connect("destroy", self._on_close) + self._window.set_default_size(800, 300) + self._window.show_all() + + self._loginSink = gtk_toolbox.threaded_stage( + gtk_toolbox.comap( + self._attempt_login, + gtk_toolbox.null_sink(), + ) + ) + + backgroundSetup = threading.Thread(target=self._idle_setup) + backgroundSetup.setDaemon(True) + backgroundSetup.start() + + def _idle_setup(self): + """ + If something can be done after the UI loads, push it here so it's not blocking the UI + """ + # Barebones UI handlers + try: + import null_backend + import null_views + + self._phoneBackends = {self.NULL_BACKEND: null_backend.NullDialer()} + with gtk_toolbox.gtk_lock(): + self._dialpads = {self.NULL_BACKEND: null_views.Dialpad(self._widgetTree)} + self._accountViews = {self.NULL_BACKEND: null_views.AccountInfo(self._widgetTree)} + self._recentViews = {self.NULL_BACKEND: null_views.RecentCallsView(self._widgetTree)} + self._messagesViews = {self.NULL_BACKEND: null_views.MessagesView(self._widgetTree)} + self._contactsViews = {self.NULL_BACKEND: null_views.ContactsView(self._widgetTree)} + + self._dialpads[self._selectedBackendId].enable() + self._accountViews[self._selectedBackendId].enable() + self._recentViews[self._selectedBackendId].enable() + self._messagesViews[self._selectedBackendId].enable() + self._contactsViews[self._selectedBackendId].enable() + except Exception, e: + with gtk_toolbox.gtk_lock(): + self._errorDisplay.push_exception() + + # Setup maemo specifics + try: + try: + import osso + except (ImportError, OSError): + osso = None + self._osso = None + if osso is not None: + self._osso = osso.Context(constants.__app_name__, constants.__version__, False) + device = osso.DeviceState(self._osso) + device.set_device_state_callback(self._on_device_state_change, 0) + else: + logging.warning("No device state support") + + try: + import alarm_handler + self._alarmHandler = alarm_handler.AlarmHandler() + except (ImportError, OSError): + alarm_handler = None + except Exception: + with gtk_toolbox.gtk_lock(): + self._errorDisplay.push_exception() + alarm_handler = None + logging.warning("No notification support") + if hildonize.IS_HILDON_SUPPORTED: + try: + import led_handler + self._ledHandler = led_handler.LedHandler() + except Exception, e: + logging.exception('LED Handling failed: "%s"' % str(e)) + self._ledHandler = None + else: + self._ledHandler = None + + try: + import conic + except (ImportError, OSError): + conic = None + self._connection = None + if conic is not None: + self._connection = conic.Connection() + self._connection.connect("connection-event", self._on_connection_change, constants.__app_magic__) + self._connection.request_connection(conic.CONNECT_FLAG_NONE) + else: + logging.warning("No connection support") + except Exception, e: + with gtk_toolbox.gtk_lock(): + self._errorDisplay.push_exception() + + # Setup costly backends + try: + import gv_backend + import file_backend + import gv_views + + try: + os.makedirs(constants._data_path_) + except OSError, e: + if e.errno != 17: + raise + gvCookiePath = os.path.join(constants._data_path_, "gv_cookies.txt") + + self._phoneBackends.update({ + self.GV_BACKEND: gv_backend.GVDialer(gvCookiePath), + }) + with gtk_toolbox.gtk_lock(): + unifiedDialpad = gv_views.Dialpad(self._widgetTree, self._errorDisplay) + self._dialpads.update({ + self.GV_BACKEND: unifiedDialpad, + }) + self._accountViews.update({ + self.GV_BACKEND: gv_views.AccountInfo( + self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._alarmHandler, self._errorDisplay + ), + }) + self._accountViews[self.GV_BACKEND].save_everything = self._save_settings + self._recentViews.update({ + self.GV_BACKEND: gv_views.RecentCallsView( + self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay + ), + }) + self._messagesViews.update({ + self.GV_BACKEND: gv_views.MessagesView( + self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay + ), + }) + self._contactsViews.update({ + self.GV_BACKEND: gv_views.ContactsView( + self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay + ), + }) + + fsContactsPath = os.path.join(constants._data_path_, "contacts") + fileBackend = file_backend.FilesystemAddressBookFactory(fsContactsPath) + + self._dialpads[self.GV_BACKEND].number_selected = self._select_action + self._recentViews[self.GV_BACKEND].number_selected = self._select_action + self._messagesViews[self.GV_BACKEND].number_selected = self._select_action + self._contactsViews[self.GV_BACKEND].number_selected = self._select_action + + addressBooks = [ + self._phoneBackends[self.GV_BACKEND], + fileBackend, + ] + mergedBook = gv_views.MergedAddressBook(addressBooks, gv_views.MergedAddressBook.advanced_lastname_sorter) + self._contactsViews[self.GV_BACKEND].append(mergedBook) + self._contactsViews[self.GV_BACKEND].extend(addressBooks) + self._contactsViews[self.GV_BACKEND].open_addressbook(*self._contactsViews[self.GV_BACKEND].get_addressbooks().next()[0][0:2]) + + callbackMapping = { + "on_paste": self._on_paste, + "on_refresh": self._on_menu_refresh, + "on_clearcookies_clicked": self._on_clearcookies_clicked, + "on_about_activate": self._on_about_activate, + } + if hildonize.GTK_MENU_USED: + self._widgetTree.signal_autoconnect(callbackMapping) + self._notebook.connect("switch-page", self._on_notebook_switch_page) + self._widgetTree.get_widget("clearcookies").connect("clicked", self._on_clearcookies_clicked) + + with gtk_toolbox.gtk_lock(): + self._originalCurrentLabels = [ + self._notebook.get_tab_label(self._notebook.get_nth_page(pageIndex)).get_text() + for pageIndex in xrange(self._notebook.get_n_pages()) + ] + self._notebookTapHandler = gtk_toolbox.TapOrHold(self._notebook) + self._notebookTapHandler.enable() + self._notebookTapHandler.on_tap = self._reset_tab_refresh + self._notebookTapHandler.on_hold = self._on_tab_refresh + self._notebookTapHandler.on_holding = self._set_tab_refresh + self._notebookTapHandler.on_cancel = self._reset_tab_refresh + + self._initDone = True + + config = ConfigParser.SafeConfigParser() + config.read(constants._user_settings_) + with gtk_toolbox.gtk_lock(): + self.load_settings(config) + except Exception, e: + with gtk_toolbox.gtk_lock(): + self._errorDisplay.push_exception() + finally: + self._spawn_attempt_login(2) + + def _spawn_attempt_login(self, *args): + self._loginSink.send(args) + + def _attempt_login(self, numOfAttempts = 10, force = False): + """ + @note This must be run outside of the UI lock + """ + try: + assert 0 <= numOfAttempts, "That was pointless having 0 or less login attempts" + assert self._initDone, "Attempting login before app is fully loaded" + + serviceId = self.NULL_BACKEND + loggedIn = False + if not force: + try: + self.refresh_session() + serviceId = self._defaultBackendId + loggedIn = True + except Exception, e: + logging.exception('Session refresh failed with the following message "%s"' % str(e)) + + if not loggedIn: + loggedIn, serviceId = self._login_by_user(numOfAttempts) + + with gtk_toolbox.gtk_lock(): + self._change_loggedin_status(serviceId) + if loggedIn: + hildonize.show_information_banner(self._window, "Logged In") + except Exception, e: + with gtk_toolbox.gtk_lock(): + self._errorDisplay.push_exception() + + def refresh_session(self): + """ + @note Thread agnostic + """ + assert self._initDone, "Attempting login before app is fully loaded" + + loggedIn = False + if not loggedIn: + loggedIn = self._login_by_cookie() + if not loggedIn: + loggedIn = self._login_by_settings() + + if not loggedIn: + raise RuntimeError("Login Failed") + + def _login_by_cookie(self): + """ + @note Thread agnostic + """ + loggedIn = self._phoneBackends[self._defaultBackendId].is_authed() + if loggedIn: + logging.info("Logged into %r through cookies" % self._phoneBackends[self._defaultBackendId]) + return loggedIn + + def _login_by_settings(self): + """ + @note Thread agnostic + """ + username, password = self._credentials + loggedIn = self._phoneBackends[self._defaultBackendId].login(username, password) + if loggedIn: + self._credentials = username, password + logging.info("Logged into %r through settings" % self._phoneBackends[self._defaultBackendId]) + return loggedIn + + def _login_by_user(self, numOfAttempts): + """ + @note This must be run outside of the UI lock + """ + loggedIn, (username, password) = False, self._credentials + tmpServiceId = self.GV_BACKEND + for attemptCount in xrange(numOfAttempts): + if loggedIn: + break + with gtk_toolbox.gtk_lock(): + credentials = self._credentialsDialog.request_credentials( + defaultCredentials = self._credentials + ) + if not self._phoneBackends[tmpServiceId].get_callback_number(): + # subtle reminder to the users to configure things + self._notebook.set_current_page(self.ACCOUNT_TAB) + username, password = credentials + loggedIn = self._phoneBackends[tmpServiceId].login(username, password) + + if loggedIn: + serviceId = tmpServiceId + self._credentials = username, password + logging.info("Logged into %r through user request" % self._phoneBackends[serviceId]) + else: + serviceId = self.NULL_BACKEND + self._notebook.set_current_page(self.ACCOUNT_TAB) + + return loggedIn, serviceId + + def _select_action(self, action, number, message): + self.refresh_session() + if action == "select": + self._dialpads[self._selectedBackendId].set_number(number) + self._notebook.set_current_page(self.KEYPAD_TAB) + elif action == "dial": + self._on_dial_clicked(number) + elif action == "sms": + self._on_sms_clicked(number, message) + else: + assert False, "Unknown action: %s" % action + + def _change_loggedin_status(self, newStatus): + oldStatus = self._selectedBackendId + if oldStatus == newStatus: + return + + self._dialpads[oldStatus].disable() + self._accountViews[oldStatus].disable() + self._recentViews[oldStatus].disable() + self._messagesViews[oldStatus].disable() + self._contactsViews[oldStatus].disable() + + self._dialpads[newStatus].enable() + self._accountViews[newStatus].enable() + self._recentViews[newStatus].enable() + self._messagesViews[newStatus].enable() + self._contactsViews[newStatus].enable() + + if self._phoneBackends[self._selectedBackendId].get_callback_number() is None: + self._phoneBackends[self._selectedBackendId].set_sane_callback() + + self._selectedBackendId = newStatus + + self._accountViews[self._selectedBackendId].update() + self._refresh_active_tab() + + def load_settings(self, config): + """ + @note UI Thread + """ + try: + self._defaultBackendId = config.getint(constants.__pretty_app_name__, "active") + blobs = ( + config.get(constants.__pretty_app_name__, "bin_blob_%i" % i) + for i in xrange(len(self._credentials)) + ) + creds = ( + base64.b64decode(blob) + for blob in blobs + ) + self._credentials = tuple(creds) + + if self._alarmHandler is not None: + self._alarmHandler.load_settings(config, "alarm") + except ConfigParser.NoOptionError, e: + logging.exception( + "Settings file %s is missing section %s" % ( + constants._user_settings_, + e.section, + ), + ) + except ConfigParser.NoSectionError, e: + logging.exception( + "Settings file %s is missing section %s" % ( + constants._user_settings_, + e.section, + ), + ) + + for backendId, view in itertools.chain( + self._dialpads.iteritems(), + self._accountViews.iteritems(), + self._messagesViews.iteritems(), + self._recentViews.iteritems(), + self._contactsViews.iteritems(), + ): + sectionName = "%s - %s" % (backendId, view.name()) + try: + view.load_settings(config, sectionName) + except ConfigParser.NoOptionError, e: + logging.exception( + "Settings file %s is missing section %s" % ( + constants._user_settings_, + e.section, + ), + ) + except ConfigParser.NoSectionError, e: + logging.exception( + "Settings file %s is missing section %s" % ( + constants._user_settings_, + e.section, + ), + ) + + try: + previousOrientation = config.getint(constants.__pretty_app_name__, "orientation") + if previousOrientation == gtk.ORIENTATION_HORIZONTAL: + hildonize.window_to_landscape(self._window) + elif previousOrientation == gtk.ORIENTATION_VERTICAL: + hildonize.window_to_portrait(self._window) + except ConfigParser.NoOptionError, e: + logging.exception( + "Settings file %s is missing section %s" % ( + constants._user_settings_, + e.section, + ), + ) + except ConfigParser.NoSectionError, e: + logging.exception( + "Settings file %s is missing section %s" % ( + constants._user_settings_, + e.section, + ), + ) + + def save_settings(self, config): + """ + @note Thread Agnostic + """ + config.add_section(constants.__pretty_app_name__) + config.set(constants.__pretty_app_name__, "active", str(self._selectedBackendId)) + config.set(constants.__pretty_app_name__, "orientation", str(int(gtk_toolbox.get_screen_orientation()))) + for i, value in enumerate(self._credentials): + blob = base64.b64encode(value) + config.set(constants.__pretty_app_name__, "bin_blob_%i" % i, blob) + config.add_section("alarm") + if self._alarmHandler is not None: + self._alarmHandler.save_settings(config, "alarm") + + for backendId, view in itertools.chain( + self._dialpads.iteritems(), + self._accountViews.iteritems(), + self._messagesViews.iteritems(), + self._recentViews.iteritems(), + self._contactsViews.iteritems(), + ): + sectionName = "%s - %s" % (backendId, view.name()) + config.add_section(sectionName) + view.save_settings(config, sectionName) + + def _save_settings(self): + """ + @note Thread Agnostic + """ + config = ConfigParser.SafeConfigParser() + self.save_settings(config) + with open(constants._user_settings_, "wb") as configFile: + config.write(configFile) + + def _refresh_active_tab(self): + pageIndex = self._notebook.get_current_page() + if pageIndex == self.CONTACTS_TAB: + self._contactsViews[self._selectedBackendId].update(force=True) + elif pageIndex == self.RECENT_TAB: + self._recentViews[self._selectedBackendId].update(force=True) + elif pageIndex == self.MESSAGES_TAB: + self._messagesViews[self._selectedBackendId].update(force=True) + + if pageIndex in (self.RECENT_TAB, self.MESSAGES_TAB): + if self._ledHandler is not None: + self._ledHandler.off() + + def _on_close(self, *args, **kwds): + try: + if self._osso is not None: + self._osso.close() + + if self._initDone: + self._save_settings() + finally: + gtk.main_quit() + + def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData): + """ + For shutdown or save_unsaved_data, our only state is cookies and I think the cookie manager handles that for us. + For system_inactivity, we have no background tasks to pause + + @note Hildon specific + """ + try: + if memory_low: + for backendId in self.BACKENDS: + self._phoneBackends[backendId].clear_caches() + self._contactsViews[self._selectedBackendId].clear_caches() + gc.collect() + + if save_unsaved_data or shutdown: + self._save_settings() + except Exception, e: + self._errorDisplay.push_exception() + + def _on_connection_change(self, connection, event, magicIdentifier): + """ + @note Hildon specific + """ + try: + import conic + + status = event.get_status() + error = event.get_error() + iap_id = event.get_iap_id() + bearer = event.get_bearer_type() + + if status == conic.STATUS_CONNECTED: + if self._initDone: + self._spawn_attempt_login(2) + elif status == conic.STATUS_DISCONNECTED: + if self._initDone: + self._defaultBackendId = self._selectedBackendId + self._change_loggedin_status(self.NULL_BACKEND) + except Exception, e: + self._errorDisplay.push_exception() + + def _on_window_state_change(self, widget, event, *args): + """ + @note Hildon specific + """ + try: + if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN: + self._isFullScreen = True + else: + self._isFullScreen = False + except Exception, e: + self._errorDisplay.push_exception() + + def _on_key_press(self, widget, event, *args): + """ + @note Hildon specific + """ + try: + if ( + event.keyval == gtk.keysyms.F6 or + event.keyval == gtk.keysyms.Return and event.get_state() & gtk.gdk.CONTROL_MASK + ): + if self._isFullScreen: + self._window.unfullscreen() + else: + self._window.fullscreen() + except Exception, e: + self._errorDisplay.push_exception() + + def _on_clearcookies_clicked(self, *args): + try: + self._phoneBackends[self._selectedBackendId].logout() + self._accountViews[self._selectedBackendId].clear() + self._recentViews[self._selectedBackendId].clear() + self._messagesViews[self._selectedBackendId].clear() + self._contactsViews[self._selectedBackendId].clear() + self._change_loggedin_status(self.NULL_BACKEND) + + self._spawn_attempt_login(2, True) + except Exception, e: + self._errorDisplay.push_exception() + + def _on_notebook_switch_page(self, notebook, page, pageIndex): + try: + self._reset_tab_refresh() + + didRecentUpdate = False + didMessagesUpdate = False + + if pageIndex == self.RECENT_TAB: + didRecentUpdate = self._recentViews[self._selectedBackendId].update() + elif pageIndex == self.MESSAGES_TAB: + didMessagesUpdate = self._messagesViews[self._selectedBackendId].update() + elif pageIndex == self.CONTACTS_TAB: + self._contactsViews[self._selectedBackendId].update() + elif pageIndex == self.ACCOUNT_TAB: + self._accountViews[self._selectedBackendId].update() + + if didRecentUpdate or didMessagesUpdate: + if self._ledHandler is not None: + self._ledHandler.off() + except Exception, e: + self._errorDisplay.push_exception() + + def _set_tab_refresh(self, *args): + try: + pageIndex = self._notebook.get_current_page() + child = self._notebook.get_nth_page(pageIndex) + self._notebook.get_tab_label(child).set_text("Refresh?") + except Exception, e: + self._errorDisplay.push_exception() + return False + + def _reset_tab_refresh(self, *args): + try: + pageIndex = self._notebook.get_current_page() + child = self._notebook.get_nth_page(pageIndex) + self._notebook.get_tab_label(child).set_text(self._originalCurrentLabels[pageIndex]) + except Exception, e: + self._errorDisplay.push_exception() + return False + + def _on_tab_refresh(self, *args): + try: + self._refresh_active_tab() + self._reset_tab_refresh() + except Exception, e: + self._errorDisplay.push_exception() + return False + + def _on_sms_clicked(self, number, message): + try: + assert number, "No number specified" + assert message, "Empty message" + try: + loggedIn = self._phoneBackends[self._selectedBackendId].is_authed() + except Exception, e: + loggedIn = False + self._errorDisplay.push_exception() + return + + if not loggedIn: + self._errorDisplay.push_message( + "Backend link with GoogleVoice is not working, please try again" + ) + return + + dialed = False + try: + self._phoneBackends[self._selectedBackendId].send_sms(number, message) + hildonize.show_information_banner(self._window, "Sending to %s" % number) + dialed = True + except Exception, e: + self._errorDisplay.push_exception() + + if dialed: + self._dialpads[self._selectedBackendId].clear() + except Exception, e: + self._errorDisplay.push_exception() + + def _on_dial_clicked(self, number): + try: + assert number, "No number to call" + try: + loggedIn = self._phoneBackends[self._selectedBackendId].is_authed() + except Exception, e: + loggedIn = False + self._errorDisplay.push_exception() + return + + if not loggedIn: + self._errorDisplay.push_message( + "Backend link with GoogleVoice is not working, please try again" + ) + return + + dialed = False + try: + assert self._phoneBackends[self._selectedBackendId].get_callback_number() != "", "No callback number specified" + self._phoneBackends[self._selectedBackendId].dial(number) + hildonize.show_information_banner(self._window, "Calling %s" % number) + dialed = True + except Exception, e: + self._errorDisplay.push_exception() + + if dialed: + self._dialpads[self._selectedBackendId].clear() + except Exception, e: + self._errorDisplay.push_exception() + + def _on_menu_refresh(self, *args): + try: + self._refresh_active_tab() + except Exception, e: + self._errorDisplay.push_exception() + + def _on_paste(self, *args): + try: + contents = self._clipboard.wait_for_text() + if contents is not None: + self._dialpads[self._selectedBackendId].set_number(contents) + except Exception, e: + self._errorDisplay.push_exception() + + def _on_about_activate(self, *args): + try: + dlg = gtk.AboutDialog() + dlg.set_name(constants.__pretty_app_name__) + dlg.set_version("%s-%d" % (constants.__version__, constants.__build__)) + dlg.set_copyright("Copyright 2008 - LGPL") + dlg.set_comments("Dialcentral is a touch screen enhanced interface to your GoogleVoice account. This application is not affiliated with Google in any way") + dlg.set_website("http://gc-dialer.garage.maemo.org/") + dlg.set_authors(["", "Eric Warnke ", "Ed Page "]) + dlg.run() + dlg.destroy() + except Exception, e: + self._errorDisplay.push_exception() + + +def run_doctest(): + import doctest + + failureCount, testCount = doctest.testmod() + if not failureCount: + print "Tests Successful" + sys.exit(0) + else: + sys.exit(1) + + +def run_dialpad(): + _lock_file = os.path.join(constants._data_path_, ".lock") + #with gtk_toolbox.flock(_lock_file, 0): + gtk.gdk.threads_init() + + if hildonize.IS_HILDON_SUPPORTED: + gtk.set_application_name(constants.__pretty_app_name__) + handle = Dialcentral() + gtk.main() + + +class DummyOptions(object): + + def __init__(self): + self.test = False + + +if __name__ == "__main__": + logging.basicConfig(level=logging.DEBUG) + try: + if len(sys.argv) > 1: + try: + import optparse + except ImportError: + optparse = None + + if optparse is not None: + parser = optparse.OptionParser() + parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests") + (commandOptions, commandArgs) = parser.parse_args() + else: + commandOptions = DummyOptions() + commandArgs = [] + + if commandOptions.test: + run_doctest() + else: + run_dialpad() + finally: + logging.shutdown() diff --git a/src/dialcentral.glade b/src/dialcentral.glade new file mode 100644 index 0000000..ba8d0ff --- /dev/null +++ b/src/dialcentral.glade @@ -0,0 +1,1292 @@ + + + + + + Dialer + 800 + 480 + + + True + + + True + + + True + _Login + True + + + + + + True + Paste + True + + + + + + True + Refresh + True + + + + + + True + _About + True + + + + + + False + 0 + + + + + True + + + True + + + True + gtk-dialog-error + + + False + False + 0 + + + + + True + True + end + True + + + 1 + + + + + True + gtk-close + + + False + False + 2 + + + + + + + 1 + + + + + True + True + left + False + True + + + True + + + True + + + 50 + True + <span size="35000" weight="bold"></span> + True + center + + + 0 + + + + + gtk-go-back + True + True + True + True + False + + + + False + 1 + + + + + False + False + 0 + + + + + True + 4 + 3 + True + + + True + False + False + False + + + + + True + <span size="33000" weight="bold">1</span> +<span size="9000"> </span> + True + + + + + + + True + False + False + False + + + + + + + + True + <span size="30000" weight="bold">2</span> +<span size="12000">ABC</span> + True + center + + + + + 1 + 2 + + + + + True + False + False + False + + + + + + + + True + <span size="30000" weight="bold" stretch="ultraexpanded">3</span> +<span size="12000">DEF</span> + True + center + + + + + 2 + 3 + + + + + True + False + False + False + + + + + + + + True + <span size="30000" weight="bold">4</span> +<span size="12000">GHI</span> + True + center + + + + + 1 + 2 + + + + + True + False + False + False + + + + + + + + True + <span size="30000" weight="bold">5</span> +<span size="12000">JKL</span> + True + center + + + + + 1 + 2 + 1 + 2 + + + + + True + False + False + False + + + + + + + + True + <span size="30000" weight="bold">6</span> +<span size="12000">MNO</span> + True + center + + + + + 2 + 3 + 1 + 2 + + + + + True + False + False + False + + + + + + + + + True + <span size="30000" weight="bold">7</span> +<span size="12000">PQRS</span> + True + center + + + + + 2 + 3 + + + + + True + False + False + False + + + + + + + + True + <span size="30000" weight="bold">8</span> +<span size="12000">TUV</span> + True + center + + + + + 1 + 2 + 2 + 3 + + + + + True + False + False + False + + + + + + + + + True + <span size="30000" weight="bold">9</span> +<span size="12000">WXYZ</span> + True + center + + + + + 2 + 3 + 2 + 3 + + + + + True + False + False + False + + + + + True + <span size="33000" weight="bold">0</span> +<span size="9000"></span> + True + center + + + + + 1 + 2 + 3 + 4 + + + + + True + False + False + + + + True + + + True + 1 + gtk-yes + + + 0 + + + + + True + 0 + 5 + <span size="17000" weight="bold">Dial</span> + True + + + 1 + + + + + + + 2 + 3 + 3 + 4 + + + + + True + True + True + + + + True + + + True + 1 + gtk-file + + + 0 + + + + + True + 0 + 5 + <span size="17000" weight="bold">SMS</span> + True + + + 1 + + + + + + + 3 + 4 + + + + + 1 + + + + + True + False + + + + + 30 + True + Keypad + + + True + False + tab + + + + + True + vertical + + + True + True + never + + + True + True + True + horizontal + True + + + + + 0 + + + + + 1 + True + False + + + + + 30 + True + Recent + + + 1 + True + False + tab + + + + + True + vertical + + + True + True + never + automatic + + + True + True + True + horizontal + True + + + + + 0 + + + + + 2 + + + + + True + Messages + + + 2 + True + False + tab + + + + + True + vertical + + + True + True + True + + + False + 0 + + + + + True + True + never + + + True + True + False + True + True + horizontal + True + + + + + 1 + + + + + 3 + + + + + True + Contacts + + + 3 + True + False + tab + + + + + True + 11 + 7 + 2 + + + True + 0 + No Number Available + True + + + 1 + 2 + GTK_FILL + + + + + + True + 0 + 0 + 10 + Account Number: + + + + + + + + True + 0 + 0 + 10 + Callback Number: + + + 1 + 2 + + + + + + True + 0 + + + 2 + 3 + + + + + True + 0 + + + 4 + 5 + + + + + True + vertical + + + Missed Calls + True + True + False + True + + + 0 + + + + + Voicemail + True + True + False + True + + + 1 + + + + + SMS + True + True + False + True + + + 2 + + + + + 1 + 2 + 4 + 5 + + + + + Notifications + True + True + False + 0 + True + + + 3 + 4 + GTK_FILL + + + + + + Login + True + True + True + False + + + 1 + 2 + 6 + 7 + GTK_FILL + + + + + + True + 0 + + + 5 + 6 + + + + + True + True + True + + + 1 + 2 + 3 + 4 + GTK_FILL + + + + + True + True + True + + + 1 + 2 + 1 + 2 + + + + + + + + + + + + + + + 4 + True + False + + + + + 30 + True + Account + + + 4 + False + tab + + + + + 2 + + + + + + + 5 + Login + False + True + center-on-parent + True + dialog + True + True + False + mainWindow + False + + + True + 2 + + + True + + + False + False + 1 + + + + + True + 2 + 2 + + + True + Username + + + + + True + Password + + + 1 + 2 + + + + + True + True + + + 1 + 2 + + + + + True + True + False + + + 1 + 2 + 1 + 2 + + + + + 2 + + + + + True + end + + + gtk-close + True + True + True + True + + + False + False + 0 + + + + + gtk-ok + True + True + True + True + True + + + False + False + 1 + + + + + False + end + 0 + + + + + + + 5 + Select Phone Type + True + center-on-parent + 500 + 300 + True + dialog + True + True + False + + + True + 2 + + + True + vertical + + + True + True + never + automatic + + + True + True + + + + + 0 + + + + + 1 + + + + + True + + + False + 2 + + + + + True + vertical + + + True + True + never + automatic + + + True + True + True + False + + + + + False + 0 + + + + + False + 3 + + + + + True + end + + + SMS + -4 + True + True + True + + + False + False + 0 + + + + + Dial + True + True + True + + + False + False + 1 + + + + + Select + True + True + True + + + False + False + 2 + + + + + gtk-cancel + True + True + True + True + + + False + False + 3 + + + + + False + end + 0 + + + + + + + 5 + Send SMS + True + center-on-parent + 500 + 300 + True + dialog + True + True + False + + + True + vertical + 2 + + + True + vertical + + + True + True + never + automatic + + + True + True + + + + + 0 + + + + + 1 + + + + + True + + + False + 2 + + + + + True + vertical + + + True + True + never + automatic + + + True + True + word + + + + + 0 + + + + + 3 + + + + + True + + + True + Letters Left: + True + + + False + False + 0 + + + + + True + True + + + 1 + + + + + False + 4 + + + + + True + end + + + Send + True + True + True + + + False + False + 0 + + + + + gtk-cancel + True + True + True + True + + + False + False + 1 + + + + + False + end + 0 + + + + + + diff --git a/src/dialcentral.py b/src/dialcentral.py new file mode 100755 index 0000000..4f669ff --- /dev/null +++ b/src/dialcentral.py @@ -0,0 +1,28 @@ +#!/usr/bin/python + +import os +import sys +import logging + + +sys.path.insert(0,"/usr/lib/dialcentral/") + + +import constants +import dc_glade + + +try: + os.makedirs(constants._data_path_) +except OSError, e: + if e.errno != 17: + raise + +userLogPath = "%s/dialcentral.log" % constants._data_path_ +logging.basicConfig(level=logging.DEBUG, filename=userLogPath) +logging.info("Dialcentral %s-%s" % (constants.__version__, constants.__build__)) + +try: + dc_glade.run_dialpad() +finally: + logging.shutdown() diff --git a/src/example_custom_notifier.py b/src/example_custom_notifier.py new file mode 100755 index 0000000..3679b5b --- /dev/null +++ b/src/example_custom_notifier.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python + +import sys + + +sys.path.insert(0,"/usr/lib/dialcentral/") + + +import alarm_notify + + +def notify_on_change(): + notifyUser = alarm_notify.is_changed() + + if notifyUser: + import subprocess + import led_handler + led = led_handler.LedHandler() + led.on() + soundOn = subprocess.call("/usr/bin/dbus-send --dest=com.nokia.osso_media_server --print-reply /com/nokia/osso_media_server com.nokia.osso_media_server.music.play_media string:file:///usr/lib/gv-notifier/alert.mp3",shell=True) + + +if __name__ == "__main__": + notify_on_change() diff --git a/src/file_backend.py b/src/file_backend.py new file mode 100644 index 0000000..b373561 --- /dev/null +++ b/src/file_backend.py @@ -0,0 +1,171 @@ +#!/usr/bin/python + +""" +DialCentral - Front end for Google's Grand Central service. +Copyright (C) 2008 Eric Warnke ericew AT gmail DOT com + +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 + +Filesystem backend for contact support +""" + + +import os +import re +import csv + + +class CsvAddressBook(object): + """ + Currently supported file format + @li Has the first line as a header + @li Escapes with quotes + @li Comma as delimiter + @li Column 0 is name, column 1 is number + """ + + _nameRe = re.compile("name", re.IGNORECASE) + _phoneRe = re.compile("phone", re.IGNORECASE) + _mobileRe = re.compile("mobile", re.IGNORECASE) + + def __init__(self, csvPath): + self.__csvPath = csvPath + self.__contacts = list( + self.read_csv(csvPath) + ) + + @classmethod + def read_csv(cls, csvPath): + try: + csvReader = iter(csv.reader(open(csvPath, "rU"))) + except IOError, e: + if e.errno != 2: + raise + return + + header = csvReader.next() + nameColumn, phoneColumns = cls._guess_columns(header) + + yieldCount = 0 + for row in csvReader: + contactDetails = [] + for (phoneType, phoneColumn) in phoneColumns: + try: + if len(row[phoneColumn]) == 0: + continue + contactDetails.append((phoneType, row[phoneColumn])) + except IndexError: + pass + if len(contactDetails) != 0: + yield str(yieldCount), row[nameColumn], contactDetails + yieldCount += 1 + + @classmethod + def _guess_columns(cls, row): + names = [] + phones = [] + for i, item in enumerate(row): + if cls._nameRe.search(item) is not None: + names.append((item, i)) + elif cls._phoneRe.search(item) is not None: + phones.append((item, i)) + elif cls._mobileRe.search(item) is not None: + phones.append((item, i)) + if len(names) == 0: + names.append(("Name", 0)) + if len(phones) == 0: + phones.append(("Phone", 1)) + + return names[0][1], phones + + def clear_caches(self): + pass + + @staticmethod + def factory_name(): + return "csv" + + @staticmethod + def contact_source_short_name(contactId): + return "csv" + + def get_contacts(self): + """ + @returns Iterable of (contact id, contact name) + """ + for contact in self.__contacts: + yield contact[0:2] + + def get_contact_details(self, contactId): + """ + @returns Iterable of (Phone Type, Phone Number) + """ + contactId = int(contactId) + return iter(self.__contacts[contactId][2]) + + +class FilesystemAddressBookFactory(object): + + FILETYPE_SUPPORT = { + "csv": CsvAddressBook, + } + + def __init__(self, path): + self.__path = path + + def clear_caches(self): + pass + + def get_addressbooks(self): + """ + @returns Iterable of (Address Book Factory, Book Id, Book Name) + """ + for root, dirs, filenames in os.walk(self.__path): + for filename in filenames: + try: + name, ext = filename.rsplit(".", 1) + except ValueError: + continue + + if ext in self.FILETYPE_SUPPORT: + yield self, os.path.join(root, filename), name + + def open_addressbook(self, bookId): + name, ext = bookId.rsplit(".", 1) + assert ext in self.FILETYPE_SUPPORT, "Unsupported file extension %s" % ext + return self.FILETYPE_SUPPORT[ext](bookId) + + @staticmethod + def factory_name(): + return "File" + + +def print_filebooks(contactPath = None): + """ + Included here for debugging. + + Either insert it into the code or launch python with the "-i" flag + """ + if contactPath is None: + contactPath = os.path.join(os.path.expanduser("~"), ".dialcentral", "contacts") + + abf = FilesystemAddressBookFactory(contactPath) + for book in abf.get_addressbooks(): + ab = abf.open_addressbook(book[1]) + print book + for contact in ab.get_contacts(): + print "\t", contact + for details in ab.get_contact_details(contact[0]): + print "\t\t", details diff --git a/src/gtk_toolbox.py b/src/gtk_toolbox.py new file mode 100644 index 0000000..26803d7 --- /dev/null +++ b/src/gtk_toolbox.py @@ -0,0 +1,741 @@ +#!/usr/bin/python + +from __future__ import with_statement + +import os +import errno +import sys +import time +import itertools +import functools +import contextlib +import logging +import threading +import Queue + +import gobject +import gtk + + +def get_screen_orientation(): + width, height = gtk.gdk.get_default_root_window().get_size() + if width < height: + return gtk.ORIENTATION_VERTICAL + else: + return gtk.ORIENTATION_HORIZONTAL + + +def orientation_change_connect(handler, *args): + """ + @param handler(orientation, *args) -> None(?) + """ + initialScreenOrientation = get_screen_orientation() + orientationAndArgs = list(itertools.chain((initialScreenOrientation, ), args)) + + def _on_screen_size_changed(screen): + newScreenOrientation = get_screen_orientation() + if newScreenOrientation != orientationAndArgs[0]: + orientationAndArgs[0] = newScreenOrientation + handler(*orientationAndArgs) + + rootScreen = gtk.gdk.get_default_root_window() + return gtk.connect(rootScreen, "size-changed", _on_screen_size_changed) + + +@contextlib.contextmanager +def flock(path, timeout=-1): + WAIT_FOREVER = -1 + DELAY = 0.1 + timeSpent = 0 + + acquired = False + + while timeSpent <= timeout or timeout == WAIT_FOREVER: + try: + fd = os.open(path, os.O_CREAT | os.O_EXCL | os.O_RDWR) + acquired = True + break + except OSError, e: + if e.errno != errno.EEXIST: + raise + time.sleep(DELAY) + timeSpent += DELAY + + assert acquired, "Failed to grab file-lock %s within timeout %d" % (path, timeout) + + try: + yield fd + finally: + os.unlink(path) + + +@contextlib.contextmanager +def gtk_lock(): + gtk.gdk.threads_enter() + try: + yield + finally: + gtk.gdk.threads_leave() + + +def find_parent_window(widget): + while True: + parent = widget.get_parent() + if isinstance(parent, gtk.Window): + return parent + widget = parent + + +def make_idler(func): + """ + Decorator that makes a generator-function into a function that will continue execution on next call + """ + a = [] + + @functools.wraps(func) + def decorated_func(*args, **kwds): + if not a: + a.append(func(*args, **kwds)) + try: + a[0].next() + return True + except StopIteration: + del a[:] + return False + + return decorated_func + + +def asynchronous_gtk_message(original_func): + """ + @note Idea came from http://www.aclevername.com/articles/python-webgui/ + """ + + def execute(allArgs): + args, kwargs = allArgs + with gtk_lock(): + original_func(*args, **kwargs) + return False + + @functools.wraps(original_func) + def delayed_func(*args, **kwargs): + gobject.idle_add(execute, (args, kwargs)) + + return delayed_func + + +def synchronous_gtk_message(original_func): + """ + @note Idea came from http://www.aclevername.com/articles/python-webgui/ + """ + + @functools.wraps(original_func) + def immediate_func(*args, **kwargs): + with gtk_lock(): + return original_func(*args, **kwargs) + + return immediate_func + + +def autostart(func): + """ + >>> @autostart + ... def grep_sink(pattern): + ... print "Looking for %s" % pattern + ... while True: + ... line = yield + ... if pattern in line: + ... print line, + >>> g = grep_sink("python") + Looking for python + >>> g.send("Yeah but no but yeah but no") + >>> g.send("A series of tubes") + >>> g.send("python generators rock!") + python generators rock! + >>> g.close() + """ + + @functools.wraps(func) + def start(*args, **kwargs): + cr = func(*args, **kwargs) + cr.next() + return cr + + return start + + +@autostart +def printer_sink(format = "%s"): + """ + >>> pr = printer_sink("%r") + >>> pr.send("Hello") + 'Hello' + >>> pr.send("5") + '5' + >>> pr.send(5) + 5 + >>> p = printer_sink() + >>> p.send("Hello") + Hello + >>> p.send("World") + World + >>> # p.throw(RuntimeError, "Goodbye") + >>> # p.send("Meh") + >>> # p.close() + """ + while True: + item = yield + print format % (item, ) + + +@autostart +def null_sink(): + """ + Good for uses like with cochain to pick up any slack + """ + while True: + item = yield + + +@autostart +def comap(function, target): + """ + >>> p = printer_sink() + >>> cm = comap(lambda x: x+1, p) + >>> cm.send((0, )) + 1 + >>> cm.send((1.0, )) + 2.0 + >>> cm.send((-2, )) + -1 + """ + while True: + try: + item = yield + mappedItem = function(*item) + target.send(mappedItem) + except Exception, e: + logging.exception("Forwarding exception!") + target.throw(e.__class__, str(e)) + + +def _flush_queue(queue): + while not queue.empty(): + yield queue.get() + + +@autostart +def queue_sink(queue): + """ + >>> q = Queue.Queue() + >>> qs = queue_sink(q) + >>> qs.send("Hello") + >>> qs.send("World") + >>> qs.throw(RuntimeError, "Goodbye") + >>> qs.send("Meh") + >>> qs.close() + >>> print [i for i in _flush_queue(q)] + [(None, 'Hello'), (None, 'World'), (, 'Goodbye'), (None, 'Meh'), (, None)] + """ + while True: + try: + item = yield + queue.put((None, item)) + except Exception, e: + queue.put((e.__class__, str(e))) + except GeneratorExit: + queue.put((GeneratorExit, None)) + raise + + +def decode_item(item, target): + if item[0] is None: + target.send(item[1]) + return False + elif item[0] is GeneratorExit: + target.close() + return True + else: + target.throw(item[0], item[1]) + return False + + +def nonqueue_source(queue, target): + isDone = False + while not isDone: + item = queue.get() + isDone = decode_item(item, target) + while not queue.empty(): + queue.get_nowait() + + +def threaded_stage(target, thread_factory = threading.Thread): + messages = Queue.Queue() + + run_source = functools.partial(nonqueue_source, messages, target) + thread = thread_factory(target=run_source) + thread.setDaemon(True) + thread.start() + + # Sink running in current thread + return queue_sink(messages) + + +class LoginWindow(object): + + def __init__(self, widgetTree): + """ + @note Thread agnostic + """ + self._dialog = widgetTree.get_widget("loginDialog") + self._parentWindow = widgetTree.get_widget("mainWindow") + self._serviceCombo = widgetTree.get_widget("serviceCombo") + self._usernameEntry = widgetTree.get_widget("usernameentry") + self._passwordEntry = widgetTree.get_widget("passwordentry") + + self._serviceList = gtk.ListStore(gobject.TYPE_INT, gobject.TYPE_STRING) + self._serviceCombo.set_model(self._serviceList) + cell = gtk.CellRendererText() + self._serviceCombo.pack_start(cell, True) + self._serviceCombo.add_attribute(cell, 'text', 1) + self._serviceCombo.set_active(0) + + widgetTree.get_widget("loginbutton").connect("clicked", self._on_loginbutton_clicked) + widgetTree.get_widget("logins_close_button").connect("clicked", self._on_loginclose_clicked) + + def request_credentials(self, + parentWindow = None, + defaultCredentials = ("", "") + ): + """ + @note UI Thread + """ + if parentWindow is None: + parentWindow = self._parentWindow + + self._serviceCombo.hide() + self._serviceList.clear() + + self._usernameEntry.set_text(defaultCredentials[0]) + self._passwordEntry.set_text(defaultCredentials[1]) + + try: + self._dialog.set_transient_for(parentWindow) + self._dialog.set_default_response(gtk.RESPONSE_OK) + response = self._dialog.run() + if response != gtk.RESPONSE_OK: + raise RuntimeError("Login Cancelled") + + username = self._usernameEntry.get_text() + password = self._passwordEntry.get_text() + self._passwordEntry.set_text("") + finally: + self._dialog.hide() + + return username, password + + def request_credentials_from(self, + services, + parentWindow = None, + defaultCredentials = ("", "") + ): + """ + @note UI Thread + """ + if parentWindow is None: + parentWindow = self._parentWindow + + self._serviceList.clear() + for serviceIdserviceName in services: + self._serviceList.append(serviceIdserviceName) + self._serviceCombo.set_active(0) + self._serviceCombo.show() + + self._usernameEntry.set_text(defaultCredentials[0]) + self._passwordEntry.set_text(defaultCredentials[1]) + + try: + self._dialog.set_transient_for(parentWindow) + self._dialog.set_default_response(gtk.RESPONSE_OK) + response = self._dialog.run() + if response != gtk.RESPONSE_OK: + raise RuntimeError("Login Cancelled") + + username = self._usernameEntry.get_text() + password = self._passwordEntry.get_text() + finally: + self._dialog.hide() + + itr = self._serviceCombo.get_active_iter() + serviceId = int(self._serviceList.get_value(itr, 0)) + self._serviceList.clear() + return serviceId, username, password + + def _on_loginbutton_clicked(self, *args): + self._dialog.response(gtk.RESPONSE_OK) + + def _on_loginclose_clicked(self, *args): + self._dialog.response(gtk.RESPONSE_CANCEL) + + +def safecall(f, errorDisplay=None, default=None, exception=Exception): + ''' + Returns modified f. When the modified f is called and throws an + exception, the default value is returned + ''' + def _safecall(*args, **argv): + try: + return f(*args,**argv) + except exception, e: + if errorDisplay is not None: + errorDisplay.push_exception(e) + return default + return _safecall + + +class ErrorDisplay(object): + + def __init__(self, widgetTree): + super(ErrorDisplay, self).__init__() + self.__errorBox = widgetTree.get_widget("errorEventBox") + self.__errorDescription = widgetTree.get_widget("errorDescription") + self.__errorClose = widgetTree.get_widget("errorClose") + self.__parentBox = self.__errorBox.get_parent() + + self.__errorBox.connect("button_release_event", self._on_close) + + self.__messages = [] + self.__parentBox.remove(self.__errorBox) + + def push_message_with_lock(self, message): + with gtk_lock(): + self.push_message(message) + + def push_message(self, message): + self.__messages.append(message) + if 1 == len(self.__messages): + self.__show_message(message) + + def push_exception_with_lock(self): + with gtk_lock(): + self.push_exception() + + def push_exception(self): + userMessage = str(sys.exc_info()[1]) + self.push_message(userMessage) + logging.exception(userMessage) + + def pop_message(self): + del self.__messages[0] + if 0 == len(self.__messages): + self.__hide_message() + else: + self.__errorDescription.set_text(self.__messages[0]) + + def _on_close(self, *args): + self.pop_message() + + def __show_message(self, message): + self.__errorDescription.set_text(message) + self.__parentBox.pack_start(self.__errorBox, False, False) + self.__parentBox.reorder_child(self.__errorBox, 1) + + def __hide_message(self): + self.__errorDescription.set_text("") + self.__parentBox.remove(self.__errorBox) + + +class DummyErrorDisplay(object): + + def __init__(self): + super(DummyErrorDisplay, self).__init__() + + self.__messages = [] + + def push_message_with_lock(self, message): + self.push_message(message) + + def push_message(self, message): + if 0 < len(self.__messages): + self.__messages.append(message) + else: + self.__show_message(message) + + def push_exception(self, exception = None): + userMessage = str(sys.exc_value) + logging.exception(userMessage) + + def pop_message(self): + if 0 < len(self.__messages): + self.__show_message(self.__messages[0]) + del self.__messages[0] + + def __show_message(self, message): + logging.debug(message) + + +class MessageBox(gtk.MessageDialog): + + def __init__(self, message): + parent = None + gtk.MessageDialog.__init__( + self, + parent, + gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT, + gtk.MESSAGE_ERROR, + gtk.BUTTONS_OK, + message, + ) + self.set_default_response(gtk.RESPONSE_OK) + self.connect('response', self._handle_clicked) + + def _handle_clicked(self, *args): + self.destroy() + + +class MessageBox2(gtk.MessageDialog): + + def __init__(self, message): + parent = None + gtk.MessageDialog.__init__( + self, + parent, + gtk.DIALOG_DESTROY_WITH_PARENT, + gtk.MESSAGE_ERROR, + gtk.BUTTONS_OK, + message, + ) + self.set_default_response(gtk.RESPONSE_OK) + self.connect('response', self._handle_clicked) + + def _handle_clicked(self, *args): + self.destroy() + + +class PopupCalendar(object): + + def __init__(self, parent, displayDate, title = ""): + self._displayDate = displayDate + + self._calendar = gtk.Calendar() + self._calendar.select_month(self._displayDate.month, self._displayDate.year) + self._calendar.select_day(self._displayDate.day) + self._calendar.set_display_options( + gtk.CALENDAR_SHOW_HEADING | + gtk.CALENDAR_SHOW_DAY_NAMES | + gtk.CALENDAR_NO_MONTH_CHANGE | + 0 + ) + self._calendar.connect("day-selected", self._on_day_selected) + + self._popupWindow = gtk.Window() + self._popupWindow.set_title(title) + self._popupWindow.add(self._calendar) + self._popupWindow.set_transient_for(parent) + self._popupWindow.set_modal(True) + self._popupWindow.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG) + self._popupWindow.set_skip_pager_hint(True) + self._popupWindow.set_skip_taskbar_hint(True) + + def run(self): + self._popupWindow.show_all() + + def _on_day_selected(self, *args): + try: + self._calendar.select_month(self._displayDate.month, self._displayDate.year) + self._calendar.select_day(self._displayDate.day) + except Exception, e: + logging.exception(e) + + +class QuickAddView(object): + + def __init__(self, widgetTree, errorDisplay, signalSink, prefix): + self._errorDisplay = errorDisplay + self._manager = None + self._signalSink = signalSink + + self._clipboard = gtk.clipboard_get() + + self._taskNameEntry = widgetTree.get_widget(prefix+"-nameEntry") + self._addTaskButton = widgetTree.get_widget(prefix+"-addButton") + self._pasteTaskNameButton = widgetTree.get_widget(prefix+"-pasteNameButton") + self._clearTaskNameButton = widgetTree.get_widget(prefix+"-clearNameButton") + self._onAddId = None + self._onAddClickedId = None + self._onAddReleasedId = None + self._addToEditTimerId = None + self._onClearId = None + self._onPasteId = None + + def enable(self, manager): + self._manager = manager + + self._onAddId = self._addTaskButton.connect("clicked", self._on_add) + self._onAddClickedId = self._addTaskButton.connect("pressed", self._on_add_pressed) + self._onAddReleasedId = self._addTaskButton.connect("released", self._on_add_released) + self._onPasteId = self._pasteTaskNameButton.connect("clicked", self._on_paste) + self._onClearId = self._clearTaskNameButton.connect("clicked", self._on_clear) + + def disable(self): + self._manager = None + + self._addTaskButton.disconnect(self._onAddId) + self._addTaskButton.disconnect(self._onAddClickedId) + self._addTaskButton.disconnect(self._onAddReleasedId) + self._pasteTaskNameButton.disconnect(self._onPasteId) + self._clearTaskNameButton.disconnect(self._onClearId) + + def set_addability(self, addability): + self._addTaskButton.set_sensitive(addability) + + def _on_add(self, *args): + try: + name = self._taskNameEntry.get_text() + self._taskNameEntry.set_text("") + + self._signalSink.stage.send(("add", name)) + except Exception, e: + self._errorDisplay.push_exception() + + def _on_add_edit(self, *args): + try: + name = self._taskNameEntry.get_text() + self._taskNameEntry.set_text("") + + self._signalSink.stage.send(("add-edit", name)) + except Exception, e: + self._errorDisplay.push_exception() + + def _on_add_pressed(self, widget): + try: + self._addToEditTimerId = gobject.timeout_add(1000, self._on_add_edit) + except Exception, e: + self._errorDisplay.push_exception() + + def _on_add_released(self, widget): + try: + if self._addToEditTimerId is not None: + gobject.source_remove(self._addToEditTimerId) + self._addToEditTimerId = None + except Exception, e: + self._errorDisplay.push_exception() + + def _on_paste(self, *args): + try: + entry = self._taskNameEntry.get_text() + addedText = self._clipboard.wait_for_text() + if addedText: + entry += addedText + self._taskNameEntry.set_text(entry) + except Exception, e: + self._errorDisplay.push_exception() + + def _on_clear(self, *args): + try: + self._taskNameEntry.set_text("") + except Exception, e: + self._errorDisplay.push_exception() + + +class TapOrHold(object): + + def __init__(self, widget): + self._widget = widget + self._isTap = True + self._isPointerInside = True + self._holdTimeoutId = None + self._tapTimeoutId = None + self._taps = 0 + + self._bpeId = None + self._breId = None + self._eneId = None + self._lneId = None + + def enable(self): + self._bpeId = self._widget.connect("button-press-event", self._on_button_press) + self._breId = self._widget.connect("button-release-event", self._on_button_release) + self._eneId = self._widget.connect("enter-notify-event", self._on_enter) + self._lneId = self._widget.connect("leave-notify-event", self._on_leave) + + def disable(self): + self._widget.disconnect(self._bpeId) + self._widget.disconnect(self._breId) + self._widget.disconnect(self._eneId) + self._widget.disconnect(self._lneId) + + def on_tap(self, taps): + print "TAP", taps + + def on_hold(self, taps): + print "HOLD", taps + + def on_holding(self): + print "HOLDING" + + def on_cancel(self): + print "CANCEL" + + def _on_button_press(self, *args): + # Hack to handle weird notebook behavior + self._isPointerInside = True + self._isTap = True + + if self._tapTimeoutId is not None: + gobject.source_remove(self._tapTimeoutId) + self._tapTimeoutId = None + + # Handle double taps + if self._holdTimeoutId is None: + self._tapTimeoutId = None + + self._taps = 1 + self._holdTimeoutId = gobject.timeout_add(1000, self._on_hold_timeout) + else: + self._taps = 2 + + def _on_button_release(self, *args): + assert self._tapTimeoutId is None + # Handle release after timeout if user hasn't double-clicked + self._tapTimeoutId = gobject.timeout_add(100, self._on_tap_timeout) + + def _on_actual_press(self, *args): + if self._holdTimeoutId is not None: + gobject.source_remove(self._holdTimeoutId) + self._holdTimeoutId = None + + if self._isPointerInside: + if self._isTap: + self.on_tap(self._taps) + else: + self.on_hold(self._taps) + else: + self.on_cancel() + + def _on_tap_timeout(self, *args): + self._tapTimeoutId = None + self._on_actual_press() + return False + + def _on_hold_timeout(self, *args): + self._holdTimeoutId = None + self._isTap = False + self.on_holding() + return False + + def _on_enter(self, *args): + self._isPointerInside = True + + def _on_leave(self, *args): + self._isPointerInside = False + + +if __name__ == "__main__": + if False: + import datetime + cal = PopupCalendar(None, datetime.datetime.now()) + cal._popupWindow.connect("destroy", lambda w: gtk.main_quit()) + cal.run() + + gtk.main() diff --git a/src/gv_backend.py b/src/gv_backend.py new file mode 100644 index 0000000..0e5da67 --- /dev/null +++ b/src/gv_backend.py @@ -0,0 +1,645 @@ +#!/usr/bin/python + +""" +DialCentral - Front end for Google's GoogleVoice service. +Copyright (C) 2008 Eric Warnke ericew AT gmail DOT com + +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 + +Google Voice backend code + +Resources + http://thatsmith.com/2009/03/google-voice-addon-for-firefox/ + http://posttopic.com/topic/google-voice-add-on-development +""" + + +import os +import re +import urllib +import urllib2 +import time +import datetime +import itertools +import logging +from xml.sax import saxutils + +from xml.etree import ElementTree + +import browser_emu + +try: + import simplejson +except ImportError: + simplejson = None + + +_TRUE_REGEX = re.compile("true") +_FALSE_REGEX = re.compile("false") + + +def safe_eval(s): + s = _TRUE_REGEX.sub("True", s) + s = _FALSE_REGEX.sub("False", s) + return eval(s, {}, {}) + + +if simplejson is None: + def parse_json(flattened): + return safe_eval(flattened) +else: + def parse_json(flattened): + return simplejson.loads(flattened) + + +def itergroup(iterator, count, padValue = None): + """ + Iterate in groups of 'count' values. If there + aren't enough values, the last result is padded with + None. + + >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3): + ... print tuple(val) + (1, 2, 3) + (4, 5, 6) + >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3): + ... print list(val) + [1, 2, 3] + [4, 5, 6] + >>> for val in itergroup([1, 2, 3, 4, 5, 6, 7], 3): + ... print tuple(val) + (1, 2, 3) + (4, 5, 6) + (7, None, None) + >>> for val in itergroup("123456", 3): + ... print tuple(val) + ('1', '2', '3') + ('4', '5', '6') + >>> for val in itergroup("123456", 3): + ... print repr("".join(val)) + '123' + '456' + """ + paddedIterator = itertools.chain(iterator, itertools.repeat(padValue, count-1)) + nIterators = (paddedIterator, ) * count + return itertools.izip(*nIterators) + + +class GVDialer(object): + """ + This class encapsulates all of the knowledge necessary to interact with the GoogleVoice servers + the functions include login, setting up a callback number, and initalting a callback + """ + + def __init__(self, cookieFile = None): + # Important items in this function are the setup of the browser emulation and cookie file + self._browser = browser_emu.MozillaEmulator(1) + if cookieFile is None: + cookieFile = os.path.join(os.path.expanduser("~"), ".gv_cookies.txt") + self._browser.cookies.filename = cookieFile + if os.path.isfile(cookieFile): + self._browser.cookies.load() + + self._token = "" + self._accountNum = "" + self._lastAuthed = 0.0 + self._callbackNumber = "" + self._callbackNumbers = {} + + self.__contacts = None + + def is_authed(self, force = False): + """ + Attempts to detect a current session + @note Once logged in try not to reauth more than once a minute. + @returns If authenticated + """ + + if (time.time() - self._lastAuthed) < 120 and not force: + return True + + try: + self._grab_account_info() + except Exception, e: + logging.exception(str(e)) + return False + + self._browser.cookies.save() + self._lastAuthed = time.time() + return True + + _loginURL = "https://www.google.com/accounts/ServiceLoginAuth" + + def login(self, username, password): + """ + Attempt to login to GoogleVoice + @returns Whether login was successful or not + """ + if self.is_authed(): + return True + + loginPostData = urllib.urlencode({ + 'Email' : username, + 'Passwd' : password, + 'service': "grandcentral", + "ltmpl": "mobile", + "btmpl": "mobile", + "PersistentCookie": "yes", + }) + + try: + loginSuccessOrFailurePage = self._browser.download(self._loginURL, loginPostData) + except urllib2.URLError, e: + logging.exception(str(e)) + raise RuntimeError("%s is not accesible" % self._loginURL) + + return self.is_authed() + + def logout(self): + self._lastAuthed = 0.0 + self._browser.cookies.clear() + self._browser.cookies.save() + + self.clear_caches() + + _gvDialingStrRe = re.compile("This may take a few seconds", re.M) + _clicktocallURL = "https://www.google.com/voice/m/sendcall" + + def dial(self, number): + """ + This is the main function responsible for initating the callback + """ + number = self._send_validation(number) + try: + clickToCallData = urllib.urlencode({ + "number": number, + "phone": self._callbackNumber, + "_rnr_se": self._token, + }) + otherData = { + 'Referer' : 'https://google.com/voice/m/callsms', + } + callSuccessPage = self._browser.download(self._clicktocallURL, clickToCallData, None, otherData) + except urllib2.URLError, e: + logging.exception(str(e)) + raise RuntimeError("%s is not accesible" % self._clicktocallURL) + + if self._gvDialingStrRe.search(callSuccessPage) is None: + raise RuntimeError("Google Voice returned an error") + + return True + + _sendSmsURL = "https://www.google.com/voice/m/sendsms" + + def send_sms(self, number, message): + number = self._send_validation(number) + try: + smsData = urllib.urlencode({ + "number": number, + "smstext": message, + "_rnr_se": self._token, + "id": "undefined", + "c": "undefined", + }) + otherData = { + 'Referer' : 'https://google.com/voice/m/sms', + } + smsSuccessPage = self._browser.download(self._sendSmsURL, smsData, None, otherData) + except urllib2.URLError, e: + logging.exception(str(e)) + raise RuntimeError("%s is not accesible" % self._sendSmsURL) + + return True + + def clear_caches(self): + self.__contacts = None + + _validateRe = re.compile("^[0-9]{10,}$") + + def is_valid_syntax(self, number): + """ + @returns If This number be called ( syntax validation only ) + """ + return self._validateRe.match(number) is not None + + def get_account_number(self): + """ + @returns The GoogleVoice phone number + """ + return self._accountNum + + def set_sane_callback(self): + """ + Try to set a sane default callback number on these preferences + 1) 1747 numbers ( Gizmo ) + 2) anything with gizmo in the name + 3) anything with computer in the name + 4) the first value + """ + numbers = self.get_callback_numbers() + + for number, description in numbers.iteritems(): + if re.compile(r"""1747""").match(number) is not None: + self.set_callback_number(number) + return + + for number, description in numbers.iteritems(): + if re.compile(r"""gizmo""", re.I).search(description) is not None: + self.set_callback_number(number) + return + + for number, description in numbers.iteritems(): + if re.compile(r"""computer""", re.I).search(description) is not None: + self.set_callback_number(number) + return + + for number, description in numbers.iteritems(): + self.set_callback_number(number) + return + + def get_callback_numbers(self): + """ + @returns a dictionary mapping call back numbers to descriptions + @note These results are cached for 30 minutes. + """ + if not self.is_authed(): + return {} + return self._callbackNumbers + + _setforwardURL = "https://www.google.com//voice/m/setphone" + + def set_callback_number(self, callbacknumber): + """ + Set the number that GoogleVoice calls + @param callbacknumber should be a proper 10 digit number + """ + self._callbackNumber = callbacknumber + return True + + def get_callback_number(self): + """ + @returns Current callback number or None + """ + return self._callbackNumber + + def get_recent(self): + """ + @returns Iterable of (personsName, phoneNumber, date, action) + """ + sortedRecent = [ + (exactDate, name, number, relativeDate, action) + for (name, number, exactDate, relativeDate, action) in self._get_recent() + ] + sortedRecent.sort(reverse = True) + for exactDate, name, number, relativeDate, action in sortedRecent: + yield name, number, relativeDate, action + + def get_addressbooks(self): + """ + @returns Iterable of (Address Book Factory, Book Id, Book Name) + """ + yield self, "", "" + + def open_addressbook(self, bookId): + return self + + @staticmethod + def contact_source_short_name(contactId): + return "GV" + + @staticmethod + def factory_name(): + return "Google Voice" + + _contactsRe = re.compile(r"""(.*?)""", re.S) + _contactsNextRe = re.compile(r""".*Next.*?""", re.S) + _contactsURL = "https://www.google.com/voice/mobile/contacts" + + def get_contacts(self): + """ + @returns Iterable of (contact id, contact name) + """ + if self.__contacts is None: + self.__contacts = [] + + contactsPagesUrls = [self._contactsURL] + for contactsPageUrl in contactsPagesUrls: + try: + contactsPage = self._browser.download(contactsPageUrl) + except urllib2.URLError, e: + logging.exception(str(e)) + raise RuntimeError("%s is not accesible" % contactsPageUrl) + for contact_match in self._contactsRe.finditer(contactsPage): + contactId = contact_match.group(1) + contactName = saxutils.unescape(contact_match.group(2)) + contact = contactId, contactName + self.__contacts.append(contact) + yield contact + + next_match = self._contactsNextRe.match(contactsPage) + if next_match is not None: + newContactsPageUrl = self._contactsURL + next_match.group(1) + contactsPagesUrls.append(newContactsPageUrl) + else: + for contact in self.__contacts: + yield contact + + _contactDetailPhoneRe = re.compile(r"""([0-9+\-\(\) \t]+?)\((\w+)\)""", re.S) + _contactDetailURL = "https://www.google.com/voice/mobile/contact" + + def get_contact_details(self, contactId): + """ + @returns Iterable of (Phone Type, Phone Number) + """ + try: + detailPage = self._browser.download(self._contactDetailURL + '/' + contactId) + except urllib2.URLError, e: + logging.exception(str(e)) + raise RuntimeError("%s is not accesible" % self._contactDetailURL) + + for detail_match in self._contactDetailPhoneRe.finditer(detailPage): + phoneNumber = detail_match.group(1) + phoneType = saxutils.unescape(detail_match.group(2)) + yield (phoneType, phoneNumber) + + _voicemailURL = "https://www.google.com/voice/inbox/recent/voicemail/" + _smsURL = "https://www.google.com/voice/inbox/recent/sms/" + + def get_messages(self): + try: + voicemailPage = self._browser.download(self._voicemailURL) + except urllib2.URLError, e: + logging.exception(str(e)) + raise RuntimeError("%s is not accesible" % self._voicemailURL) + voicemailHtml = self._grab_html(voicemailPage) + parsedVoicemail = self._parse_voicemail(voicemailHtml) + decoratedVoicemails = self._decorate_voicemail(parsedVoicemail) + + try: + smsPage = self._browser.download(self._smsURL) + except urllib2.URLError, e: + logging.exception(str(e)) + raise RuntimeError("%s is not accesible" % self._smsURL) + smsHtml = self._grab_html(smsPage) + parsedSms = self._parse_sms(smsHtml) + decoratedSms = self._decorate_sms(parsedSms) + + allMessages = itertools.chain(decoratedVoicemails, decoratedSms) + sortedMessages = list(allMessages) + sortedMessages.sort(reverse=True) + for exactDate, header, number, relativeDate, message in sortedMessages: + yield header, number, relativeDate, message + + def _grab_json(self, flatXml): + xmlTree = ElementTree.fromstring(flatXml) + jsonElement = xmlTree.getchildren()[0] + flatJson = jsonElement.text + jsonTree = parse_json(flatJson) + return jsonTree + + def _grab_html(self, flatXml): + xmlTree = ElementTree.fromstring(flatXml) + htmlElement = xmlTree.getchildren()[1] + flatHtml = htmlElement.text + return flatHtml + + _tokenRe = re.compile(r"""""") + _accountNumRe = re.compile(r"""(.{14})""") + _callbackRe = re.compile(r"""\s+(.*?):\s*(.*?)\s*$""", re.M) + _forwardURL = "https://www.google.com/voice/mobile/phones" + + def _grab_account_info(self): + page = self._browser.download(self._forwardURL) + + tokenGroup = self._tokenRe.search(page) + if tokenGroup is None: + raise RuntimeError("Could not extract authentication token from GoogleVoice") + self._token = tokenGroup.group(1) + + anGroup = self._accountNumRe.search(page) + if anGroup is not None: + self._accountNum = anGroup.group(1) + else: + logging.debug("Could not extract account number from GoogleVoice") + + self._callbackNumbers = {} + for match in self._callbackRe.finditer(page): + callbackNumber = match.group(2) + callbackName = match.group(1) + self._callbackNumbers[callbackNumber] = callbackName + + def _send_validation(self, number): + if not self.is_valid_syntax(number): + raise ValueError('Number is not valid: "%s"' % number) + elif not self.is_authed(): + raise RuntimeError("Not Authenticated") + + if len(number) == 11 and number[0] == 1: + # Strip leading 1 from 11 digit dialing + number = number[1:] + return number + + _recentCallsURL = "https://www.google.com/voice/inbox/recent/" + _placedCallsURL = "https://www.google.com/voice/inbox/recent/placed/" + _receivedCallsURL = "https://www.google.com/voice/inbox/recent/received/" + _missedCallsURL = "https://www.google.com/voice/inbox/recent/missed/" + + def _get_recent(self): + """ + @returns Iterable of (personsName, phoneNumber, exact date, relative date, action) + """ + for action, url in ( + ("Received", self._receivedCallsURL), + ("Missed", self._missedCallsURL), + ("Placed", self._placedCallsURL), + ): + try: + flatXml = self._browser.download(url) + except urllib2.URLError, e: + logging.exception(str(e)) + raise RuntimeError("%s is not accesible" % url) + + allRecentHtml = self._grab_html(flatXml) + allRecentData = self._parse_voicemail(allRecentHtml) + for recentCallData in allRecentData: + exactTime = recentCallData["time"] + if recentCallData["name"]: + header = recentCallData["name"] + elif recentCallData["prettyNumber"]: + header = recentCallData["prettyNumber"] + elif recentCallData["location"]: + header = recentCallData["location"] + else: + header = "Unknown" + yield header, recentCallData["number"], exactTime, recentCallData["relTime"], action + + _seperateVoicemailsRegex = re.compile(r"""^\s*
""", re.MULTILINE | re.DOTALL) + _exactVoicemailTimeRegex = re.compile(r"""(.*?)""", re.MULTILINE) + _relativeVoicemailTimeRegex = re.compile(r"""(.*?)""", re.MULTILINE) + _voicemailNameRegex = re.compile(r"""(.*?)""", re.MULTILINE | re.DOTALL) + _voicemailNumberRegex = re.compile(r"""""", re.MULTILINE) + _prettyVoicemailNumberRegex = re.compile(r"""(.*?)""", re.MULTILINE) + _voicemailLocationRegex = re.compile(r""".*?(.*?)""", re.MULTILINE) + #_voicemailMessageRegex = re.compile(r"""(.*?)""", re.MULTILINE) + #_voicemailMessageRegex = re.compile(r"""(.*?)""", re.MULTILINE) + _voicemailMessageRegex = re.compile(r"""((.*?)|(.*?))""", re.MULTILINE) + + @staticmethod + def _interpret_voicemail_regex(group): + quality, content, number = group.group(2), group.group(3), group.group(4) + if quality is not None and content is not None: + return quality, content + elif number is not None: + return "high", number + + def _parse_voicemail(self, voicemailHtml): + splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml) + for messageId, messageHtml in itergroup(splitVoicemail[1:], 2): + exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml) + exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else "" + exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p") + relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml) + relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else "" + locationGroup = self._voicemailLocationRegex.search(messageHtml) + location = locationGroup.group(1).strip() if locationGroup else "" + + nameGroup = self._voicemailNameRegex.search(messageHtml) + name = nameGroup.group(1).strip() if nameGroup else "" + numberGroup = self._voicemailNumberRegex.search(messageHtml) + number = numberGroup.group(1).strip() if numberGroup else "" + prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml) + prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else "" + + messageGroups = self._voicemailMessageRegex.finditer(messageHtml) + messageParts = ( + self._interpret_voicemail_regex(group) + for group in messageGroups + ) if messageGroups else () + + yield { + "id": messageId.strip(), + "name": name, + "time": exactTime, + "relTime": relativeTime, + "prettyNumber": prettyNumber, + "number": number, + "location": location, + "messageParts": messageParts, + } + + def _decorate_voicemail(self, parsedVoicemail): + messagePartFormat = { + "med1": "%s", + "med2": "%s", + "high": "%s", + } + for voicemailData in parsedVoicemail: + exactTime = voicemailData["time"] + if voicemailData["name"]: + header = voicemailData["name"] + elif voicemailData["prettyNumber"]: + header = voicemailData["prettyNumber"] + elif voicemailData["location"]: + header = voicemailData["location"] + else: + header = "Unknown" + message = " ".join(( + messagePartFormat[quality] % part + for (quality, part) in voicemailData["messageParts"] + )).strip() + if not message: + message = "No Transcription" + yield exactTime, header, voicemailData["number"], voicemailData["relTime"], (message, ) + + _smsFromRegex = re.compile(r"""(.*?)""", re.MULTILINE | re.DOTALL) + _smsTextRegex = re.compile(r"""(.*?)""", re.MULTILINE | re.DOTALL) + _smsTimeRegex = re.compile(r"""(.*?)""", re.MULTILINE | re.DOTALL) + + def _parse_sms(self, smsHtml): + splitSms = self._seperateVoicemailsRegex.split(smsHtml) + for messageId, messageHtml in itergroup(splitSms[1:], 2): + exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml) + exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else "" + exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p") + relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml) + relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else "" + + nameGroup = self._voicemailNameRegex.search(messageHtml) + name = nameGroup.group(1).strip() if nameGroup else "" + numberGroup = self._voicemailNumberRegex.search(messageHtml) + number = numberGroup.group(1).strip() if numberGroup else "" + prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml) + prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else "" + + fromGroups = self._smsFromRegex.finditer(messageHtml) + fromParts = (group.group(1).strip() for group in fromGroups) + textGroups = self._smsTextRegex.finditer(messageHtml) + textParts = (group.group(1).strip() for group in textGroups) + timeGroups = self._smsTimeRegex.finditer(messageHtml) + timeParts = (group.group(1).strip() for group in timeGroups) + + messageParts = itertools.izip(fromParts, textParts, timeParts) + + yield { + "id": messageId.strip(), + "name": name, + "time": exactTime, + "relTime": relativeTime, + "prettyNumber": prettyNumber, + "number": number, + "messageParts": messageParts, + } + + def _decorate_sms(self, parsedSms): + for messageData in parsedSms: + exactTime = messageData["time"] + if messageData["name"]: + header = messageData["name"] + elif messageData["prettyNumber"]: + header = messageData["prettyNumber"] + else: + header = "Unknown" + number = messageData["number"] + relativeTime = messageData["relTime"] + messages = [ + "%s: %s" % (messagePart[0], messagePart[-1]) + for messagePart in messageData["messageParts"] + ] + if not messages: + messages = ("No Transcription", ) + yield exactTime, header, number, relativeTime, messages + + +def test_backend(username, password): + backend = GVDialer() + print "Authenticated: ", backend.is_authed() + print "Login?: ", backend.login(username, password) + print "Authenticated: ", backend.is_authed() + # print "Token: ", backend._token + print "Account: ", backend.get_account_number() + print "Callback: ", backend.get_callback_number() + # print "All Callback: ", + import pprint + # pprint.pprint(backend.get_callback_numbers()) + # print "Recent: ", + # pprint.pprint(list(backend.get_recent())) + # print "Contacts: ", + # for contact in backend.get_contacts(): + # print contact + # pprint.pprint(list(backend.get_contact_details(contact[0]))) + for message in backend.get_messages(): + pprint.pprint(message) + + return backend diff --git a/src/gv_views.py b/src/gv_views.py new file mode 100644 index 0000000..0eac88b --- /dev/null +++ b/src/gv_views.py @@ -0,0 +1,1483 @@ +#!/usr/bin/python2.5 + +""" +DialCentral - Front end for Google's GoogleVoice service. +Copyright (C) 2008 Mark Bergman bergman AT merctech DOT com + +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 + +@todo Alternate UI for dialogs (stackables) +""" + +from __future__ import with_statement + +import ConfigParser +import logging + +import gobject +import pango +import gtk + +import gtk_toolbox +import hildonize +import null_backend + + +def make_ugly(prettynumber): + """ + function to take a phone number and strip out all non-numeric + characters + + >>> make_ugly("+012-(345)-678-90") + '01234567890' + """ + import re + uglynumber = re.sub('\D', '', prettynumber) + return uglynumber + + +def make_pretty(phonenumber): + """ + Function to take a phone number and return the pretty version + pretty numbers: + if phonenumber begins with 0: + ...-(...)-...-.... + if phonenumber begins with 1: ( for gizmo callback numbers ) + 1 (...)-...-.... + if phonenumber is 13 digits: + (...)-...-.... + if phonenumber is 10 digits: + ...-.... + >>> make_pretty("12") + '12' + >>> make_pretty("1234567") + '123-4567' + >>> make_pretty("2345678901") + '(234)-567-8901' + >>> make_pretty("12345678901") + '1 (234)-567-8901' + >>> make_pretty("01234567890") + '+012-(345)-678-90' + """ + if phonenumber is None or phonenumber is "": + return "" + + phonenumber = make_ugly(phonenumber) + + if len(phonenumber) < 3: + return phonenumber + + if phonenumber[0] == "0": + prettynumber = "" + prettynumber += "+%s" % phonenumber[0:3] + if 3 < len(phonenumber): + prettynumber += "-(%s)" % phonenumber[3:6] + if 6 < len(phonenumber): + prettynumber += "-%s" % phonenumber[6:9] + if 9 < len(phonenumber): + prettynumber += "-%s" % phonenumber[9:] + return prettynumber + elif len(phonenumber) <= 7: + prettynumber = "%s-%s" % (phonenumber[0:3], phonenumber[3:]) + elif len(phonenumber) > 8 and phonenumber[0] == "1": + prettynumber = "1 (%s)-%s-%s" % (phonenumber[1:4], phonenumber[4:7], phonenumber[7:]) + elif len(phonenumber) > 7: + prettynumber = "(%s)-%s-%s" % (phonenumber[0:3], phonenumber[3:6], phonenumber[6:]) + return prettynumber + + +def abbrev_relative_date(date): + """ + >>> abbrev_relative_date("42 hours ago") + '42 h' + >>> abbrev_relative_date("2 days ago") + '2 d' + >>> abbrev_relative_date("4 weeks ago") + '4 w' + """ + parts = date.split(" ") + return "%s %s" % (parts[0], parts[1][0]) + + +class MergedAddressBook(object): + """ + Merger of all addressbooks + """ + + def __init__(self, addressbookFactories, sorter = None): + self.__addressbookFactories = addressbookFactories + self.__addressbooks = None + self.__sort_contacts = sorter if sorter is not None else self.null_sorter + + def clear_caches(self): + self.__addressbooks = None + for factory in self.__addressbookFactories: + factory.clear_caches() + + def get_addressbooks(self): + """ + @returns Iterable of (Address Book Factory, Book Id, Book Name) + """ + yield self, "", "" + + def open_addressbook(self, bookId): + return self + + def contact_source_short_name(self, contactId): + if self.__addressbooks is None: + return "" + bookIndex, originalId = contactId.split("-", 1) + return self.__addressbooks[int(bookIndex)].contact_source_short_name(originalId) + + @staticmethod + def factory_name(): + return "All Contacts" + + def get_contacts(self): + """ + @returns Iterable of (contact id, contact name) + """ + if self.__addressbooks is None: + self.__addressbooks = list( + factory.open_addressbook(id) + for factory in self.__addressbookFactories + for (f, id, name) in factory.get_addressbooks() + ) + contacts = ( + ("-".join([str(bookIndex), contactId]), contactName) + for (bookIndex, addressbook) in enumerate(self.__addressbooks) + for (contactId, contactName) in addressbook.get_contacts() + ) + sortedContacts = self.__sort_contacts(contacts) + return sortedContacts + + def get_contact_details(self, contactId): + """ + @returns Iterable of (Phone Type, Phone Number) + """ + if self.__addressbooks is None: + return [] + bookIndex, originalId = contactId.split("-", 1) + return self.__addressbooks[int(bookIndex)].get_contact_details(originalId) + + @staticmethod + def null_sorter(contacts): + """ + Good for speed/low memory + """ + return contacts + + @staticmethod + def basic_firtname_sorter(contacts): + """ + Expects names in "First Last" format + """ + contactsWithKey = [ + (contactName.rsplit(" ", 1)[0], (contactId, contactName)) + for (contactId, contactName) in contacts + ] + contactsWithKey.sort() + return (contactData for (lastName, contactData) in contactsWithKey) + + @staticmethod + def basic_lastname_sorter(contacts): + """ + Expects names in "First Last" format + """ + contactsWithKey = [ + (contactName.rsplit(" ", 1)[-1], (contactId, contactName)) + for (contactId, contactName) in contacts + ] + contactsWithKey.sort() + return (contactData for (lastName, contactData) in contactsWithKey) + + @staticmethod + def reversed_firtname_sorter(contacts): + """ + Expects names in "Last, First" format + """ + contactsWithKey = [ + (contactName.split(", ", 1)[-1], (contactId, contactName)) + for (contactId, contactName) in contacts + ] + contactsWithKey.sort() + return (contactData for (lastName, contactData) in contactsWithKey) + + @staticmethod + def reversed_lastname_sorter(contacts): + """ + Expects names in "Last, First" format + """ + contactsWithKey = [ + (contactName.split(", ", 1)[0], (contactId, contactName)) + for (contactId, contactName) in contacts + ] + contactsWithKey.sort() + return (contactData for (lastName, contactData) in contactsWithKey) + + @staticmethod + def guess_firstname(name): + if ", " in name: + return name.split(", ", 1)[-1] + else: + return name.rsplit(" ", 1)[0] + + @staticmethod + def guess_lastname(name): + if ", " in name: + return name.split(", ", 1)[0] + else: + return name.rsplit(" ", 1)[-1] + + @classmethod + def advanced_firstname_sorter(cls, contacts): + contactsWithKey = [ + (cls.guess_firstname(contactName), (contactId, contactName)) + for (contactId, contactName) in contacts + ] + contactsWithKey.sort() + return (contactData for (lastName, contactData) in contactsWithKey) + + @classmethod + def advanced_lastname_sorter(cls, contacts): + contactsWithKey = [ + (cls.guess_lastname(contactName), (contactId, contactName)) + for (contactId, contactName) in contacts + ] + contactsWithKey.sort() + return (contactData for (lastName, contactData) in contactsWithKey) + + +class PhoneTypeSelector(object): + + ACTION_CANCEL = "cancel" + ACTION_SELECT = "select" + ACTION_DIAL = "dial" + ACTION_SEND_SMS = "sms" + + def __init__(self, widgetTree, gcBackend): + self._gcBackend = gcBackend + self._widgetTree = widgetTree + + self._dialog = self._widgetTree.get_widget("phonetype_dialog") + self._smsDialog = SmsEntryDialog(self._widgetTree) + + self._smsButton = self._widgetTree.get_widget("sms_button") + self._smsButton.connect("clicked", self._on_phonetype_send_sms) + + self._dialButton = self._widgetTree.get_widget("dial_button") + self._dialButton.connect("clicked", self._on_phonetype_dial) + + self._selectButton = self._widgetTree.get_widget("select_button") + self._selectButton.connect("clicked", self._on_phonetype_select) + + self._cancelButton = self._widgetTree.get_widget("cancel_button") + self._cancelButton.connect("clicked", self._on_phonetype_cancel) + + self._messagemodel = gtk.ListStore(gobject.TYPE_STRING) + self._messagesView = self._widgetTree.get_widget("phoneSelectionMessages") + self._scrollWindow = self._messagesView.get_parent() + + self._typemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING) + self._typeviewselection = None + self._typeview = self._widgetTree.get_widget("phonetypes") + self._typeview.connect("row-activated", self._on_phonetype_select) + + self._action = self.ACTION_CANCEL + + def run(self, contactDetails, messages = (), parent = None): + self._action = self.ACTION_CANCEL + + # Add the column to the phone selection tree view + self._typemodel.clear() + self._typeview.set_model(self._typemodel) + + textrenderer = gtk.CellRendererText() + numberColumn = gtk.TreeViewColumn("Phone Numbers", textrenderer, text=0) + self._typeview.append_column(numberColumn) + + textrenderer = gtk.CellRendererText() + typeColumn = gtk.TreeViewColumn("Phone Type", textrenderer, text=1) + self._typeview.append_column(typeColumn) + + for phoneType, phoneNumber in contactDetails: + display = " - ".join((phoneNumber, phoneType)) + display = phoneType + row = (phoneNumber, display) + self._typemodel.append(row) + + self._typeviewselection = self._typeview.get_selection() + self._typeviewselection.set_mode(gtk.SELECTION_SINGLE) + self._typeviewselection.select_iter(self._typemodel.get_iter_first()) + + # Add the column to the messages tree view + self._messagemodel.clear() + self._messagesView.set_model(self._messagemodel) + + textrenderer = gtk.CellRendererText() + textrenderer.set_property("wrap-mode", pango.WRAP_WORD) + textrenderer.set_property("wrap-width", 450) + messageColumn = gtk.TreeViewColumn("") + messageColumn.pack_start(textrenderer, expand=True) + messageColumn.add_attribute(textrenderer, "markup", 0) + messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) + self._messagesView.append_column(messageColumn) + self._messagesView.set_headers_visible(False) + + if messages: + for message in messages: + row = (message, ) + self._messagemodel.append(row) + self._messagesView.show() + self._scrollWindow.show() + messagesSelection = self._messagesView.get_selection() + messagesSelection.select_path((len(messages)-1, )) + else: + self._messagesView.hide() + self._scrollWindow.hide() + + if parent is not None: + self._dialog.set_transient_for(parent) + + try: + self._dialog.show() + if messages: + self._messagesView.scroll_to_cell((len(messages)-1, )) + + userResponse = self._dialog.run() + finally: + self._dialog.hide() + + if userResponse == gtk.RESPONSE_OK: + phoneNumber = self._get_number() + phoneNumber = make_ugly(phoneNumber) + else: + phoneNumber = "" + if not phoneNumber: + self._action = self.ACTION_CANCEL + + if self._action == self.ACTION_SEND_SMS: + smsMessage = self._smsDialog.run(phoneNumber, messages, parent) + if not smsMessage: + phoneNumber = "" + self._action = self.ACTION_CANCEL + else: + smsMessage = "" + + self._messagesView.remove_column(messageColumn) + self._messagesView.set_model(None) + + self._typeviewselection.unselect_all() + self._typeview.remove_column(numberColumn) + self._typeview.remove_column(typeColumn) + self._typeview.set_model(None) + + return self._action, phoneNumber, smsMessage + + def _get_number(self): + model, itr = self._typeviewselection.get_selected() + if not itr: + return "" + + phoneNumber = self._typemodel.get_value(itr, 0) + return phoneNumber + + def _on_phonetype_dial(self, *args): + self._dialog.response(gtk.RESPONSE_OK) + self._action = self.ACTION_DIAL + + def _on_phonetype_send_sms(self, *args): + self._dialog.response(gtk.RESPONSE_OK) + self._action = self.ACTION_SEND_SMS + + def _on_phonetype_select(self, *args): + self._dialog.response(gtk.RESPONSE_OK) + self._action = self.ACTION_SELECT + + def _on_phonetype_cancel(self, *args): + self._dialog.response(gtk.RESPONSE_CANCEL) + self._action = self.ACTION_CANCEL + + +class SmsEntryDialog(object): + """ + @todo Add multi-SMS messages like GoogleVoice + """ + + MAX_CHAR = 160 + + def __init__(self, widgetTree): + self._widgetTree = widgetTree + self._dialog = self._widgetTree.get_widget("smsDialog") + + self._smsButton = self._widgetTree.get_widget("sendSmsButton") + self._smsButton.connect("clicked", self._on_send) + + self._cancelButton = self._widgetTree.get_widget("cancelSmsButton") + self._cancelButton.connect("clicked", self._on_cancel) + + self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount") + + self._messagemodel = gtk.ListStore(gobject.TYPE_STRING) + self._messagesView = self._widgetTree.get_widget("smsMessages") + self._scrollWindow = self._messagesView.get_parent() + + self._smsEntry = self._widgetTree.get_widget("smsEntry") + self._smsEntry.get_buffer().connect("changed", self._on_entry_changed) + + def run(self, number, messages = (), parent = None): + # Add the column to the messages tree view + self._messagemodel.clear() + self._messagesView.set_model(self._messagemodel) + + textrenderer = gtk.CellRendererText() + textrenderer.set_property("wrap-mode", pango.WRAP_WORD) + textrenderer.set_property("wrap-width", 450) + messageColumn = gtk.TreeViewColumn("") + messageColumn.pack_start(textrenderer, expand=True) + messageColumn.add_attribute(textrenderer, "markup", 0) + messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) + self._messagesView.append_column(messageColumn) + self._messagesView.set_headers_visible(False) + + if messages: + for message in messages: + row = (message, ) + self._messagemodel.append(row) + self._messagesView.show() + self._scrollWindow.show() + messagesSelection = self._messagesView.get_selection() + messagesSelection.select_path((len(messages)-1, )) + else: + self._messagesView.hide() + self._scrollWindow.hide() + + self._smsEntry.get_buffer().set_text("") + self._update_letter_count() + + if parent is not None: + self._dialog.set_transient_for(parent) + + try: + self._dialog.show() + if messages: + self._messagesView.scroll_to_cell((len(messages)-1, )) + self._smsEntry.grab_focus() + + userResponse = self._dialog.run() + finally: + self._dialog.hide() + + if userResponse == gtk.RESPONSE_OK: + entryBuffer = self._smsEntry.get_buffer() + enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter()) + enteredMessage = enteredMessage[0:self.MAX_CHAR] + else: + enteredMessage = "" + + self._messagesView.remove_column(messageColumn) + self._messagesView.set_model(None) + + return enteredMessage.strip() + + def _update_letter_count(self, *args): + entryLength = self._smsEntry.get_buffer().get_char_count() + charsLeft = self.MAX_CHAR - entryLength + self._letterCountLabel.set_text(str(charsLeft)) + if charsLeft < 0: + self._smsButton.set_sensitive(False) + else: + self._smsButton.set_sensitive(True) + + def _on_entry_changed(self, *args): + self._update_letter_count() + + def _on_send(self, *args): + self._dialog.response(gtk.RESPONSE_OK) + + def _on_cancel(self, *args): + self._dialog.response(gtk.RESPONSE_CANCEL) + + +class Dialpad(object): + + def __init__(self, widgetTree, errorDisplay): + self._clipboard = gtk.clipboard_get() + self._errorDisplay = errorDisplay + self._smsDialog = SmsEntryDialog(widgetTree) + + self._numberdisplay = widgetTree.get_widget("numberdisplay") + self._smsButton = widgetTree.get_widget("sms") + self._dialButton = widgetTree.get_widget("dial") + self._backButton = widgetTree.get_widget("back") + self._phonenumber = "" + self._prettynumber = "" + + callbackMapping = { + "on_digit_clicked": self._on_digit_clicked, + } + widgetTree.signal_autoconnect(callbackMapping) + self._dialButton.connect("clicked", self._on_dial_clicked) + self._smsButton.connect("clicked", self._on_sms_clicked) + + self._originalLabel = self._backButton.get_label() + self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton) + self._backTapHandler.on_tap = self._on_backspace + self._backTapHandler.on_hold = self._on_clearall + self._backTapHandler.on_holding = self._set_clear_button + self._backTapHandler.on_cancel = self._reset_back_button + + self._window = gtk_toolbox.find_parent_window(self._numberdisplay) + self._keyPressEventId = 0 + + def enable(self): + self._dialButton.grab_focus() + self._backTapHandler.enable() + self._keyPressEventId = self._window.connect("key-press-event", self._on_key_press) + + def disable(self): + self._window.disconnect(self._keyPressEventId) + self._keyPressEventId = 0 + self._reset_back_button() + self._backTapHandler.disable() + + def number_selected(self, action, number, message): + """ + @note Actual dial function is patched in later + """ + raise NotImplementedError("Horrible unknown error has occurred") + + def get_number(self): + return self._phonenumber + + def set_number(self, number): + """ + Set the number to dial + """ + try: + self._phonenumber = make_ugly(number) + self._prettynumber = make_pretty(self._phonenumber) + self._numberdisplay.set_label("%s" % (self._prettynumber)) + except TypeError, e: + self._errorDisplay.push_exception() + + def clear(self): + self.set_number("") + + @staticmethod + def name(): + return "Dialpad" + + def load_settings(self, config, section): + pass + + def save_settings(self, config, section): + """ + @note Thread Agnostic + """ + pass + + def _on_key_press(self, widget, event): + try: + if event.keyval == ord("v") and event.get_state() & gtk.gdk.CONTROL_MASK: + contents = self._clipboard.wait_for_text() + if contents is not None: + self.set_number(contents) + except Exception, e: + self._errorDisplay.push_exception() + + def _on_sms_clicked(self, widget): + try: + action = PhoneTypeSelector.ACTION_SEND_SMS + phoneNumber = self.get_number() + + message = self._smsDialog.run(phoneNumber, (), self._window) + if not message: + phoneNumber = "" + action = PhoneTypeSelector.ACTION_CANCEL + + if action == PhoneTypeSelector.ACTION_CANCEL: + return + self.number_selected(action, phoneNumber, message) + except Exception, e: + self._errorDisplay.push_exception() + + def _on_dial_clicked(self, widget): + try: + action = PhoneTypeSelector.ACTION_DIAL + phoneNumber = self.get_number() + message = "" + self.number_selected(action, phoneNumber, message) + except Exception, e: + self._errorDisplay.push_exception() + + def _on_digit_clicked(self, widget): + try: + self.set_number(self._phonenumber + widget.get_name()[-1]) + except Exception, e: + self._errorDisplay.push_exception() + + def _on_backspace(self, taps): + try: + self.set_number(self._phonenumber[:-taps]) + self._reset_back_button() + except Exception, e: + self._errorDisplay.push_exception() + + def _on_clearall(self, taps): + try: + self.clear() + self._reset_back_button() + except Exception, e: + self._errorDisplay.push_exception() + return False + + def _set_clear_button(self): + try: + self._backButton.set_label("gtk-clear") + except Exception, e: + self._errorDisplay.push_exception() + + def _reset_back_button(self): + try: + self._backButton.set_label(self._originalLabel) + except Exception, e: + self._errorDisplay.push_exception() + + +class AccountInfo(object): + + def __init__(self, widgetTree, backend, alarmHandler, errorDisplay): + self._errorDisplay = errorDisplay + self._backend = backend + self._isPopulated = False + self._alarmHandler = alarmHandler + self._notifyOnMissed = False + self._notifyOnVoicemail = False + self._notifyOnSms = False + + self._callbackList = [] + self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display") + self._callbackSelectButton = widgetTree.get_widget("callbackSelectButton") + self._onCallbackSelectChangedId = 0 + + self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox") + self._minutesEntryButton = widgetTree.get_widget("minutesEntryButton") + self._missedCheckbox = widgetTree.get_widget("missedCheckbox") + self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox") + self._smsCheckbox = widgetTree.get_widget("smsCheckbox") + self._onNotifyToggled = 0 + self._onMinutesChanged = 0 + self._onMissedToggled = 0 + self._onVoicemailToggled = 0 + self._onSmsToggled = 0 + self._applyAlarmTimeoutId = None + + self._window = gtk_toolbox.find_parent_window(self._minutesEntryButton) + self._defaultCallback = "" + + def enable(self): + assert self._backend.is_authed(), "Attempting to enable backend while not logged in" + + self._accountViewNumberDisplay.set_use_markup(True) + self.set_account_number("") + + del self._callbackList[:] + self._onCallbackSelectChangedId = self._callbackSelectButton.connect("clicked", self._on_callbackentry_clicked) + + if self._alarmHandler is not None: + self._notifyCheckbox.set_active(self._alarmHandler.isEnabled) + self._minutesEntryButton.set_label("%d minutes" % self._alarmHandler.recurrence) + self._missedCheckbox.set_active(self._notifyOnMissed) + self._voicemailCheckbox.set_active(self._notifyOnVoicemail) + self._smsCheckbox.set_active(self._notifyOnSms) + + self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled) + self._onMinutesChanged = self._minutesEntryButton.connect("clicked", self._on_minutes_clicked) + self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled) + self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled) + self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled) + else: + self._notifyCheckbox.set_sensitive(False) + self._minutesEntryButton.set_sensitive(False) + self._missedCheckbox.set_sensitive(False) + self._voicemailCheckbox.set_sensitive(False) + self._smsCheckbox.set_sensitive(False) + + self.update(force=True) + + def disable(self): + self._callbackSelectButton.disconnect(self._onCallbackSelectChangedId) + self._onCallbackSelectChangedId = 0 + + if self._alarmHandler is not None: + self._notifyCheckbox.disconnect(self._onNotifyToggled) + self._minutesEntryButton.disconnect(self._onMinutesChanged) + self._missedCheckbox.disconnect(self._onNotifyToggled) + self._voicemailCheckbox.disconnect(self._onNotifyToggled) + self._smsCheckbox.disconnect(self._onNotifyToggled) + self._onNotifyToggled = 0 + self._onMinutesChanged = 0 + self._onMissedToggled = 0 + self._onVoicemailToggled = 0 + self._onSmsToggled = 0 + else: + self._notifyCheckbox.set_sensitive(True) + self._minutesEntryButton.set_sensitive(True) + self._missedCheckbox.set_sensitive(True) + self._voicemailCheckbox.set_sensitive(True) + self._smsCheckbox.set_sensitive(True) + + self.clear() + del self._callbackList[:] + + def get_selected_callback_number(self): + currentLabel = self._callbackSelectButton.get_label() + if currentLabel is not None: + return make_ugly(currentLabel) + else: + return "" + + def set_account_number(self, number): + """ + Displays current account number + """ + self._accountViewNumberDisplay.set_label("%s" % (number)) + + def update(self, force = False): + if not force and self._isPopulated: + return False + self._populate_callback_combo() + self.set_account_number(self._backend.get_account_number()) + return True + + def clear(self): + self._callbackSelectButton.set_label("") + self.set_account_number("") + self._isPopulated = False + + def save_everything(self): + raise NotImplementedError + + @staticmethod + def name(): + return "Account Info" + + def load_settings(self, config, section): + self._defaultCallback = config.get(section, "callback") + self._notifyOnMissed = config.getboolean(section, "notifyOnMissed") + self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail") + self._notifyOnSms = config.getboolean(section, "notifyOnSms") + + def save_settings(self, config, section): + """ + @note Thread Agnostic + """ + callback = self.get_selected_callback_number() + config.set(section, "callback", callback) + config.set(section, "notifyOnMissed", repr(self._notifyOnMissed)) + config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail)) + config.set(section, "notifyOnSms", repr(self._notifyOnSms)) + + def _populate_callback_combo(self): + self._isPopulated = True + del self._callbackList[:] + try: + callbackNumbers = self._backend.get_callback_numbers() + except Exception, e: + self._errorDisplay.push_exception() + self._isPopulated = False + return + + for number, description in callbackNumbers.iteritems(): + self._callbackList.append(make_pretty(number)) + + if not self.get_selected_callback_number(): + self._set_callback_number(self._defaultCallback) + + def _set_callback_number(self, number): + try: + if not self._backend.is_valid_syntax(number) and 0 < len(number): + self._errorDisplay.push_message("%s is not a valid callback number" % number) + elif number == self._backend.get_callback_number(): + logging.warning( + "Callback number already is %s" % ( + self._backend.get_callback_number(), + ), + ) + else: + self._backend.set_callback_number(number) + assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % ( + make_pretty(number), make_pretty(self._backend.get_callback_number()) + ) + self._callbackSelectButton.set_label(make_pretty(number)) + logging.info( + "Callback number set to %s" % ( + self._backend.get_callback_number(), + ), + ) + except Exception, e: + self._errorDisplay.push_exception() + + def _update_alarm_settings(self, recurrence): + try: + isEnabled = self._notifyCheckbox.get_active() + if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence: + self._alarmHandler.apply_settings(isEnabled, recurrence) + finally: + self.save_everything() + self._notifyCheckbox.set_active(self._alarmHandler.isEnabled) + self._minutesEntryButton.set_label("%d Minutes" % self._alarmHandler.recurrence) + + def _on_callbackentry_clicked(self, *args): + try: + actualSelection = make_pretty(self.get_selected_callback_number()) + + userSelection = hildonize.touch_selector_entry( + self._window, + "Callback Number", + self._callbackList, + actualSelection, + ) + number = make_ugly(userSelection) + self._set_callback_number(number) + except RuntimeError, e: + logging.exception("%s" % str(e)) + except Exception, e: + self._errorDisplay.push_exception() + + def _on_notify_toggled(self, *args): + try: + if self._applyAlarmTimeoutId is not None: + gobject.source_remove(self._applyAlarmTimeoutId) + self._applyAlarmTimeoutId = None + self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout) + except Exception, e: + self._errorDisplay.push_exception() + + def _on_minutes_clicked(self, *args): + recurrenceChoices = [ + (1, "1 minute"), + (2, "2 minutes"), + (3, "3 minutes"), + (5, "5 minutes"), + (8, "8 minutes"), + (10, "10 minutes"), + (15, "15 minutes"), + (30, "30 minutes"), + (45, "45 minutes"), + (60, "1 hour"), + (3*60, "3 hours"), + (6*60, "6 hours"), + (12*60, "12 hours"), + ] + try: + actualSelection = self._alarmHandler.recurrence + + closestSelectionIndex = 0 + for i, possible in enumerate(recurrenceChoices): + if possible[0] <= actualSelection: + closestSelectionIndex = i + recurrenceIndex = hildonize.touch_selector( + self._window, + "Minutes", + (("%s" % m[1]) for m in recurrenceChoices), + closestSelectionIndex, + ) + recurrence = recurrenceChoices[recurrenceIndex][0] + + self._update_alarm_settings(recurrence) + except RuntimeError, e: + logging.exception("%s" % str(e)) + except Exception, e: + self._errorDisplay.push_exception() + + def _on_apply_timeout(self, *args): + try: + self._applyAlarmTimeoutId = None + + self._update_alarm_settings(self._alarmHandler.recurrence) + except Exception, e: + self._errorDisplay.push_exception() + return False + + def _on_missed_toggled(self, *args): + try: + self._notifyOnMissed = self._missedCheckbox.get_active() + self.save_everything() + except Exception, e: + self._errorDisplay.push_exception() + + def _on_voicemail_toggled(self, *args): + try: + self._notifyOnVoicemail = self._voicemailCheckbox.get_active() + self.save_everything() + except Exception, e: + self._errorDisplay.push_exception() + + def _on_sms_toggled(self, *args): + try: + self._notifyOnSms = self._smsCheckbox.get_active() + self.save_everything() + except Exception, e: + self._errorDisplay.push_exception() + + +class RecentCallsView(object): + + NUMBER_IDX = 0 + DATE_IDX = 1 + ACTION_IDX = 2 + FROM_IDX = 3 + + def __init__(self, widgetTree, backend, errorDisplay): + self._errorDisplay = errorDisplay + self._backend = backend + + self._isPopulated = False + self._recentmodel = gtk.ListStore( + gobject.TYPE_STRING, # number + gobject.TYPE_STRING, # date + gobject.TYPE_STRING, # action + gobject.TYPE_STRING, # from + ) + self._recentview = widgetTree.get_widget("recentview") + self._recentviewselection = None + self._onRecentviewRowActivatedId = 0 + + textrenderer = gtk.CellRendererText() + textrenderer.set_property("yalign", 0) + self._dateColumn = gtk.TreeViewColumn("Date") + self._dateColumn.pack_start(textrenderer, expand=True) + self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX) + + textrenderer = gtk.CellRendererText() + textrenderer.set_property("yalign", 0) + self._actionColumn = gtk.TreeViewColumn("Action") + self._actionColumn.pack_start(textrenderer, expand=True) + self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX) + + textrenderer = gtk.CellRendererText() + textrenderer.set_property("yalign", 0) + textrenderer.set_property("ellipsize", pango.ELLIPSIZE_END) + textrenderer.set_property("width-chars", len("1 (555) 555-1234")) + self._numberColumn = gtk.TreeViewColumn("Number") + self._numberColumn.pack_start(textrenderer, expand=True) + self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX) + + textrenderer = gtk.CellRendererText() + textrenderer.set_property("yalign", 0) + hildonize.set_cell_thumb_selectable(textrenderer) + self._nameColumn = gtk.TreeViewColumn("From") + self._nameColumn.pack_start(textrenderer, expand=True) + self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX) + self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) + + self._window = gtk_toolbox.find_parent_window(self._recentview) + self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend) + + self._updateSink = gtk_toolbox.threaded_stage( + gtk_toolbox.comap( + self._idly_populate_recentview, + gtk_toolbox.null_sink(), + ) + ) + + def enable(self): + assert self._backend.is_authed(), "Attempting to enable backend while not logged in" + self._recentview.set_model(self._recentmodel) + + self._recentview.append_column(self._dateColumn) + self._recentview.append_column(self._actionColumn) + self._recentview.append_column(self._numberColumn) + self._recentview.append_column(self._nameColumn) + self._recentviewselection = self._recentview.get_selection() + self._recentviewselection.set_mode(gtk.SELECTION_SINGLE) + + self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated) + + def disable(self): + self._recentview.disconnect(self._onRecentviewRowActivatedId) + + self.clear() + + self._recentview.remove_column(self._dateColumn) + self._recentview.remove_column(self._actionColumn) + self._recentview.remove_column(self._nameColumn) + self._recentview.remove_column(self._numberColumn) + self._recentview.set_model(None) + + def number_selected(self, action, number, message): + """ + @note Actual dial function is patched in later + """ + raise NotImplementedError("Horrible unknown error has occurred") + + def update(self, force = False): + if not force and self._isPopulated: + return False + self._updateSink.send(()) + return True + + def clear(self): + self._isPopulated = False + self._recentmodel.clear() + + @staticmethod + def name(): + return "Recent Calls" + + def load_settings(self, config, section): + pass + + def save_settings(self, config, section): + """ + @note Thread Agnostic + """ + pass + + def _idly_populate_recentview(self): + with gtk_toolbox.gtk_lock(): + banner = hildonize.show_busy_banner_start(self._window, "Loading Recent History") + try: + self._recentmodel.clear() + self._isPopulated = True + + try: + recentItems = self._backend.get_recent() + except Exception, e: + self._errorDisplay.push_exception_with_lock() + self._isPopulated = False + recentItems = [] + + for personName, phoneNumber, date, action in recentItems: + if not personName: + personName = "Unknown" + date = abbrev_relative_date(date) + prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber + prettyNumber = make_pretty(prettyNumber) + item = (prettyNumber, date, action.capitalize(), personName) + with gtk_toolbox.gtk_lock(): + self._recentmodel.append(item) + except Exception, e: + self._errorDisplay.push_exception_with_lock() + finally: + with gtk_toolbox.gtk_lock(): + hildonize.show_busy_banner_end(banner) + + return False + + def _on_recentview_row_activated(self, treeview, path, view_column): + try: + model, itr = self._recentviewselection.get_selected() + if not itr: + return + + number = self._recentmodel.get_value(itr, self.NUMBER_IDX) + number = make_ugly(number) + contactPhoneNumbers = [("Phone", number)] + description = self._recentmodel.get_value(itr, self.FROM_IDX) + + action, phoneNumber, message = self._phoneTypeSelector.run( + contactPhoneNumbers, + messages = (description, ), + parent = self._window, + ) + if action == PhoneTypeSelector.ACTION_CANCEL: + return + assert phoneNumber, "A lack of phone number exists" + + self.number_selected(action, phoneNumber, message) + self._recentviewselection.unselect_all() + except Exception, e: + self._errorDisplay.push_exception() + + +class MessagesView(object): + + NUMBER_IDX = 0 + DATE_IDX = 1 + HEADER_IDX = 2 + MESSAGE_IDX = 3 + MESSAGES_IDX = 4 + + def __init__(self, widgetTree, backend, errorDisplay): + self._errorDisplay = errorDisplay + self._backend = backend + + self._isPopulated = False + self._messagemodel = gtk.ListStore( + gobject.TYPE_STRING, # number + gobject.TYPE_STRING, # date + gobject.TYPE_STRING, # header + gobject.TYPE_STRING, # message + object, # messages + ) + self._messageview = widgetTree.get_widget("messages_view") + self._messageviewselection = None + self._onMessageviewRowActivatedId = 0 + + self._messageRenderer = gtk.CellRendererText() + self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD) + self._messageRenderer.set_property("wrap-width", 500) + self._messageColumn = gtk.TreeViewColumn("Messages") + self._messageColumn.pack_start(self._messageRenderer, expand=True) + self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX) + self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) + + self._window = gtk_toolbox.find_parent_window(self._messageview) + self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend) + + self._updateSink = gtk_toolbox.threaded_stage( + gtk_toolbox.comap( + self._idly_populate_messageview, + gtk_toolbox.null_sink(), + ) + ) + + def enable(self): + assert self._backend.is_authed(), "Attempting to enable backend while not logged in" + self._messageview.set_model(self._messagemodel) + self._messageview.set_headers_visible(False) + + self._messageview.append_column(self._messageColumn) + self._messageviewselection = self._messageview.get_selection() + self._messageviewselection.set_mode(gtk.SELECTION_SINGLE) + + self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated) + + def disable(self): + self._messageview.disconnect(self._onMessageviewRowActivatedId) + + self.clear() + + self._messageview.remove_column(self._messageColumn) + self._messageview.set_model(None) + + def number_selected(self, action, number, message): + """ + @note Actual dial function is patched in later + """ + raise NotImplementedError("Horrible unknown error has occurred") + + def update(self, force = False): + if not force and self._isPopulated: + return False + self._updateSink.send(()) + return True + + def clear(self): + self._isPopulated = False + self._messagemodel.clear() + + @staticmethod + def name(): + return "Messages" + + def load_settings(self, config, section): + pass + + def save_settings(self, config, section): + """ + @note Thread Agnostic + """ + pass + + def _idly_populate_messageview(self): + with gtk_toolbox.gtk_lock(): + banner = hildonize.show_busy_banner_start(self._window, "Loading Messages") + try: + self._messagemodel.clear() + self._isPopulated = True + + try: + messageItems = self._backend.get_messages() + except Exception, e: + self._errorDisplay.push_exception_with_lock() + self._isPopulated = False + messageItems = [] + + for header, number, relativeDate, messages in messageItems: + prettyNumber = number[2:] if number.startswith("+1") else number + prettyNumber = make_pretty(prettyNumber) + + firstMessage = "%s - %s (%s)" % (header, prettyNumber, relativeDate) + newMessages = [firstMessage] + newMessages.extend(messages) + + number = make_ugly(number) + + row = (number, relativeDate, header, "\n".join(newMessages), newMessages) + with gtk_toolbox.gtk_lock(): + self._messagemodel.append(row) + except Exception, e: + self._errorDisplay.push_exception_with_lock() + finally: + with gtk_toolbox.gtk_lock(): + hildonize.show_busy_banner_end(banner) + + return False + + def _on_messageview_row_activated(self, treeview, path, view_column): + try: + model, itr = self._messageviewselection.get_selected() + if not itr: + return + + contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))] + description = self._messagemodel.get_value(itr, self.MESSAGES_IDX) + + action, phoneNumber, message = self._phoneTypeSelector.run( + contactPhoneNumbers, + messages = description, + parent = self._window, + ) + if action == PhoneTypeSelector.ACTION_CANCEL: + return + assert phoneNumber, "A lock of phone number exists" + + self.number_selected(action, phoneNumber, message) + self._messageviewselection.unselect_all() + except Exception, e: + self._errorDisplay.push_exception() + + +class ContactsView(object): + + def __init__(self, widgetTree, backend, errorDisplay): + self._errorDisplay = errorDisplay + self._backend = backend + + self._addressBook = None + self._selectedComboIndex = 0 + self._addressBookFactories = [null_backend.NullAddressBook()] + + self._booksList = [] + self._bookSelectionButton = widgetTree.get_widget("addressbookSelectButton") + + self._isPopulated = False + self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING) + self._contactsviewselection = None + self._contactsview = widgetTree.get_widget("contactsview") + + self._contactColumn = gtk.TreeViewColumn("Contact") + displayContactSource = False + if displayContactSource: + textrenderer = gtk.CellRendererText() + self._contactColumn.pack_start(textrenderer, expand=False) + self._contactColumn.add_attribute(textrenderer, 'text', 0) + textrenderer = gtk.CellRendererText() + hildonize.set_cell_thumb_selectable(textrenderer) + self._contactColumn.pack_start(textrenderer, expand=True) + self._contactColumn.add_attribute(textrenderer, 'text', 1) + textrenderer = gtk.CellRendererText() + self._contactColumn.pack_start(textrenderer, expand=True) + self._contactColumn.add_attribute(textrenderer, 'text', 4) + self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) + self._contactColumn.set_sort_column_id(1) + self._contactColumn.set_visible(True) + + self._onContactsviewRowActivatedId = 0 + self._onAddressbookButtonChangedId = 0 + self._window = gtk_toolbox.find_parent_window(self._contactsview) + self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend) + + self._updateSink = gtk_toolbox.threaded_stage( + gtk_toolbox.comap( + self._idly_populate_contactsview, + gtk_toolbox.null_sink(), + ) + ) + + def enable(self): + assert self._backend.is_authed(), "Attempting to enable backend while not logged in" + + self._contactsview.set_model(self._contactsmodel) + self._contactsview.append_column(self._contactColumn) + self._contactsviewselection = self._contactsview.get_selection() + self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE) + + del self._booksList[:] + for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks(): + if factoryName and bookName: + entryName = "%s: %s" % (factoryName, bookName) + elif factoryName: + entryName = factoryName + elif bookName: + entryName = bookName + else: + entryName = "Bad name (%d)" % factoryId + row = (str(factoryId), bookId, entryName) + self._booksList.append(row) + + self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated) + self._onAddressbookButtonChangedId = self._bookSelectionButton.connect("clicked", self._on_addressbook_button_changed) + + if len(self._booksList) <= self._selectedComboIndex: + self._selectedComboIndex = 0 + self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2]) + + selectedFactoryId = self._booksList[self._selectedComboIndex][0] + selectedBookId = self._booksList[self._selectedComboIndex][1] + self.open_addressbook(selectedFactoryId, selectedBookId) + + def disable(self): + self._contactsview.disconnect(self._onContactsviewRowActivatedId) + self._bookSelectionButton.disconnect(self._onAddressbookButtonChangedId) + + self.clear() + + self._bookSelectionButton.set_label("") + self._contactsview.set_model(None) + self._contactsview.remove_column(self._contactColumn) + + def number_selected(self, action, number, message): + """ + @note Actual dial function is patched in later + """ + raise NotImplementedError("Horrible unknown error has occurred") + + def get_addressbooks(self): + """ + @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name)) + """ + for i, factory in enumerate(self._addressBookFactories): + for bookFactory, bookId, bookName in factory.get_addressbooks(): + yield (str(i), bookId), (factory.factory_name(), bookName) + + def open_addressbook(self, bookFactoryId, bookId): + bookFactoryIndex = int(bookFactoryId) + addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId) + + forceUpdate = True if addressBook is not self._addressBook else False + + self._addressBook = addressBook + self.update(force=forceUpdate) + + def update(self, force = False): + if not force and self._isPopulated: + return False + self._updateSink.send(()) + return True + + def clear(self): + self._isPopulated = False + self._contactsmodel.clear() + for factory in self._addressBookFactories: + factory.clear_caches() + self._addressBook.clear_caches() + + def append(self, book): + self._addressBookFactories.append(book) + + def extend(self, books): + self._addressBookFactories.extend(books) + + @staticmethod + def name(): + return "Contacts" + + def load_settings(self, config, sectionName): + try: + self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook") + except ConfigParser.NoOptionError: + self._selectedComboIndex = 0 + + def save_settings(self, config, sectionName): + config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex)) + + def _idly_populate_contactsview(self): + with gtk_toolbox.gtk_lock(): + banner = hildonize.show_busy_banner_start(self._window, "Loading Contacts") + try: + addressBook = None + while addressBook is not self._addressBook: + addressBook = self._addressBook + with gtk_toolbox.gtk_lock(): + self._contactsview.set_model(None) + self.clear() + + try: + contacts = addressBook.get_contacts() + except Exception, e: + contacts = [] + self._isPopulated = False + self._errorDisplay.push_exception_with_lock() + for contactId, contactName in contacts: + contactType = (addressBook.contact_source_short_name(contactId), ) + self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", )) + + with gtk_toolbox.gtk_lock(): + self._contactsview.set_model(self._contactsmodel) + + self._isPopulated = True + except Exception, e: + self._errorDisplay.push_exception_with_lock() + finally: + with gtk_toolbox.gtk_lock(): + hildonize.show_busy_banner_end(banner) + return False + + def _on_addressbook_button_changed(self, *args, **kwds): + try: + try: + newSelectedComboIndex = hildonize.touch_selector( + self._window, + "Addressbook", + (("%s" % m[2]) for m in self._booksList), + self._selectedComboIndex, + ) + except RuntimeError: + return + + selectedFactoryId = self._booksList[newSelectedComboIndex][0] + selectedBookId = self._booksList[newSelectedComboIndex][1] + self.open_addressbook(selectedFactoryId, selectedBookId) + self._selectedComboIndex = newSelectedComboIndex + self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2]) + except Exception, e: + self._errorDisplay.push_exception() + + def _on_contactsview_row_activated(self, treeview, path, view_column): + try: + model, itr = self._contactsviewselection.get_selected() + if not itr: + return + + contactId = self._contactsmodel.get_value(itr, 3) + contactName = self._contactsmodel.get_value(itr, 1) + try: + contactDetails = self._addressBook.get_contact_details(contactId) + except Exception, e: + contactDetails = [] + self._errorDisplay.push_exception() + contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails] + + if len(contactPhoneNumbers) == 0: + return + + action, phoneNumber, message = self._phoneTypeSelector.run( + contactPhoneNumbers, + messages = (contactName, ), + parent = self._window, + ) + if action == PhoneTypeSelector.ACTION_CANCEL: + return + assert phoneNumber, "A lack of phone number exists" + + self.number_selected(action, phoneNumber, message) + self._contactsviewselection.unselect_all() + except Exception, e: + self._errorDisplay.push_exception() diff --git a/src/hildonize.py b/src/hildonize.py new file mode 100755 index 0000000..391b365 --- /dev/null +++ b/src/hildonize.py @@ -0,0 +1,646 @@ +#!/usr/bin/env python + + +import gobject +import gtk +import dbus + + +class _NullHildonModule(object): + pass + + +try: + import hildon as _hildon + hildon = _hildon # Dumb but gets around pyflakiness +except (ImportError, OSError): + hildon = _NullHildonModule + + +IS_HILDON_SUPPORTED = hildon is not _NullHildonModule + + +class _NullHildonProgram(object): + + def add_window(self, window): + pass + + +def _hildon_get_app_class(): + return hildon.Program + + +def _null_get_app_class(): + return _NullHildonProgram + + +try: + hildon.Program + get_app_class = _hildon_get_app_class +except AttributeError: + get_app_class = _null_get_app_class + + +def _hildon_set_application_title(window, title): + pass + + +def _null_set_application_title(window, title): + window.set_title(title) + + +if IS_HILDON_SUPPORTED: + set_application_title = _hildon_set_application_title +else: + set_application_title = _null_set_application_title + + +def _fremantle_hildonize_window(app, window): + oldWindow = window + newWindow = hildon.StackableWindow() + oldWindow.get_child().reparent(newWindow) + app.add_window(newWindow) + return newWindow + + +def _hildon_hildonize_window(app, window): + oldWindow = window + newWindow = hildon.Window() + oldWindow.get_child().reparent(newWindow) + app.add_window(newWindow) + return newWindow + + +def _null_hildonize_window(app, window): + return window + + +try: + hildon.StackableWindow + hildonize_window = _fremantle_hildonize_window +except AttributeError: + try: + hildon.Window + hildonize_window = _hildon_hildonize_window + except AttributeError: + hildonize_window = _null_hildonize_window + + +def _fremantle_hildonize_menu(window, gtkMenu, buttons): + appMenu = hildon.AppMenu() + for button in buttons: + appMenu.append(button) + window.set_app_menu(appMenu) + gtkMenu.get_parent().remove(gtkMenu) + return appMenu + + +def _hildon_hildonize_menu(window, gtkMenu, ignoredButtons): + hildonMenu = gtk.Menu() + for child in gtkMenu.get_children(): + child.reparent(hildonMenu) + window.set_menu(hildonMenu) + gtkMenu.destroy() + return hildonMenu + + +def _null_hildonize_menu(window, gtkMenu, ignoredButtons): + return gtkMenu + + +try: + hildon.AppMenu + GTK_MENU_USED = False + IS_FREMANTLE_SUPPORTED = True + hildonize_menu = _fremantle_hildonize_menu +except AttributeError: + GTK_MENU_USED = True + IS_FREMANTLE_SUPPORTED = False + if IS_HILDON_SUPPORTED: + hildonize_menu = _hildon_hildonize_menu + else: + hildonize_menu = _null_hildonize_menu + + +def _hildon_set_cell_thumb_selectable(renderer): + renderer.set_property("scale", 1.5) + + +def _null_set_cell_thumb_selectable(renderer): + pass + + +if IS_HILDON_SUPPORTED: + set_cell_thumb_selectable = _hildon_set_cell_thumb_selectable +else: + set_cell_thumb_selectable = _null_set_cell_thumb_selectable + + +def _fremantle_show_information_banner(parent, message): + hildon.hildon_banner_show_information(parent, "", message) + + +def _hildon_show_information_banner(parent, message): + hildon.hildon_banner_show_information(parent, None, message) + + +def _null_show_information_banner(parent, message): + pass + + +if IS_FREMANTLE_SUPPORTED: + show_information_banner = _fremantle_show_information_banner +else: + try: + hildon.hildon_banner_show_information + show_information_banner = _hildon_show_information_banner + except AttributeError: + show_information_banner = _null_show_information_banner + + +def _fremantle_show_busy_banner_start(parent, message): + hildon.hildon_gtk_window_set_progress_indicator(parent, True) + return parent + + +def _fremantle_show_busy_banner_end(banner): + hildon.hildon_gtk_window_set_progress_indicator(banner, False) + + +def _hildon_show_busy_banner_start(parent, message): + return hildon.hildon_banner_show_animation(parent, None, message) + + +def _hildon_show_busy_banner_end(banner): + banner.destroy() + + +def _null_show_busy_banner_start(parent, message): + return None + + +def _null_show_busy_banner_end(banner): + assert banner is None + + +try: + hildon.hildon_gtk_window_set_progress_indicator + show_busy_banner_start = _fremantle_show_busy_banner_start + show_busy_banner_end = _fremantle_show_busy_banner_end +except AttributeError: + try: + hildon.hildon_banner_show_animation + show_busy_banner_start = _hildon_show_busy_banner_start + show_busy_banner_end = _hildon_show_busy_banner_end + except AttributeError: + show_busy_banner_start = _null_show_busy_banner_start + show_busy_banner_end = _null_show_busy_banner_end + + +def _hildon_hildonize_text_entry(textEntry): + textEntry.set_property('hildon-input-mode', 7) + + +def _null_hildonize_text_entry(textEntry): + pass + + +if IS_HILDON_SUPPORTED: + hildonize_text_entry = _hildon_hildonize_text_entry +else: + hildonize_text_entry = _null_hildonize_text_entry + + +def _hildon_mark_window_rotatable(window): + # gtk documentation is unclear whether this does a "=" or a "|=" + window.set_flags(hildon.HILDON_PORTRAIT_MODE_SUPPORT) + + +def _null_mark_window_rotatable(window): + pass + + +try: + hildon.HILDON_PORTRAIT_MODE_SUPPORT + mark_window_rotatable = _hildon_mark_window_rotatable +except AttributeError: + mark_window_rotatable = _null_mark_window_rotatable + + +def _hildon_window_to_portrait(window): + # gtk documentation is unclear whether this does a "=" or a "|=" + window.set_flags(hildon.HILDON_PORTRAIT_MODE_SUPPORT) + + +def _hildon_window_to_landscape(window): + # gtk documentation is unclear whether this does a "=" or a "&= ~" + window.unset_flags(hildon.HILDON_PORTRAIT_MODE_REQUEST) + + +def _null_window_to_portrait(window): + pass + + +def _null_window_to_landscape(window): + pass + + +try: + hildon.HILDON_PORTRAIT_MODE_SUPPORT + hildon.HILDON_PORTRAIT_MODE_REQUEST + + window_to_portrait = _hildon_window_to_portrait + window_to_landscape = _hildon_window_to_landscape +except AttributeError: + window_to_portrait = _null_window_to_portrait + window_to_landscape = _null_window_to_landscape + + +def get_device_orientation(): + bus = dbus.SystemBus() + try: + rawMceRequest = bus.get_object("com.nokia.mce", "/com/nokia/mce/request") + mceRequest = dbus.Interface(rawMceRequest, dbus_interface="com.nokia.mce.request") + orientation, standState, faceState, xAxis, yAxis, zAxis = mceRequest.get_device_orientation() + except dbus.exception.DBusException: + # catching for documentation purposes that when a system doesn't + # support this, this is what to expect + raise + + if orientation == "": + return gtk.ORIENTATION_HORIZONTAL + elif orientation == "": + return gtk.ORIENTATION_VERTICAL + else: + raise RuntimeError("Unknown orientation: %s" % orientation) + + +def _hildon_hildonize_password_entry(textEntry): + textEntry.set_property('hildon-input-mode', 7 | (1 << 29)) + + +def _null_hildonize_password_entry(textEntry): + pass + + +if IS_HILDON_SUPPORTED: + hildonize_password_entry = _hildon_hildonize_password_entry +else: + hildonize_password_entry = _null_hildonize_password_entry + + +def _hildon_hildonize_combo_entry(comboEntry): + comboEntry.set_property('hildon-input-mode', 1 << 4) + + +def _null_hildonize_combo_entry(textEntry): + pass + + +if IS_HILDON_SUPPORTED: + hildonize_combo_entry = _hildon_hildonize_combo_entry +else: + hildonize_combo_entry = _null_hildonize_combo_entry + + +def _fremantle_hildonize_scrollwindow(scrolledWindow): + pannableWindow = hildon.PannableArea() + + child = scrolledWindow.get_child() + scrolledWindow.remove(child) + pannableWindow.add(child) + + parent = scrolledWindow.get_parent() + parent.remove(scrolledWindow) + parent.add(pannableWindow) + + return pannableWindow + + +def _hildon_hildonize_scrollwindow(scrolledWindow): + hildon.hildon_helper_set_thumb_scrollbar(scrolledWindow, True) + return scrolledWindow + + +def _null_hildonize_scrollwindow(scrolledWindow): + return scrolledWindow + + +try: + hildon.PannableArea + hildonize_scrollwindow = _fremantle_hildonize_scrollwindow + hildonize_scrollwindow_with_viewport = _hildon_hildonize_scrollwindow +except AttributeError: + try: + hildon.hildon_helper_set_thumb_scrollbar + hildonize_scrollwindow = _hildon_hildonize_scrollwindow + hildonize_scrollwindow_with_viewport = _hildon_hildonize_scrollwindow + except AttributeError: + hildonize_scrollwindow = _null_hildonize_scrollwindow + hildonize_scrollwindow_with_viewport = _null_hildonize_scrollwindow + + +def _hildon_request_number(parent, title, range, default): + spinner = hildon.NumberEditor(*range) + spinner.set_value(default) + + dialog = gtk.Dialog( + title, + parent, + gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT, + (gtk.STOCK_OK, gtk.RESPONSE_OK, gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL), + ) + dialog.set_default_response(gtk.RESPONSE_CANCEL) + dialog.get_child().add(spinner) + + try: + dialog.show_all() + response = dialog.run() + finally: + dialog.hide() + + if response == gtk.RESPONSE_OK: + return spinner.get_value() + elif response == gtk.RESPONSE_CANCEL or response == gtk.RESPONSE_DELETE_EVENT: + raise RuntimeError("User cancelled request") + else: + raise RuntimeError("Unrecognized response %r", response) + + +def _null_request_number(parent, title, range, default): + adjustment = gtk.Adjustment(default, range[0], range[1], 1, 5, 0) + spinner = gtk.SpinButton(adjustment, 0, 0) + spinner.set_wrap(False) + + dialog = gtk.Dialog( + title, + parent, + gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT, + (gtk.STOCK_OK, gtk.RESPONSE_OK, gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL), + ) + dialog.set_default_response(gtk.RESPONSE_CANCEL) + dialog.get_child().add(spinner) + + try: + dialog.show_all() + response = dialog.run() + finally: + dialog.hide() + + if response == gtk.RESPONSE_OK: + return spinner.get_value_as_int() + elif response == gtk.RESPONSE_CANCEL or response == gtk.RESPONSE_DELETE_EVENT: + raise RuntimeError("User cancelled request") + else: + raise RuntimeError("Unrecognized response %r", response) + + +try: + hildon.NumberEditor # TODO deprecated in fremantle + request_number = _hildon_request_number +except AttributeError: + request_number = _null_request_number + + +def _hildon_touch_selector(parent, title, items, defaultIndex): + model = gtk.ListStore(gobject.TYPE_STRING) + for item in items: + model.append((item, )) + + selector = hildon.TouchSelector() + selector.append_text_column(model, True) + selector.set_column_selection_mode(hildon.TOUCH_SELECTOR_SELECTION_MODE_SINGLE) + selector.set_active(0, defaultIndex) + + dialog = hildon.PickerDialog(parent) + dialog.set_selector(selector) + + try: + dialog.show_all() + response = dialog.run() + finally: + dialog.hide() + + if response == gtk.RESPONSE_OK: + return selector.get_active(0) + elif response == gtk.RESPONSE_CANCEL or response == gtk.RESPONSE_DELETE_EVENT: + raise RuntimeError("User cancelled request") + else: + raise RuntimeError("Unrecognized response %r", response) + + +def _on_null_touch_selector_activated(treeView, path, column, dialog): + dialog.response(gtk.RESPONSE_OK) + + +def _null_touch_selector(parent, title, items, defaultIndex = -1): + model = gtk.ListStore(gobject.TYPE_STRING) + for item in items: + model.append((item, )) + + cell = gtk.CellRendererText() + set_cell_thumb_selectable(cell) + column = gtk.TreeViewColumn(title) + column .pack_start(cell, expand=True) + column.add_attribute(cell, "text", 0) + + treeView = gtk.TreeView() + treeView.set_model(model) + treeView.append_column(column) + selection = treeView.get_selection() + selection.set_mode(gtk.SELECTION_SINGLE) + if 0 < defaultIndex: + selection.select_path((defaultIndex, )) + + scrolledWin = gtk.ScrolledWindow() + scrolledWin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + scrolledWin.add(treeView) + hildonize_scrollwindow(scrolledWin) + + dialog = gtk.Dialog( + title, + parent, + gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT, + (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL), + ) + dialog.set_default_response(gtk.RESPONSE_CANCEL) + dialog.get_child().add(scrolledWin) + parentSize = parent.get_size() + dialog.resize(parentSize[0], max(parentSize[1]-100, 100)) + treeView.connect("row-activated", _on_null_touch_selector_activated, dialog) + + try: + dialog.show_all() + response = dialog.run() + finally: + dialog.hide() + + if response == gtk.RESPONSE_OK: + model, itr = selection.get_selected() + if itr is None: + raise RuntimeError("No selection made") + return model.get_path(itr)[0] + elif response == gtk.RESPONSE_CANCEL or response == gtk.RESPONSE_DELETE_EVENT: + raise RuntimeError("User cancelled request") + else: + raise RuntimeError("Unrecognized response %r", response) + + +try: + hildon.PickerDialog + hildon.TouchSelector + touch_selector = _hildon_touch_selector +except AttributeError: + touch_selector = _null_touch_selector + + +def _hildon_touch_selector_entry(parent, title, items, defaultItem): + # Got a segfault when using append_text_column with TouchSelectorEntry, so using this way + selector = hildon.hildon_touch_selector_entry_new_text() + defaultIndex = -1 + for i, item in enumerate(items): + selector.append_text(item) + if item == defaultItem: + defaultIndex = i + + dialog = hildon.PickerDialog(parent) + dialog.set_selector(selector) + + if 0 < defaultIndex: + selector.set_active(0, defaultIndex) + else: + selector.get_entry().set_text(defaultItem) + + try: + dialog.show_all() + response = dialog.run() + finally: + dialog.hide() + + if response == gtk.RESPONSE_OK: + selectedIndex = selector.get_active(0) + return selector.get_entry().get_text() + elif response == gtk.RESPONSE_CANCEL or response == gtk.RESPONSE_DELETE_EVENT: + raise RuntimeError("User cancelled request") + else: + raise RuntimeError("Unrecognized response %r", response) + + +def _on_null_touch_selector_entry_entry_activated(entry, dialog, customEntry, result): + dialog.response(gtk.RESPONSE_OK) + result.append(customEntry.get_text()) + + +def _on_null_touch_selector_entry_tree_activated(treeView, path, column, dialog, selection, result): + dialog.response(gtk.RESPONSE_OK) + model, itr = selection.get_selected() + if itr is not None: + result.append(model.get_value(itr, 0)) + + +def _null_touch_selector_entry(parent, title, items, defaultItem): + model = gtk.ListStore(gobject.TYPE_STRING) + defaultIndex = -1 + for i, item in enumerate(items): + model.append((item, )) + if item == defaultItem: + defaultIndex = i + + cell = gtk.CellRendererText() + set_cell_thumb_selectable(cell) + column = gtk.TreeViewColumn(title) + column .pack_start(cell, expand=True) + column.add_attribute(cell, "text", 0) + + treeView = gtk.TreeView() + treeView.set_model(model) + treeView.append_column(column) + selection = treeView.get_selection() + selection.set_mode(gtk.SELECTION_SINGLE) + + scrolledWin = gtk.ScrolledWindow() + scrolledWin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + scrolledWin.add(treeView) + hildonize_scrollwindow(scrolledWin) + + customEntry = gtk.Entry() + + layout = gtk.VBox() + layout.pack_start(customEntry, expand=False) + layout.pack_start(scrolledWin) + + dialog = gtk.Dialog( + title, + parent, + gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT, + (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL), + ) + dialog.set_default_response(gtk.RESPONSE_CANCEL) + dialog.get_child().add(layout) + parentSize = parent.get_size() + dialog.resize(parentSize[0], max(parentSize[1]-100, 100)) + + if 0 < defaultIndex: + selection.select_path((defaultIndex, )) + else: + customEntry.set_text(defaultItem) + result = [] + customEntry.connect("activate", _on_null_touch_selector_entry_entry_activated, dialog, customEntry, result) + treeView.connect("row-activated", _on_null_touch_selector_entry_tree_activated, dialog, selection, result) + + try: + dialog.show_all() + response = dialog.run() + finally: + dialog.hide() + + if response == gtk.RESPONSE_OK: + model, itr = selection.get_selected() + if len(result) != 1: + raise RuntimeError("No selection made") + else: + return result[0] + elif response == gtk.RESPONSE_CANCEL or response == gtk.RESPONSE_DELETE_EVENT: + raise RuntimeError("User cancelled request") + else: + raise RuntimeError("Unrecognized response %r", response) + + +try: + hildon.PickerDialog + hildon.TouchSelectorEntry + touch_selector_entry = _hildon_touch_selector_entry +except AttributeError: + touch_selector_entry = _null_touch_selector_entry + + +if __name__ == "__main__": + app = get_app_class()() + + label = gtk.Label("Hello World from a Label!") + + win = gtk.Window() + win.add(label) + win = hildonize_window(app, win) + if False: + print touch_selector(win, "Test", ["A", "B", "C", "D"], 2) + if True: + print touch_selector_entry(win, "Test", ["A", "B", "C", "D"], "C") + print touch_selector_entry(win, "Test", ["A", "B", "C", "D"], "Blah") + if False: + import pprint + name, value = "", "" + goodLocals = [ + (name, value) for (name, value) in locals().iteritems() + if not name.startswith("_") + ] + pprint.pprint(goodLocals) + if False: + import time + show_information_banner(win, "Hello World") + time.sleep(5) + if False: + import time + banner = show_busy_banner_start(win, "Hello World") + time.sleep(5) + show_busy_banner_end(banner) diff --git a/src/led_handler.py b/src/led_handler.py new file mode 100755 index 0000000..211036e --- /dev/null +++ b/src/led_handler.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python + +import dbus + + +class LedHandler(object): + + def __init__(self): + self._bus = dbus.SystemBus() + self._rawMceRequest = self._bus.get_object("com.nokia.mce", "/com/nokia/mce/request") + self._mceRequest = dbus.Interface(self._rawMceRequest, dbus_interface="com.nokia.mce.request") + + self._ledPattern = "PatternCommunicationChat" + + def on(self): + self._mceRequest.req_led_pattern_activate(self._ledPattern) + + def off(self): + self._mceRequest.req_led_pattern_deactivate(self._ledPattern) + + +if __name__ == "__main__": + leds = LedHandler() + leds.off() diff --git a/src/null_backend.py b/src/null_backend.py new file mode 100644 index 0000000..c07f724 --- /dev/null +++ b/src/null_backend.py @@ -0,0 +1,134 @@ +#!/usr/bin/python + +""" +DialCentral - Front end for Google's Grand Central service. +Copyright (C) 2008 Eric Warnke ericew AT gmail DOT com + +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 +""" + + +class NullDialer(object): + + def __init__(self): + pass + + def is_authed(self, force = False): + return False + + def login(self, username, password): + return self.is_authed() + + def logout(self): + self.clear_caches() + + def dial(self, number): + return True + + def send_sms(self, number, message): + raise NotImplementedError("SMS Is Not Supported") + + def clear_caches(self): + pass + + def is_valid_syntax(self, number): + """ + @returns If This number be called ( syntax validation only ) + """ + return False + + def get_account_number(self): + """ + @returns The grand central phone number + """ + return "" + + def set_sane_callback(self): + pass + + def get_callback_numbers(self): + return {} + + def set_callback_number(self, callbacknumber): + return True + + def get_callback_number(self): + return "" + + def get_recent(self): + return () + + def get_addressbooks(self): + return () + + def open_addressbook(self, bookId): + return self + + @staticmethod + def contact_source_short_name(contactId): + return "ERROR" + + @staticmethod + def factory_name(): + return "ERROR" + + def get_contacts(self): + return () + + def get_contact_details(self, contactId): + return () + + def get_messages(self): + return () + + +class NullAddressBook(object): + """ + Minimal example of both an addressbook factory and an addressbook + """ + + def clear_caches(self): + pass + + def get_addressbooks(self): + """ + @returns Iterable of (Address Book Factory, Book Id, Book Name) + """ + yield self, "", "None" + + def open_addressbook(self, bookId): + return self + + @staticmethod + def contact_source_short_name(contactId): + return "" + + @staticmethod + def factory_name(): + return "" + + @staticmethod + def get_contacts(): + """ + @returns Iterable of (contact id, contact name) + """ + return [] + + @staticmethod + def get_contact_details(contactId): + """ + @returns Iterable of (Phone Type, Phone Number) + """ + return [] diff --git a/src/null_views.py b/src/null_views.py new file mode 100644 index 0000000..41d759a --- /dev/null +++ b/src/null_views.py @@ -0,0 +1,206 @@ +#!/usr/bin/python2.5 + +""" +DialCentral - Front end for Google's Grand Central service. +Copyright (C) 2008 Mark Bergman bergman AT merctech DOT com + +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 +""" + +import gobject +import gtk + + +class Dialpad(object): + + def __init__(self, widgetTree): + self._numberdisplay = widgetTree.get_widget("numberdisplay") + self._dialButton = widgetTree.get_widget("dial") + self._smsButton = widgetTree.get_widget("sms") + + def enable(self): + self._dialButton.set_sensitive(False) + self._smsButton.set_sensitive(False) + + def disable(self): + self._dialButton.set_sensitive(True) + self._smsButton.set_sensitive(True) + + @staticmethod + def name(): + return "Dialpad" + + def load_settings(self, config, sectionName): + pass + + def save_settings(self, config, sectionName): + """ + @note Thread Agnostic + """ + pass + + +class AccountInfo(object): + + def __init__(self, widgetTree): + self._callbackList = gtk.ListStore(gobject.TYPE_STRING) + self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display") + self._callbackSelectButton = widgetTree.get_widget("callbackSelectButton") + self._clearCookiesButton = widgetTree.get_widget("clearcookies") + + self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox") + self._minutesEntryButton = widgetTree.get_widget("minutesEntryButton") + self._missedCheckbox = widgetTree.get_widget("missedCheckbox") + self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox") + self._smsCheckbox = widgetTree.get_widget("smsCheckbox") + + def enable(self): + self._callbackSelectButton.set_sensitive(False) + self._clearCookiesButton.set_sensitive(False) + + self._notifyCheckbox.set_sensitive(False) + self._minutesEntryButton.set_sensitive(False) + self._missedCheckbox.set_sensitive(False) + self._voicemailCheckbox.set_sensitive(False) + self._smsCheckbox.set_sensitive(False) + + self._accountViewNumberDisplay.set_label("") + + def disable(self): + self._callbackSelectButton.set_sensitive(True) + self._clearCookiesButton.set_sensitive(True) + + self._notifyCheckbox.set_sensitive(True) + self._minutesEntryButton.set_sensitive(True) + self._missedCheckbox.set_sensitive(True) + self._voicemailCheckbox.set_sensitive(True) + self._smsCheckbox.set_sensitive(True) + + @staticmethod + def update(force = False): + return False + + @staticmethod + def clear(): + pass + + @staticmethod + def name(): + return "Account Info" + + def load_settings(self, config, sectionName): + pass + + def save_settings(self, config, sectionName): + """ + @note Thread Agnostic + """ + pass + + +class RecentCallsView(object): + + def __init__(self, widgetTree): + pass + + def enable(self): + pass + + def disable(self): + pass + + def update(self, force = False): + return False + + @staticmethod + def clear(): + pass + + @staticmethod + def name(): + return "Recent Calls" + + def load_settings(self, config, sectionName): + pass + + def save_settings(self, config, sectionName): + """ + @note Thread Agnostic + """ + pass + + +class MessagesView(object): + + def __init__(self, widgetTree): + pass + + def enable(self): + pass + + def disable(self): + pass + + def update(self, force = False): + return False + + @staticmethod + def clear(): + pass + + @staticmethod + def name(): + return "Messages" + + def load_settings(self, config, sectionName): + pass + + def save_settings(self, config, sectionName): + """ + @note Thread Agnostic + """ + pass + + +class ContactsView(object): + + def __init__(self, widgetTree): + self._bookSelectionButton = widgetTree.get_widget("addressbookSelectButton") + + def enable(self): + self._bookSelectionButton.set_sensitive(False) + + def disable(self): + self._bookSelectionButton.set_sensitive(True) + + def update(self, force = False): + return False + + @staticmethod + def clear(): + pass + + @staticmethod + def name(): + return "Contacts" + + def load_settings(self, config, sectionName): + pass + + def save_settings(self, config, sectionName): + """ + @note Thread Agnostic + """ + pass diff --git a/support/builddeb.py b/support/builddeb.py new file mode 100755 index 0000000..c910be1 --- /dev/null +++ b/support/builddeb.py @@ -0,0 +1,282 @@ +#!/usr/bin/python2.5 + +""" +@bug In update desrcription stuff +""" + +import os +import sys + +try: + import py2deb +except ImportError: + import fake_py2deb as py2deb + +import constants + + +__appname__ = constants.__app_name__ +__description__ = "Touch screen enhanced interface to the GoogleVoice/GrandCentral phone service" +__author__ = "Ed Page" +__email__ = "eopage@byu.net" +__version__ = constants.__version__ +__build__ = constants.__build__ +__changelog__ = """ +1.0.6 +* Fremantle Prep: Simplified menus in prep for no menu or the Fremantle App Menu +* Fremantle Prep: Implemented a work around for https://bugs.maemo.org/show_bug.cgi?id=4957 +* Fremantle Prep: Switched to touch selectors for notification time, callback number, and contact addressbook +* Fremantle Prep: Making various widgets pannable rather than scrollable +* Fremantle Prep: CTRL-V added for paste for Dialpad +* Fremantle Prep: CTRL-Enter added for fullscreen +* UI Tweak: Phone selection and SMS Message dialogs now highlight the last message and are easier to scroll +* UI Tweak: Tweaked sizes of stuff on recent tab +* UI Tweak: Added notifcations for various things like login and dialing +* UI Tweak: Switch to accounts tab when logging in and callback is blank as a sublte hint to configure it +* UI Tweak: Switch to accounts tab on failed login to remind the user they are not logged in +* Packaging: Disables notifications on uninstall +* Packaging: Including a vastly improved py2deb for better packages (icons on package, etc) +* Debugging: Adding seperator between dialcentral launches in log +* Bug Fix: Made startup more error resistant +* Bug Fix: some dependencies for Diablo +* Bug Fix: Error on refreshing tabs when not logged in +* Bug Fix: #4471 Notification Checkbox Won't Stay Checked (hour roll over error) +* Bug Fix: Phone numbers in voicemails wouldn't appear +* Bug Fix: category for Fremantle/Diablo +* Bug Fix: needing to manually create "~/.dialcentral" due to earlier logging changes +* Bug Fix: dependencies for fremantle +* Bug Fix: Issues when trying to stack error messages +* Bug Fix: Python2.6 deprecates some stuff I did +* Bug Fix: On refreshing the Accounts tab, the callback number resets to the number from startup + +1.0.5 +* Contacts Tab remembers the last address book viewed on restart +* Applied some suggested changes for being more thumb friendly +* Messaging Dialog auto-scrolls to bottom +* Removed GrandCentral support +* Numbers can now be entered immediately, before login +* Bug Fix: Not clearing the entered number on sending an SMS +* Bug Fix: Disabling SMS button when logged off +* Bug Fix: Trying to make SMS and phone selection dialogs more readable +* Bug Fix: Adding some more thumb scrollbars + +1.0.4 +* "Back" button and tabs now visually indicate when they've entered a "hold" state +* Fixed the duplicate title on Maemo +* Removing some device connection observer code due to high bug to low benefit ratio +* Notification support +* Fixed a bug from 1.0.3 where once you refreshed a tab by holding on it, every tab would then be forced to refresh + +1.0.3 +* Holding down a tab for a second will now force a refresh +* Fixed a bug dealing with overzealously refreshing the contacts tab +* Finding some undescriptive errors and made them more descriptive +* Swapped the order GrandCentral and GoogleVoice appear in login window +* Fixed the "Recent" and "Message" tabs, google changed things on me again + +1.0.2 +* Random bug fixes +* Random performance improvements + +1.0.1 +* Fixed a voicemail transcripts due to a GoogleVoice change + +1.0.0 +* Added names to the recent tab for GoogleVoice + +0.9.9 +* SMS From Dialpad +* Display of names for messages tab +* Condensed messages/recent's date column + +0.9.8 + * Added columns to recent view and messages view to help seperate messages + * Attempted refreshing session on dial/sms send + * Fixed a GC Bug + * Minor bug fixes as usual + +0.9.7 + * Switched to Force Refresh for when wanting to check for more messages + * Removed timeouts that forced refreshes on various tabs + * Added support for a settings file, fairly primitive right now + * Fixed Maemo Support + * Lots of major and minor bug fixes + +0.9.6 + * Experimenting with the tabs being on the side + * Now the phone selector is used always, even if there is just one phone number + * Added a Messages Tab, which displays SMS and Voicemail messages + * Added option to send SMS messages + +0.9.5 + * Fixed a login issue due to Google changing their webpage + +0.9.4 - "" + * Misc Bug fixes and experiments + +0.9.3 - "" + * Removed the much disliked contact source ID + * Added saving of callback number when using GoogleVoice + * Got proper formatting on things ("&" rather than "&") + * Misc Bug fixes + +0.9.2 - "Two heads are better than one" + * Adding of UI to switch between GC and GV + * Minimized flashing the dial button between grayed out and not on startup + * Bug fixes + +0.9.1 - "Get your hands off that" + * GoogleVoice Support, what a pain + * More flexible CSV support. It now checks the header row for what column name/number are in + * Experimenting with faster startup by caching PYC files with the package + * Fixing of some bad error handling + * More debug output for when people run into issues + +0.9.0 - "Slick as snot" + * Caching of contacts + * Refactoring to make working with the code easier + * Filesystem backed contacts but currently only supporting a specific csv format + * Gracefully handle lack of connection and connection transitions + * Gracefully handle failed login + * A tiny bit better error reporting + +0.8.3 - "Extras Love" + * Version bump fighting the extras autobuilder, I hope this works + +0.8.2 - "Feed is for horses, so what about feedback?" + * Merged addressbook + * many more smaller fixes + +0.8.1 - "Two Beers" + * Thumb scrollbars ( Kudos Khertan ) + +0.8.0 - "Spit and polish" + * Addressbook support + * threaded networking for better interactivity + * Hold down back to clear number + * Standard about dialog + * many more smaller fixes +""" + + +__postinstall__ = """#!/bin/sh -e + +gtk-update-icon-cache -f /usr/share/icons/hicolor +""" + +__preremove__ = """#!/bin/sh -e + +python /usr/lib/dialcentral/alarm_handler.py -d || true +""" + + +def find_files(path): + for root, dirs, files in os.walk(path): + for file in files: + if file.startswith("src-"): + fileParts = file.split("-") + unused, relPathParts, newName = fileParts[0], fileParts[1:-1], fileParts[-1] + assert unused == "src" + relPath = os.sep.join(relPathParts) + yield relPath, file, newName + + +def unflatten_files(files): + d = {} + for relPath, oldName, newName in files: + if relPath not in d: + d[relPath] = [] + d[relPath].append((oldName, newName)) + return d + + +def build_package(distribution): + try: + os.chdir(os.path.dirname(sys.argv[0])) + except: + pass + + py2deb.Py2deb.SECTIONS = py2deb.SECTIONS_BY_POLICY[distribution] + p = py2deb.Py2deb(__appname__) + p.description = __description__ + p.upgradeDescription = __changelog__.split("\n\n", 1)[0] + p.author = __author__ + p.mail = __email__ + p.license = "lgpl" + p.depends = ", ".join([ + "python2.6 | python2.5", + "python-gtk2 | python2.5-gtk2", + "python-xml | python2.5-xml", + "python-dbus | python2.5-dbus", + ]) + maemoSpecificDepends = ", python-osso | python2.5-osso, python-hildon | python2.5-hildon" + p.depends += { + "debian": ", python-glade2", + "chinook": maemoSpecificDepends, + "diablo": maemoSpecificDepends, + "fremantle": maemoSpecificDepends + ", python-glade2", + "mer": maemoSpecificDepends + ", python-glade2", + }[distribution] + p.recommends = ", ".join([ + ]) + p.section = { + "debian": "comm", + "chinook": "communication", + "diablo": "user/network", + "fremantle": "user/network", + "mer": "user/network", + }[distribution] + p.arch = "all" + p.urgency = "low" + p.distribution = "chinook diablo fremantle mer debian" + p.repository = "extras" + p.changelog = __changelog__ + p.postinstall = __postinstall__ + p.preremove = __preremove__ + p.icon = { + "debian": "26x26-dialcentral.png", + "chinook": "26x26-dialcentral.png", + "diablo": "26x26-dialcentral.png", + "fremantle": "64x64-dialcentral.png", # Fremantle natively uses 48x48 + "mer": "64x64-dialcentral.png", + }[distribution] + p["/usr/bin"] = [ "dialcentral.py" ] + for relPath, files in unflatten_files(find_files(".")).iteritems(): + fullPath = "/usr/lib/dialcentral" + if relPath: + fullPath += os.sep+relPath + p[fullPath] = list( + "|".join((oldName, newName)) + for (oldName, newName) in files + ) + p["/usr/share/applications/hildon"] = ["dialcentral.desktop"] + p["/usr/share/icons/hicolor/26x26/hildon"] = ["26x26-dialcentral.png|dialcentral.png"] + p["/usr/share/icons/hicolor/64x64/hildon"] = ["64x64-dialcentral.png|dialcentral.png"] + p["/usr/share/icons/hicolor/scalable/hildon"] = ["scale-dialcentral.png|dialcentral.png"] + + print p + print p.generate( + version="%s-%s" % (__version__, __build__), + changelog=__changelog__, + build=False, + tar=True, + changes=True, + dsc=True, + ) + print "Building for %s finished" % distribution + + +if __name__ == "__main__": + if len(sys.argv) > 1: + try: + import optparse + except ImportError: + optparse = None + + if optparse is not None: + parser = optparse.OptionParser() + (commandOptions, commandArgs) = parser.parse_args() + else: + commandArgs = None + commandArgs = ["diablo"] + build_package(commandArgs[0]) diff --git a/support/dialcentral.desktop b/support/dialcentral.desktop new file mode 100644 index 0000000..ce28c88 --- /dev/null +++ b/support/dialcentral.desktop @@ -0,0 +1,7 @@ +[Desktop Entry] +Encoding=UTF-8 +Version=1.0 +Type=Application +Name=DialCentral +Exec=/usr/bin/run-standalone.sh /usr/bin/dialcentral.py +Icon=dialcentral diff --git a/support/fake_py2deb.py b/support/fake_py2deb.py new file mode 100644 index 0000000..5d6149d --- /dev/null +++ b/support/fake_py2deb.py @@ -0,0 +1,56 @@ +import pprint + + +class Py2deb(object): + + def __init__(self, appName): + self._appName = appName + self.description = "" + self.author = "" + self.mail = "" + self.license = "" + self.depends = "" + self.section = "" + self.arch = "" + self.ugency = "" + self.distribution = "" + self.repository = "" + self.changelog = "" + self.postinstall = "" + self.icon = "" + self._install = {} + + def generate(self, appVersion, appBuild, changelog, tar, dsc, changes, build, src): + return """ +Package: %s +version: %s-%s +Changes: +%s + +Build Options: + Tar: %s + Dsc: %s + Changes: %s + Build: %s + Src: %s + """ % ( + self._appName, appVersion, appBuild, changelog, tar, dsc, changes, build, src + ) + + def __str__(self): + parts = [] + parts.append("%s Package Settings:" % (self._appName, )) + for settingName in dir(self): + if settingName.startswith("_"): + continue + parts.append("\t%s: %s" % (settingName, getattr(self, settingName))) + + parts.append(pprint.pformat(self._install)) + + return "\n".join(parts) + + def __getitem__(self, key): + return self._install[key] + + def __setitem__(self, key, item): + self._install[key] = item diff --git a/support/icons/hicolor/26x26/hildon/dialcentral.png b/support/icons/hicolor/26x26/hildon/dialcentral.png new file mode 100644 index 0000000000000000000000000000000000000000..df50c66807a4ac5a11657771901db51b56e2d2c6 GIT binary patch literal 1671 zcmV;226*|2P)Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01ejw01ejxLMWSf00007bV*G`2iOK2 z4=f7;iQIkw00sw1L_t(Y$EB5hOk39(#(($v+P*dcyI^C8Nj%6$p;`i>sX$$pc7bjJ zG?0L-iNX?ziH=B1rfHfckt{@!l0UXcQ=(`}qNtsS@}aV}sEUXpmu|2qDM^{MFJ(<# zzy=x^5DZ+dudlD~-5)Df)}~9RJ^!2|o%25LqxXHzdjvki<;$0?{{DVdlB7TT{r=v~ zn>R0On&wzNR}=-)G|_b(!!R&S6Gc(bb)7^afv)Qaf>0a?1YSLT`t*7L0bp)!?s#5a z-Y-^(PM$nTS63HLgQ6%zA`v2y2on<%BoYZKDk^AbXb>{wmStIw2X%FIQBqPuZ*MQ@ zbQ(bru-okjf`H9t%d9q=4M~!?diCmmBt3roxZd5}?UbIRXlZE)S(d4*s{_aot?nHT z2a+UVv)Q=)!3WI6<2?H3BD$_qy1tmBM~^zjJ{hZeB6%W_VB^M(w6(Q8wKQ(OpS0IY zfy0Yzfb?0Ho-KH`K?2Zq9f!k#VHoJTPBNLKy1JUFsVQb>XOSccQ4|qH5lz!F&2?sF z;r9E9#Z6u)ucEjMewBNIx4duQ`PdT?0mjG2Gv&}Ujg^%Z0D{3FB_$;k6%}EcCfBZA zBQGxxpU;QO<-%n*S-&=mg4Uf78(}>8A-m?k!}FyTq>Pj+qZ}ZaOrokPsZDcGQ8IhxXHL;3SN_66Qxy48HT~~@-l{DuxcJr z6p4ESXfvfnM|T-8iF7&FfhP{3m2H4o@Q`xkexesQdn3>b8|C0cI@Eb!5>jt zD6#Jc2Pu8|+idt+A?42nxO?UX+iSkX>zChNCzY3%+m@D=SYBR6QIyQ1si~>q)~#EZ zrU^hG5TL24iTn5OqiGs#-`|Z#8|T)9os(}%WO+?m>)~fxHj|a@!Z3`aB#L6tvMf|p zC7n)Z5&%RZ5ze1K55Tcw$B0IwbaZt5d#=pK2*26SyHPK5??jms99(+PK;5nfeEPsd z)3mjs)9JLSs+v)rOeWEFo&NrQ_Uze1XJ;p>s&eAQ2?`1dICA6&_wL=})0s!SQu`t$ z*^dx%mXZ8An{vm=T9b{IR8bVgA^Cj1(CFwWs;aWGvO+SM#4rr%>+2aA8DaPC-2m+0 zzaO{TO?P)U#Vw88&R#Kwn=UilT7p)G0Jg2!LqtgMV^G>WEa#N%;NsT79}9pcKBE9B(lke{DVRaF%;GczA^B|8XM}7<$T;OH#r^;Nm1Zx?qto{6GjmP77yFate%>h`bjhz-FoNFoxV5HlA@AtupscLyE8qV@SuD%?HSi4ZF)$Cb0pIwN z_@4qM;0MZq9}9x;$(Lrco`A*0MQd?!(Tc@l)>H7t<6K|(ps!!Qe)sIzv)))N_QzBz z^>Ev^ZKV!}qg9q=(&;p^ER#y5n4h1Igu~&!O`A4h7)B@*3bnSkx1V~P>t9a#+JnYl RY6buR002ovPDHLkV1gVhDY*au literal 0 HcmV?d00001 diff --git a/support/icons/hicolor/64x64/hildon/dialcentral.png b/support/icons/hicolor/64x64/hildon/dialcentral.png new file mode 100644 index 0000000000000000000000000000000000000000..8d98390d0478e4bbef85641af67982cdd29ab00c GIT binary patch literal 6411 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 LUFGt}R$>1G8dR*- literal 0 HcmV?d00001 diff --git a/support/icons/hicolor/scalable/hildon/dialcentral.png b/support/icons/hicolor/scalable/hildon/dialcentral.png new file mode 100644 index 0000000000000000000000000000000000000000..a8753506901fcff1ea8d827774b21ac938bf3026 GIT binary patch literal 32182 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 OilMHV&O>ddsQ&|#ca-%2 literal 0 HcmV?d00001 diff --git a/support/py2deb.py b/support/py2deb.py new file mode 100644 index 0000000..dceea15 --- /dev/null +++ b/support/py2deb.py @@ -0,0 +1,955 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +## +## Copyright (C) 2009 manatlan manatlan[at]gmail(dot)com +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation; version 2 only. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +""" +Known limitations : +- don't sign package (-us -uc) +- no distinctions between author and maintainer(packager) + +depends on : +- dpkg-dev (dpkg-buildpackage) +- alien +- python +- fakeroot + +changelog + - ??? 9/14/2009 (By epage) + - PEP8 + - added recommends + - fixed bug where it couldn't handle the contents of the pre/post scripts being specified + - Added customization based on the targeted policy for sections (Maemo support) + - Added maemo specific tarball, dsc, changes file generation support (including icon support) + - 0.5 05/09/2009 + - pre/post install/remove scripts enabled + - deb package install py2deb in dist-packages for py2.6 + - 0.4 14/10/2008 + - use os.environ USERNAME or USER (debian way) + - install on py 2.(4,5,6) (*FIX* do better here) + +""" + +import os +import hashlib +import sys +import shutil +import time +import string +import StringIO +import stat +import commands +import base64 +from tarfile import TarFile +from glob import glob +from datetime import datetime +import socket # gethostname() +from subprocess import Popen, PIPE + +#~ __version__ = "0.4" +__version__ = "0.5" +__author__ = "manatlan" +__mail__ = "manatlan@gmail.com" + + +PERMS_URW_GRW_OR = stat.S_IRUSR | stat.S_IWUSR | \ + stat.S_IRGRP | stat.S_IWGRP | \ + stat.S_IROTH + +UID_ROOT = 0 +GID_ROOT = 0 + + +def run(cmds): + p = Popen(cmds, shell=False, stdout=PIPE, stderr=PIPE) + time.sleep(0.01) # to avoid "IOError: [Errno 4] Interrupted system call" + out = string.join(p.stdout.readlines()).strip() + outerr = string.join(p.stderr.readlines()).strip() + return out + + +def deb2rpm(file): + txt=run(['alien', '-r', file]) + return txt.split(" generated")[0] + + +def py2src(TEMP, name): + l=glob("%(TEMP)s/%(name)s*.tar.gz"%locals()) + if len(l) != 1: + raise Py2debException("don't find source package tar.gz") + + tar = os.path.basename(l[0]) + shutil.move(l[0], tar) + + return tar + + +def md5sum(filename): + f = open(filename, "r") + try: + return hashlib.md5(f.read()).hexdigest() + finally: + f.close() + + +class Py2changes(object): + + def __init__(self, ChangedBy, description, changes, files, category, repository, **kwargs): + self.options = kwargs # TODO: Is order important? + self.description = description + self.changes=changes + self.files=files + self.category=category + self.repository=repository + self.ChangedBy=ChangedBy + + def getContent(self): + content = ["%s: %s" % (k, v) + for k,v in self.options.iteritems()] + + if self.description: + description=self.description.replace("\n","\n ") + content.append('Description: ') + content.append(' %s' % description) + if self.changes: + changes=self.changes.replace("\n","\n ") + content.append('Changes: ') + content.append(' %s' % changes) + if self.ChangedBy: + content.append("Changed-By: %s" % self.ChangedBy) + + content.append('Files:') + + for onefile in self.files: + md5 = md5sum(onefile) + size = os.stat(onefile).st_size.__str__() + content.append(' ' + md5 + ' ' + size + ' ' + self.category +' '+self.repository+' '+os.path.basename(onefile)) + + return "\n".join(content) + "\n\n" + + +def py2changes(params): + changescontent = Py2changes( + "%(author)s <%(mail)s>" % params, + "%(description)s" % params, + "%(changelog)s" % params, + ( + "%(TEMP)s/%(name)s_%(version)s.tar.gz" % params, + "%(TEMP)s/%(name)s_%(version)s.dsc" % params, + ), + "%(section)s" % params, + "", #"%(repository)s" % params, + Format='1.7', + Date=time.strftime("%a, %d %b %Y %H:%M:%S +0000", time.gmtime()), + Source="%(name)s" % params, + Architecture="%(arch)s" % params, + Version="%(version)s" % params, + Distribution="", #"%(distribution)s" % params, + Urgency="", #"%(urgency)s" % params, + Maintainer="%(author)s <%(mail)s>" % params + ) + f = open("%(TEMP)s/%(name)s_%(version)s.changes" % params,"wb") + f.write(changescontent.getContent()) + f.close() + + fileHandle = open('/tmp/py2deb2.tmp', 'w') + fileHandle.write('#!/bin/sh\n') + fileHandle.write("cd " +os.getcwd()+ "\n") + fileHandle.write("gpg --local-user %(mail)s --clearsign %(TEMP)s/%(name)s_%(version)s.changes\n" % params) + fileHandle.write("mv %(TEMP)s/%(name)s_%(version)s.changes.asc %(TEMP)s/%(name)s_%(version)s.changes\n" % params) + fileHandle.write('\nexit') + fileHandle.close() + commands.getoutput("chmod 777 /tmp/py2deb2.tmp") + commands.getoutput("/tmp/py2deb2.tmp") + + ret = [] + + l=glob("%(TEMP)s/%(name)s*.tar.gz" % params) + if len(l)!=1: + raise Py2debException("don't find source package tar.gz") + tar = os.path.basename(l[0]) + shutil.move(l[0],tar) + ret.append(tar) + + l=glob("%(TEMP)s/%(name)s*.dsc" % params) + if len(l)!=1: + raise Py2debException("don't find source package dsc") + tar = os.path.basename(l[0]) + shutil.move(l[0],tar) + ret.append(tar) + + l=glob("%(TEMP)s/%(name)s*.changes" % params) + if len(l)!=1: + raise Py2debException("don't find source package changes") + tar = os.path.basename(l[0]) + shutil.move(l[0],tar) + ret.append(tar) + + return ret + + +class Py2dsc(object): + + def __init__(self, StandardsVersion, BuildDepends, files, **kwargs): + self.options = kwargs # TODO: Is order important? + self.StandardsVersion = StandardsVersion + self.BuildDepends=BuildDepends + self.files=files + + @property + def content(self): + content = ["%s: %s" % (k, v) + for k,v in self.options.iteritems()] + + if self.BuildDepends: + content.append("Build-Depends: %s" % self.BuildDepends) + if self.StandardsVersion: + content.append("Standards-Version: %s" % self.StandardsVersion) + + content.append('Files:') + + for onefile in self.files: + print onefile + md5 = md5sum(onefile) + size = os.stat(onefile).st_size.__str__() + content.append(' '+md5 + ' ' + size +' '+os.path.basename(onefile)) + + return "\n".join(content)+"\n\n" + + +def py2dsc(TEMP, name, version, depends, author, mail, arch): + dsccontent = Py2dsc("%(version)s" % locals(), + "%(depends)s" % locals(), + ("%(TEMP)s/%(name)s_%(version)s.tar.gz" % locals(),), + Format='1.0', + Source="%(name)s" % locals(), + Version="%(version)s" % locals(), + Maintainer="%(author)s <%(mail)s>" % locals(), + Architecture="%(arch)s" % locals(), + ) + + filename = "%(TEMP)s/%(name)s_%(version)s.dsc" % locals() + + f = open(filename, "wb") + try: + f.write(dsccontent.content) + finally: + f.close() + + fileHandle = open('/tmp/py2deb.tmp', 'w') + try: + fileHandle.write('#!/bin/sh\n') + fileHandle.write("cd " + os.getcwd() + "\n") + fileHandle.write("gpg --local-user %(mail)s --clearsign %(TEMP)s/%(name)s_%(version)s.dsc\n" % locals()) + fileHandle.write("mv %(TEMP)s/%(name)s_%(version)s.dsc.asc %(filename)s\n" % locals()) + fileHandle.write('\nexit') + fileHandle.close() + finally: + f.close() + + commands.getoutput("chmod 777 /tmp/py2deb.tmp") + commands.getoutput("/tmp/py2deb.tmp") + + return filename + + +class Py2tar(object): + + def __init__(self, dataDirectoryPath): + self._dataDirectoryPath = dataDirectoryPath + + def packed(self): + return self._getSourcesFiles() + + def _getSourcesFiles(self): + directoryPath = self._dataDirectoryPath + + outputFileObj = StringIO.StringIO() # TODO: Do more transparently? + + tarOutput = TarFile.open('sources', + mode = "w:gz", + fileobj = outputFileObj) + + # Note: We can't use this because we need to fiddle permissions: + # tarOutput.add(directoryPath, arcname = "") + + for root, dirs, files in os.walk(directoryPath): + archiveRoot = root[len(directoryPath):] + + tarinfo = tarOutput.gettarinfo(root, archiveRoot) + # TODO: Make configurable? + tarinfo.uid = UID_ROOT + tarinfo.gid = GID_ROOT + tarinfo.uname = "" + tarinfo.gname = "" + tarOutput.addfile(tarinfo) + + for f in files: + tarinfo = tarOutput.gettarinfo(os.path.join(root, f), + os.path.join(archiveRoot, f)) + tarinfo.uid = UID_ROOT + tarinfo.gid = GID_ROOT + tarinfo.uname = "" + tarinfo.gname = "" + tarOutput.addfile(tarinfo, file(os.path.join(root, f))) + + tarOutput.close() + + data_tar_gz = outputFileObj.getvalue() + + return data_tar_gz + + +def py2tar(DEST, TEMP, name, version): + tarcontent = Py2tar("%(DEST)s" % locals()) + filename = "%(TEMP)s/%(name)s_%(version)s.tar.gz" % locals() + f = open(filename, "wb") + try: + f.write(tarcontent.packed()) + finally: + f.close() + return filename + + +class Py2debException(Exception): + pass + + +SECTIONS_BY_POLICY = { + # http://www.debian.org/doc/debian-policy/ch-archive.html#s-subsections + "debian": "admin, base, comm, contrib, devel, doc, editors, electronics, embedded, games, gnome, graphics, hamradio, interpreters, kde, libs, libdevel, mail, math, misc, net, news, non-free, oldlibs, otherosfs, perl, python, science, shells, sound, tex, text, utils, web, x11", + # http://maemo.org/forrest-images/pdf/maemo-policy.pdf + "chinook": "accessories, communication, games, multimedia, office, other, programming, support, themes, tools", + # http://wiki.maemo.org/Task:Package_categories + "diablo": "user/desktop, user/development, user/education, user/games, user/graphics, user/multimedia, user/navigation, user/network, user/office, user/science, user/system, user/utilities", + # http://wiki.maemo.org/Task:Fremantle_application_categories + "mer": "user/desktop, user/development, user/education, user/games, user/graphics, user/multimedia, user/navigation, user/network, user/office, user/science, user/system, user/utilities", + # http://wiki.maemo.org/Task:Fremantle_application_categories + "fremantle": "user/desktop, user/development, user/education, user/games, user/graphics, user/multimedia, user/navigation, user/network, user/office, user/science, user/system, user/utilities", +} + + +class Py2deb(object): + """ + heavily based on technic described here : + http://wiki.showmedo.com/index.php?title=LinuxJensMakingDeb + """ + ## STATICS + clear=False # clear build folder after py2debianization + + SECTIONS = SECTIONS_BY_POLICY["debian"] + + #http://www.debian.org/doc/debian-policy/footnotes.html#f69 + ARCHS="all i386 ia64 alpha amd64 armeb arm hppa m32r m68k mips mipsel powerpc ppc64 s390 s390x sh3 sh3eb sh4 sh4eb sparc darwin-i386 darwin-ia64 darwin-alpha darwin-amd64 darwin-armeb darwin-arm darwin-hppa darwin-m32r darwin-m68k darwin-mips darwin-mipsel darwin-powerpc darwin-ppc64 darwin-s390 darwin-s390x darwin-sh3 darwin-sh3eb darwin-sh4 darwin-sh4eb darwin-sparc freebsd-i386 freebsd-ia64 freebsd-alpha freebsd-amd64 freebsd-armeb freebsd-arm freebsd-hppa freebsd-m32r freebsd-m68k freebsd-mips freebsd-mipsel freebsd-powerpc freebsd-ppc64 freebsd-s390 freebsd-s390x freebsd-sh3 freebsd-sh3eb freebsd-sh4 freebsd-sh4eb freebsd-sparc kfreebsd-i386 kfreebsd-ia64 kfreebsd-alpha kfreebsd-amd64 kfreebsd-armeb kfreebsd-arm kfreebsd-hppa kfreebsd-m32r kfreebsd-m68k kfreebsd-mips kfreebsd-mipsel kfreebsd-powerpc kfreebsd-ppc64 kfreebsd-s390 kfreebsd-s390x kfreebsd-sh3 kfreebsd-sh3eb kfreebsd-sh4 kfreebsd-sh4eb kfreebsd-sparc knetbsd-i386 knetbsd-ia64 knetbsd-alpha knetbsd-amd64 knetbsd-armeb knetbsd-arm knetbsd-hppa knetbsd-m32r knetbsd-m68k knetbsd-mips knetbsd-mipsel knetbsd-powerpc knetbsd-ppc64 knetbsd-s390 knetbsd-s390x knetbsd-sh3 knetbsd-sh3eb knetbsd-sh4 knetbsd-sh4eb knetbsd-sparc netbsd-i386 netbsd-ia64 netbsd-alpha netbsd-amd64 netbsd-armeb netbsd-arm netbsd-hppa netbsd-m32r netbsd-m68k netbsd-mips netbsd-mipsel netbsd-powerpc netbsd-ppc64 netbsd-s390 netbsd-s390x netbsd-sh3 netbsd-sh3eb netbsd-sh4 netbsd-sh4eb netbsd-sparc openbsd-i386 openbsd-ia64 openbsd-alpha openbsd-amd64 openbsd-armeb openbsd-arm openbsd-hppa openbsd-m32r openbsd-m68k openbsd-mips openbsd-mipsel openbsd-powerpc openbsd-ppc64 openbsd-s390 openbsd-s390x openbsd-sh3 openbsd-sh3eb openbsd-sh4 openbsd-sh4eb openbsd-sparc hurd-i386 hurd-ia64 hurd-alpha hurd-amd64 hurd-armeb hurd-arm hurd-hppa hurd-m32r hurd-m68k hurd-mips hurd-mipsel hurd-powerpc hurd-ppc64 hurd-s390 hurd-s390x hurd-sh3 hurd-sh3eb hurd-sh4 hurd-sh4eb hurd-sparc".split(" ") + + # license terms taken from dh_make + LICENSES=["gpl", "lgpl", "bsd", "artistic"] + + def __setitem__(self, path, files): + + if not type(files)==list: + raise Py2debException("value of key path '%s' is not a list"%path) + if not files: + raise Py2debException("value of key path '%s' should'nt be empty"%path) + if not path.startswith("/"): + raise Py2debException("key path '%s' malformed (don't start with '/')"%path) + if path.endswith("/"): + raise Py2debException("key path '%s' malformed (shouldn't ends with '/')"%path) + + nfiles=[] + for file in files: + + if ".." in file: + raise Py2debException("file '%s' contains '..', please avoid that!"%file) + + + if "|" in file: + if file.count("|")!=1: + raise Py2debException("file '%s' is incorrect (more than one pipe)"%file) + + file, nfile = file.split("|") + else: + nfile=file # same localisation + + if os.path.isdir(file): + raise Py2debException("file '%s' is a folder, and py2deb refuse folders !"%file) + + if not os.path.isfile(file): + raise Py2debException("file '%s' doesn't exist"%file) + + if file.startswith("/"): # if an absolute file is defined + if file==nfile: # and not renamed (pipe trick) + nfile=os.path.basename(file) # it's simply copied to 'path' + + nfiles.append((file, nfile)) + + nfiles.sort(lambda a, b: cmp(a[1], b[1])) #sort according new name (nfile) + + self.__files[path]=nfiles + + def __delitem__(self, k): + del self.__files[k] + + def __init__(self, + name, + description="no description", + license="gpl", + depends="", + section="utils", + arch="all", + + url="", + author = None, + mail = None, + + preinstall = None, + postinstall = None, + preremove = None, + postremove = None + ): + + if author is None: + author = ("USERNAME" in os.environ) and os.environ["USERNAME"] or None + if author is None: + author = ("USER" in os.environ) and os.environ["USER"] or "unknown" + + if mail is None: + mail = author+"@"+socket.gethostname() + + self.name = name + self.description = description + self.upgradeDescription = "" + self.license = license + self.depends = depends + self.recommends = "" + self.section = section + self.arch = arch + self.url = url + self.author = author + self.mail = mail + self.icon = "" + + self.preinstall = preinstall + self.postinstall = postinstall + self.preremove = preremove + self.postremove = postremove + + self.__files={} + + def __repr__(self): + name = self.name + license = self.license + description = self.description + depends = self.depends + recommends = self.recommends + section = self.section + arch = self.arch + url = self.url + author = self.author + mail = self.mail + + preinstall = self.preinstall + postinstall = self.postinstall + preremove = self.preremove + postremove = self.postremove + + paths=self.__files.keys() + paths.sort() + files=[] + for path in paths: + for file, nfile in self.__files[path]: + #~ rfile=os.path.normpath(os.path.join(path, nfile)) + rfile=os.path.join(path, nfile) + if nfile==file: + files.append(rfile) + else: + files.append(rfile + " (%s)"%file) + + files.sort() + files = "\n".join(files) + + + lscripts = [ preinstall and "preinst", + postinstall and "postinst", + preremove and "prerm", + postremove and "postrm", + ] + scripts = lscripts and ", ".join([i for i in lscripts if i]) or "None" + return """ +---------------------------------------------------------------------- +NAME : %(name)s +---------------------------------------------------------------------- +LICENSE : %(license)s +URL : %(url)s +AUTHOR : %(author)s +MAIL : %(mail)s +---------------------------------------------------------------------- +DEPENDS : %(depends)s +RECOMMENDS : %(recommends)s +ARCH : %(arch)s +SECTION : %(section)s +---------------------------------------------------------------------- +DESCRIPTION : +%(description)s +---------------------------------------------------------------------- +SCRIPTS : %(scripts)s +---------------------------------------------------------------------- +FILES : +%(files)s +""" % locals() + + def generate(self, version, changelog="", rpm=False, src=False, build=True, tar=False, changes=False, dsc=False): + """ generate a deb of version 'version', with or without 'changelog', with or without a rpm + (in the current folder) + return a list of generated files + """ + if not sum([len(i) for i in self.__files.values()])>0: + raise Py2debException("no files are defined") + + if not changelog: + changelog="* no changelog" + + name = self.name + description = self.description + license = self.license + depends = self.depends + recommends = self.recommends + section = self.section + arch = self.arch + url = self.url + author = self.author + mail = self.mail + files = self.__files + preinstall = self.preinstall + postinstall = self.postinstall + preremove = self.preremove + postremove = self.postremove + + if section not in Py2deb.SECTIONS: + raise Py2debException("section '%s' is unknown (%s)" % (section, str(Py2deb.SECTIONS))) + + if arch not in Py2deb.ARCHS: + raise Py2debException("arch '%s' is unknown (%s)"% (arch, str(Py2deb.ARCHS))) + + if license not in Py2deb.LICENSES: + raise Py2debException("License '%s' is unknown (%s)" % (license, str(Py2deb.LICENSES))) + + # create dates (buildDate, buildDateYear) + d=datetime.now() + buildDate=d.strftime("%a, %d %b %Y %H:%M:%S +0000") + buildDateYear=str(d.year) + + + #clean description (add a space before each next lines) + description=description.replace("\r", "").strip() + description = "\n ".join(description.split("\n")) + + #clean changelog (add 2 spaces before each next lines) + changelog=changelog.replace("\r", "").strip() + changelog = "\n ".join(changelog.split("\n")) + + + TEMP = ".py2deb_build_folder" + DEST = os.path.join(TEMP, name) + DEBIAN = os.path.join(DEST, "debian") + + # let's start the process + try: + shutil.rmtree(TEMP) + except: + pass + + os.makedirs(DEBIAN) + try: + rules=[] + dirs=[] + for path in files: + for ofile, nfile in files[path]: + if os.path.isfile(ofile): + # it's a file + + if ofile.startswith("/"): # if absolute path + # we need to change dest + dest=os.path.join(DEST, nfile) + else: + dest=os.path.join(DEST, ofile) + + # copy file to be packaged + destDir = os.path.dirname(dest) + if not os.path.isdir(destDir): + os.makedirs(destDir) + + shutil.copy2(ofile, dest) + + ndir = os.path.join(path, os.path.dirname(nfile)) + nname = os.path.basename(nfile) + + # make a line RULES to be sure the destination folder is created + # and one for copying the file + fpath = "/".join(["$(CURDIR)", "debian", name+ndir]) + rules.append('mkdir -p "%s"' % fpath) + rules.append('cp -a "%s" "%s"' % (ofile, os.path.join(fpath, nname))) + + # append a dir + dirs.append(ndir) + + else: + raise Py2debException("unknown file '' "%ofile) # shouldn't be raised (because controlled before) + + # make rules right + rules= "\n\t".join(rules) + "\n" + + # make dirs right + dirs= [i[1:] for i in set(dirs)] + dirs.sort() + + #========================================================================== + # CREATE debian/dirs + #========================================================================== + open(os.path.join(DEBIAN, "dirs"), "w").write("\n".join(dirs)) + + #========================================================================== + # CREATE debian/changelog + #========================================================================== + clog="""%(name)s (%(version)s) stable; urgency=low + + %(changelog)s + + -- %(author)s <%(mail)s> %(buildDate)s +""" % locals() + + open(os.path.join(DEBIAN, "changelog"), "w").write(clog) + + #========================================================================== + #Create pre/post install/remove + #========================================================================== + def mkscript(name, dest): + if name and name.strip()!="": + if os.path.isfile(name): # it's a file + content = file(name).read() + else: # it's a script + content = name + open(os.path.join(DEBIAN, dest), "w").write(content) + + mkscript(preinstall, "preinst") + mkscript(postinstall, "postinst") + mkscript(preremove, "prerm") + mkscript(postremove, "postrm") + + + #========================================================================== + # CREATE debian/compat + #========================================================================== + open(os.path.join(DEBIAN, "compat"), "w").write("5\n") + + #========================================================================== + # CREATE debian/control + #========================================================================== + txt="""Source: %(name)s +Section: %(section)s +Priority: extra +Maintainer: %(author)s <%(mail)s> +Build-Depends: debhelper (>= 5) +Standards-Version: 3.7.2 + +Package: %(name)s +Architecture: %(arch)s +Depends: %(depends)s +Recommends: %(recommends)s +Description: %(description)s""" % locals() + if self.upgradeDescription: + upgradeDescription = "XB-Maemo-Upgrade-Description: %s" % self.upgradeDescription.strip() + txt = "%s\n%s" % (txt, "\n ".join(upgradeDescription.split("\n"))) + if self.icon: + f = open(self.icon, "rb") + try: + rawIcon = f.read() + finally: + f.close() + uueIcon = base64.b64encode(rawIcon) + uueIconLines = [] + for i, c in enumerate(uueIcon): + if i % 60 == 0: + uueIconLines.append("") + uueIconLines[-1] += c + uueIconLines[0:0] = ("XB-Maemo-Icon-26:", ) + txt = "\n".join((txt, "\n ".join(uueIconLines), "")) + open(os.path.join(DEBIAN, "control"), "w").write(txt) + + #========================================================================== + # CREATE debian/copyright + #========================================================================== + copy={} + copy["gpl"]=""" + This package is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This package is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this package; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +On Debian systems, the complete text of the GNU General +Public License can be found in `/usr/share/common-licenses/GPL'. +""" + copy["lgpl"]=""" + This package is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This package is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this package; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +On Debian systems, the complete text of the GNU Lesser General +Public License can be found in `/usr/share/common-licenses/LGPL'. +""" + copy["bsd"]=""" + Redistribution and use in source and binary forms, with or without + modification, are permitted under the terms of the BSD License. + + THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + SUCH DAMAGE. + +On Debian systems, the complete text of the BSD License can be +found in `/usr/share/common-licenses/BSD'. +""" + copy["artistic"]=""" + This program is free software; you can redistribute it and/or modify it + under the terms of the "Artistic License" which comes with Debian. + + THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED + WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES + OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE. + +On Debian systems, the complete text of the Artistic License +can be found in `/usr/share/common-licenses/Artistic'. +""" + + txtLicense = copy[license] + pv=__version__ + txt="""This package was py2debianized(%(pv)s) by %(author)s <%(mail)s> on +%(buildDate)s. + +It was downloaded from %(url)s + +Upstream Author: %(author)s <%(mail)s> + +Copyright: %(buildDateYear)s by %(author)s + +License: + +%(txtLicense)s + +The Debian packaging is (C) %(buildDateYear)s, %(author)s <%(mail)s> and +is licensed under the GPL, see above. + + +# Please also look if there are files or directories which have a +# different copyright/license attached and list them here. +""" % locals() + open(os.path.join(DEBIAN, "copyright"), "w").write(txt) + + #========================================================================== + # CREATE debian/rules + #========================================================================== + txt="""#!/usr/bin/make -f +# -*- makefile -*- +# Sample debian/rules that uses debhelper. +# This file was originally written by Joey Hess and Craig Small. +# As a special exception, when this file is copied by dh-make into a +# dh-make output file, you may use that output file without restriction. +# This special exception was added by Craig Small in version 0.37 of dh-make. + +# Uncomment this to turn on verbose mode. +#export DH_VERBOSE=1 + + + + +CFLAGS = -Wall -g + +ifneq (,$(findstring noopt,$(DEB_BUILD_OPTIONS))) + CFLAGS += -O0 +else + CFLAGS += -O2 +endif + +configure: configure-stamp +configure-stamp: + dh_testdir + # Add here commands to configure the package. + + touch configure-stamp + + +build: build-stamp + +build-stamp: configure-stamp + dh_testdir + touch build-stamp + +clean: + dh_testdir + dh_testroot + rm -f build-stamp configure-stamp + dh_clean + +install: build + dh_testdir + dh_testroot + dh_clean -k + dh_installdirs + + # ====================================================== + #$(MAKE) DESTDIR="$(CURDIR)/debian/%(name)s" install + mkdir -p "$(CURDIR)/debian/%(name)s" + + %(rules)s + # ====================================================== + +# Build architecture-independent files here. +binary-indep: build install +# We have nothing to do by default. + +# Build architecture-dependent files here. +binary-arch: build install + dh_testdir + dh_testroot + dh_installchangelogs debian/changelog + dh_installdocs + dh_installexamples +# dh_install +# dh_installmenu +# dh_installdebconf +# dh_installlogrotate +# dh_installemacsen +# dh_installpam +# dh_installmime +# dh_python +# dh_installinit +# dh_installcron +# dh_installinfo + dh_installman + dh_link + dh_strip + dh_compress + dh_fixperms +# dh_perl +# dh_makeshlibs + dh_installdeb + dh_shlibdeps + dh_gencontrol + dh_md5sums + dh_builddeb + +binary: binary-indep binary-arch +.PHONY: build clean binary-indep binary-arch binary install configure +""" % locals() + open(os.path.join(DEBIAN, "rules"), "w").write(txt) + os.chmod(os.path.join(DEBIAN, "rules"), 0755) + + ########################################################################### + ########################################################################### + ########################################################################### + + generatedFiles = [] + + if build: + #http://www.debian.org/doc/manuals/maint-guide/ch-build.fr.html + ret=os.system('cd "%(DEST)s"; dpkg-buildpackage -tc -rfakeroot -us -uc' % locals()) + if ret != 0: + raise Py2debException("buildpackage failed (see output)") + + l=glob("%(TEMP)s/%(name)s*.deb"%locals()) + if len(l) != 1: + raise Py2debException("didn't find builded deb") + + tdeb = l[0] + deb = os.path.basename(tdeb) + shutil.move(tdeb, deb) + + generatedFiles = [deb, ] + + if rpm: + rpmFilename = deb2rpm(deb) + generatedFiles.append(rpmFilename) + + if src: + tarFilename = py2src(TEMP, name) + generatedFiles.append(tarFilename) + + if tar: + tarFilename = py2tar(DEST, TEMP, name, version) + generatedFiles.append(tarFilename) + + if dsc: + dscFilename = py2dsc(TEMP, name, version, depends, author, mail, arch) + generatedFiles.append(dscFilename) + + if changes: + changesFilenames = py2changes(locals()) + generatedFiles.extend(changesFilenames) + + return generatedFiles + + #~ except Exception,m: + #~ raise Py2debException("build error :"+str(m)) + + finally: + if Py2deb.clear: + shutil.rmtree(TEMP) + + +if __name__ == "__main__": + try: + os.chdir(os.path.dirname(sys.argv[0])) + except: + pass + + p=Py2deb("python-py2deb") + p.description="Generate simple deb(/rpm/tgz) from python (2.4, 2.5 and 2.6)" + p.url = "http://www.manatlan.com/page/py2deb" + p.author=__author__ + p.mail=__mail__ + p.depends = "dpkg-dev, fakeroot, alien, python" + p.section="python" + p["/usr/lib/python2.6/dist-packages"] = ["py2deb.py", ] + p["/usr/lib/python2.5/site-packages"] = ["py2deb.py", ] + p["/usr/lib/python2.4/site-packages"] = ["py2deb.py", ] + #~ p.postinstall = "s.py" + #~ p.preinstall = "s.py" + #~ p.postremove = "s.py" + #~ p.preremove = "s.py" + print p + print p.generate(__version__, changelog = __doc__, src=True) diff --git a/support/pylint.rc b/support/pylint.rc new file mode 100644 index 0000000..2a371a1 --- /dev/null +++ b/support/pylint.rc @@ -0,0 +1,305 @@ +# lint Python modules using external checkers. +# +# This is the main checker controling the other ones and the reports +# generation. It is itself both a raw checker and an astng checker in order +# to: +# * handle message activation / deactivation at the module level +# * handle some basic but necessary stats'data (number of classes, methods...) +# +[MASTER] + +# Specify a configuration file. +#rcfile= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Profiled execution. +profile=no + +# Add to the black list. It should be a base name, not a +# path. You may set this option multiple times. +ignore=CVS + +# Pickle collected data for later comparisons. +persistent=yes + +# Set the cache size for astng objects. +cache-size=500 + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + + +[MESSAGES CONTROL] + +# Enable only checker(s) with the given id(s). This option conflicts with the +# disable-checker option +#enable-checker= + +# Enable all checker(s) except those with the given id(s). This option +# conflicts with the enable-checker option +#disable-checker= + +# Enable all messages in the listed categories. +#enable-msg-cat= + +# Disable all messages in the listed categories. +#disable-msg-cat= + +# Enable the message(s) with the given id(s). +#enable-msg= + +# Disable the message(s) with the given id(s). +disable-msg=W0403,W0612,W0613,C0103,C0111,C0301,R0903,W0142,W0603,R0904,R0921,R0201 + +[REPORTS] + +# set the output format. Available formats are text, parseable, colorized, msvs +# (visual studio) and html +output-format=colorized + +# Include message's id in output +include-ids=yes + +# Put messages in a separate file for each module / package specified on the +# command line instead of printing them on stdout. Reports (if any) will be +# written in a file name "pylint_global.[txt|html]". +files-output=no + +# Tells wether to display a full report or only the messages +reports=no + +# Python expression which should return a note less than 10 (10 is the highest +# note).You have access to the variables errors warning, statement which +# respectivly contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (R0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Add a comment according to your evaluation note. This is used by the global +# evaluation report (R0004). +comment=no + +# Enable the report(s) with the given id(s). +#enable-report= + +# Disable the report(s) with the given id(s). +#disable-report= + + +# checks for +# * unused variables / imports +# * undefined variables +# * redefinition of variable from builtins or from an outer scope +# * use of variable before assigment +# +[VARIABLES] + +# Tells wether we should check for unused import in __init__ files. +init-import=no + +# A regular expression matching names used for dummy variables (i.e. not used). +dummy-variables-rgx=_|dummy + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins= + + +# checks for : +# * doc strings +# * modules / classes / functions / methods / arguments / variables name +# * number of arguments, local variables, branchs, returns and statements in +# functions, methods +# * required module attributes +# * dangerous default values as arguments +# * redefinition of function / method / class +# * uses of the global statement +# +[BASIC] + +# Required attributes for module, separated by a comma +required-attributes= + +# Regular expression which should only match functions or classes name which do +# not require a docstring +no-docstring-rgx=__.*__ + +# Regular expression which should only match correct module names +module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Regular expression which should only match correct module level names +const-rgx=(([A-Z_][A-Z1-9_]*)|(__.*__))$ + +# Regular expression which should only match correct class names +class-rgx=[A-Z_][a-zA-Z0-9]+$ + +# Regular expression which should only match correct function names +function-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match correct method names +method-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match correct instance attribute names +attr-rgx=[a-z_][a-zA-Z0-9_]{2,30}$ + +# Regular expression which should only match correct argument names +argument-rgx=[a-z_][a-zA-Z0-9_]{2,30}$ + +# Regular expression which should only match correct variable names +variable-rgx=[a-z_][a-zA-Z0-9_]{2,30}$ + +# Regular expression which should only match correct list comprehension / +# generator expression variable names +inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ + +# Good variable names which should always be accepted, separated by a comma +good-names=i,j,k,ex,Run,_ + +# Bad variable names which should always be refused, separated by a comma +bad-names=foo,bar,baz,toto,tutu,tata + +# List of builtins function names that should not be used, separated by a comma +bad-functions=map,filter,apply,input + + +# try to find bugs in the code using type inference +# +[TYPECHECK] + +# Tells wether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# When zope mode is activated, consider the acquired-members option to ignore +# access to some undefined attributes. +zope=no + +# List of members which are usually get through zope's acquisition mecanism and +# so shouldn't trigger E0201 when accessed (need zope=yes to be considered). +acquired-members=REQUEST,acl_users,aq_parent + + +# checks for sign of poor/misdesign: +# * number of methods, attributes, local variables... +# * size, complexity of functions, methods +# +[DESIGN] + +# Maximum number of arguments for function / method +max-args=5 + +# Maximum number of locals for function / method body +max-locals=15 + +# Maximum number of return / yield for function / method body +max-returns=6 + +# Maximum number of branch for function / method body +max-branchs=12 + +# Maximum number of statements in function / method body +max-statements=50 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of attributes for a class (see R0902). +max-attributes=15 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=1 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + + +# checks for : +# * methods without self as first argument +# * overridden methods signature +# * access only to existant members via self +# * attributes not defined in the __init__ method +# * supported interfaces implementation +# * unreachable code +# +[CLASSES] + +# List of interface methods to ignore, separated by a comma. This is used for +# instance to not check methods defines in Zope's Interface base class. +ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__,__new__,setUp + + +# checks for +# * external modules dependencies +# * relative / wildcard imports +# * cyclic imports +# * uses of deprecated modules +# +[IMPORTS] + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=regsub,string,TERMIOS,Bastion,rexec + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report R0402 must not be disabled) +import-graph= + +# Create a graph of external dependencies in the given file (report R0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of internal dependencies in the given file (report R0402 must +# not be disabled) +int-import-graph= + + +# checks for similarities and duplicated code. This computation may be +# memory / CPU intensive, so you should disable it if you experiments some +# problems. +# +[SIMILARITIES] + +# Minimum lines number of a similarity. +min-similarity-lines=4 + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + + +# checks for: +# * warning notes in the code like FIXME, XXX +# * PEP 263: source code with non ascii character but no encoding declaration +# +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME,XXX,TODO + + +# checks for : +# * unauthorized constructions +# * strict indentation +# * line length +# * use of <> instead of != +# +[FORMAT] + +# Maximum number of characters on a single line. +# @note Limiting this to the most extreme cases +max-line-length=100 + +# Maximum number of lines in a module +max-module-lines=1000 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string='\t' diff --git a/support/test_syntax.py b/support/test_syntax.py new file mode 100755 index 0000000..65a373c --- /dev/null +++ b/support/test_syntax.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python + +import commands + + +verbose = False + + +def syntax_test(file): + commandTemplate = """ + python -t -t -W all -c "import py_compile; py_compile.compile ('%(filename)s', doraise=False)" """ + compileCommand = commandTemplate % {"filename": file} + (status, text) = commands.getstatusoutput (compileCommand) + text = text.rstrip() + passed = len(text) == 0 + + if passed: + output = ("Syntax is correct for "+file) if verbose else "" + else: + output = ("Syntax is invalid for %s\n" % file) if verbose else "" + output += text + return (passed, output) + + +if __name__ == "__main__": + import sys + import os + import optparse + + opar = optparse.OptionParser() + opar.add_option("-v", "--verbose", dest="verbose", help="Toggle verbosity", action="store_true", default=False) + options, args = opar.parse_args(sys.argv[1:]) + verbose = options.verbose + + completeOutput = [] + allPassed = True + for filename in args: + passed, output = syntax_test(filename) + if not passed: + allPassed = False + if output.strip(): + completeOutput.append(output) + print "\n".join(completeOutput) + + sys.exit(0 if allPassed else 1); diff --git a/support/todo.py b/support/todo.py new file mode 100755 index 0000000..90cbd04 --- /dev/null +++ b/support/todo.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python + +from __future__ import with_statement +import itertools + + +verbose = False + + +def tag_parser(file, tag): + """ + >>> nothing = [] + >>> for todo in tag_parser(nothing, "@todo"): + ... print todo + ... + >>> one = ["@todo Help!"] + >>> for todo in tag_parser(one, "@todo"): + ... print todo + ... + 1: @todo Help! + >>> mixed = ["one", "@todo two", "three"] + >>> for todo in tag_parser(mixed, "@todo"): + ... print todo + ... + 2: @todo two + >>> embedded = ["one @todo two", "three"] + >>> for todo in tag_parser(embedded, "@todo"): + ... print todo + ... + 1: @todo two + >>> continuation = ["one", "@todo two", " three"] + >>> for todo in tag_parser(continuation, "@todo"): + ... print todo + ... + 2: @todo two three + >>> series = ["one", "@todo two", "@todo three"] + >>> for todo in tag_parser(series, "@todo"): + ... print todo + ... + 2: @todo two + 3: @todo three + """ + currentTodo = [] + prefix = None + for lineNumber, line in enumerate(file): + column = line.find(tag) + if column != -1: + if currentTodo: + yield "\n".join (currentTodo) + prefix = line[0:column] + currentTodo = ["%d: %s" % (lineNumber+1, line[column:].strip())] + elif prefix is not None and len(prefix)+1 < len(line) and line.startswith(prefix) and line[len(prefix)].isspace(): + currentTodo.append (line[len(prefix):].rstrip()) + elif currentTodo: + yield "\n".join (currentTodo) + currentTodo = [] + prefix = None + if currentTodo: + yield "\n".join (currentTodo) + + +def tag_finder(filename, tag): + todoList = [] + + with open(filename) as file: + body = "\n".join (tag_parser(file, tag)) + passed = not body + if passed: + output = "No %s's for %s" % (tag, filename) if verbose else "" + else: + header = "%s's for %s:\n" % (tag, filename) if verbose else "" + output = header + body + output += "\n" if verbose else "" + + return (passed, output) + + +if __name__ == "__main__": + import sys + import os + import optparse + + opar = optparse.OptionParser() + opar.add_option("-v", "--verbose", dest="verbose", help="Toggle verbosity", action="store_true", default=False) + options, args = opar.parse_args(sys.argv[1:]) + verbose = options.verbose + + bugsAsError = True + todosAsError = False + + completeOutput = [] + allPassed = True + for filename in args: + bugPassed, bugOutput = tag_finder(filename, "@bug") + todoPassed, todoOutput = tag_finder(filename, "@todo") + output = "\n".join ([bugOutput, todoOutput]) + if (not bugPassed and bugsAsError) or (not todoPassed and todosAsError): + allPassed = False + output = output.strip() + if output: + completeOutput.append(filename+":\n"+output+"\n\n") + print "\n".join(completeOutput) + + sys.exit(0 if allPassed else 1); diff --git a/tests/basic_data/basic.csv b/tests/basic_data/basic.csv new file mode 100644 index 0000000..6ffd844 --- /dev/null +++ b/tests/basic_data/basic.csv @@ -0,0 +1,2 @@ +familyname,phone,addr1,addr2,addr3,addr4,name1,name2,name3,name4 +"Last, First","555-123-4567","1234 Foo St","Austin, Texas 78727","","","First " diff --git a/tests/basic_data/empty.csv b/tests/basic_data/empty.csv new file mode 100644 index 0000000..e69de29 diff --git a/tests/basic_data/google.csv b/tests/basic_data/google.csv new file mode 100644 index 0000000..3fa4d3e --- /dev/null +++ b/tests/basic_data/google.csv @@ -0,0 +1,7 @@ +Name,E-mail,Notes,Section 1 - Description,Section 1 - Email,Section 1 - IM,Section 1 - Phone,Section 1 - Mobile,Section 1 - Pager,Section 1 - Fax,Section 1 - Company,Section 1 - Title,Section 1 - Other,Section 1 - Address,Section 2 - Description,Section 2 - Email,Section 2 - IM,Section 2 - Phone,Section 2 - Mobile,Section 2 - Pager,Section 2 - Fax,Section 2 - Company,Section 2 - Title,Section 2 - Other,Section 2 - Address,Section 3 - Description,Section 3 - Email,Section 3 - IM,Section 3 - Phone,Section 3 - Mobile,Section 3 - Pager,Section 3 - Fax,Section 3 - Company,Section 3 - Title,Section 3 - Other,Section 3 - Address +First Last,name@domain.com,"Categories: Others + + +Categories: Others",Other,name2@domain.com,,,,,,,,,,Personal,name3@domain.com; name4@domain.com; name5@domain.com; name6@domain.com,,17471234567,5551234567,,,,,, +First1 Last,,"Categories: Friends +",Personal,,,,5557654321,,,,,, diff --git a/tests/basic_data/grandcentral.csv b/tests/basic_data/grandcentral.csv new file mode 100644 index 0000000..4808cf2 --- /dev/null +++ b/tests/basic_data/grandcentral.csv @@ -0,0 +1,3 @@ +Name,E-mail Address,Categories,Company,Job Title,Home Address,Home Phone,Mobile Phone,Business Phone,GrandCentral,Suffix,Title,Initials,Web Page,Notes +First Last,,Family,,,1234567 Foo St Austn Tx 78727,5559983254,5554023626,5559988899,,,,,, +First1 Last,,Others,,,,5556835460,,,,,,,, diff --git a/tests/basic_data/settings.ini b/tests/basic_data/settings.ini new file mode 100644 index 0000000..d9a6c81 --- /dev/null +++ b/tests/basic_data/settings.ini @@ -0,0 +1,37 @@ +[1 - Contacts] + +[2 - Account Info] +callback = + +[1 - Recent Calls] + +[2 - Messages] + +[0 - Messages] + +[1 - Messages] + +[2 - Dialpad] + +[2 - Contacts] + +[0 - Recent Calls] + +[DialCentral] +active = 0 +bin_blob_0 = +bin_blob_1 = + +[1 - Account Info] +callback = + +[1 - Dialpad] + +[0 - Dialpad] + +[0 - Account Info] + +[0 - Contacts] + +[2 - Recent Calls] + diff --git a/tests/dummy_hildon/__init__.py b/tests/dummy_hildon/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/dummy_hildon/hildon.py b/tests/dummy_hildon/hildon.py new file mode 100644 index 0000000..331e979 --- /dev/null +++ b/tests/dummy_hildon/hildon.py @@ -0,0 +1,35 @@ +import gobject +import gtk + +class FileChooserDialog(gtk.FileChooserDialog): + """ + @bug The buttons currently don't do anything + """ + + def __init__(self, *args, **kwds): + super(FileChooserDialog, self).__init__(*args, **kwds) + self.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL) + self.add_button(gtk.STOCK_OK, gtk.RESPONSE_OK) + + +class Program(object): + + def add_window(self, window): + pass + + +class Window(gtk.Window, object): + + def __init__(self): + super(Window, self).__init__(gtk.WINDOW_TOPLEVEL) + self.set_default_size(700, 500) + + def set_menu(self, menu): + self._hildonMenu = menu + + +gobject.type_register(Window) + + +def hildon_helper_set_thumb_scrollbar(widget, value): + pass diff --git a/tests/gc_samples/__init__.py b/tests/gc_samples/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/gc_samples/dump_cookies.py b/tests/gc_samples/dump_cookies.py new file mode 100755 index 0000000..810a03b --- /dev/null +++ b/tests/gc_samples/dump_cookies.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python + +import os +import urllib +import urllib2 +import traceback +import warnings + +import sys +sys.path.append("../../src") + +import browser_emu +import gc_backend + +# Create Browser +browser = browser_emu.MozillaEmulator(1) +cookieFile = os.path.join(".", ".gc_cookies.txt") +browser.cookies.filename = cookieFile + +# Login +username = sys.argv[1] +password = sys.argv[2] + +loginPostData = urllib.urlencode({ + 'Email' : username, + 'Passwd' : password, + 'service': "grandcentral", + "ltmpl": "mobile", + "btmpl": "mobile", + "PersistentCookie": "yes", +}) + +try: + loginSuccessOrFailurePage = browser.download(gc_backend.GCDialer._loginURL, loginPostData) +except urllib2.URLError, e: + warnings.warn(traceback.format_exc()) + raise RuntimeError("%s is not accesible" % gc_backend.GCDialer._loginURL) + +forwardPage = browser.download(gc_backend.GCDialer._forwardselectURL) + +tokenGroup = gc_backend.GCDialer._accessTokenRe.search(forwardPage) +if tokenGroup is None: + print forwardPage + raise RuntimeError("Could not extract authentication token from GrandCentral") +token = tokenGroup.group(1) + + +with open("cookies.txt", "w") as f: + f.writelines( + "%s: %s\n" % (c.name, c.value) + for c in browser.cookies + ) diff --git a/tests/gc_samples/generate_gc_samples.py b/tests/gc_samples/generate_gc_samples.py new file mode 100755 index 0000000..0608ffd --- /dev/null +++ b/tests/gc_samples/generate_gc_samples.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python + +import os +import urllib +import urllib2 +import traceback +import warnings + +import sys +sys.path.append("../../src") + +import browser_emu +import gc_backend + +webpages = [ + ("forward", gc_backend.GCDialer._forwardselectURL), + ("login", gc_backend.GCDialer._loginURL), + ("setforward", gc_backend.GCDialer._setforwardURL), + ("clicktocall", gc_backend.GCDialer._clicktocallURL), + ("recent", gc_backend.GCDialer._inboxallURL), + ("contacts", gc_backend.GCDialer._contactsURL), +] + + +# Create Browser +browser = browser_emu.MozillaEmulator(1) +cookieFile = os.path.join(".", ".gc_cookies.txt") +browser.cookies.filename = cookieFile + +# Get Pages +for name, url in webpages: + try: + page = browser.download(url) + except StandardError, e: + print e.message + continue + with open("not_loggedin_%s.txt" % name, "w") as f: + f.write(page) + +# Login +username = sys.argv[1] +password = sys.argv[2] + +loginPostData = urllib.urlencode({ + 'username' : username, + 'password' : password, +}) + +try: + loginSuccessOrFailurePage = browser.download(gc_backend.GCDialer._loginURL, loginPostData) +except urllib2.URLError, e: + warnings.warn(traceback.format_exc()) + raise RuntimeError("%s is not accesible" % gc_backend.GCDialer._loginURL) + +forwardPage = browser.download(gc_backend.GCDialer._forwardselectURL) + +tokenGroup = gc_backend.GCDialer._accessTokenRe.search(forwardPage) +if tokenGroup is None: + print "="*60 + print forwardPage + print "="*60 + raise RuntimeError("Could not extract authentication token from GrandCentral") +token = tokenGroup.group(1) + +# Get Pages +for name, url in webpages: + try: + page = browser.download(url) + except StandardError, e: + warnings.warn(traceback.format_exc()) + continue + print "Writing to file" + with open("loggedin_%s.txt" % name, "w") as f: + f.write(page) diff --git a/tests/gv_samples/__init__.py b/tests/gv_samples/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/gv_samples/dump_cookies.py b/tests/gv_samples/dump_cookies.py new file mode 100755 index 0000000..748826b --- /dev/null +++ b/tests/gv_samples/dump_cookies.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python + +import os +import urllib +import urllib2 +import traceback +import warnings + +import sys +sys.path.append("../../src") + +import browser_emu +import gv_backend + +# Create Browser +browser = browser_emu.MozillaEmulator(1) +cookieFile = os.path.join(".", ".gv_cookies.txt") +browser.cookies.filename = cookieFile + +# Login +username = sys.argv[1] +password = sys.argv[2] + +loginPostData = urllib.urlencode({ + 'Email' : username, + 'Passwd' : password, + 'service': "grandcentral", + "ltmpl": "mobile", + "btmpl": "mobile", + "PersistentCookie": "yes", +}) + +try: + loginSuccessOrFailurePage = browser.download(gv_backend.GVDialer._loginURL, loginPostData) +except urllib2.URLError, e: + warnings.warn(traceback.format_exc()) + raise RuntimeError("%s is not accesible" % gv_backend.GVDialer._loginURL) + +forwardPage = browser.download(gv_backend.GVDialer._forwardURL) + +tokenGroup = gv_backend.GVDialer._tokenRe.search(forwardPage) +if tokenGroup is None: + print forwardPage + raise RuntimeError("Could not extract authentication token from GoogleVoice") +token = tokenGroup.group(1) + + +with open("cookies.txt", "w") as f: + f.writelines( + "%s: %s\n" % (c.name, c.value) + for c in browser.cookies + ) diff --git a/tests/gv_samples/generate_gv_samples.py b/tests/gv_samples/generate_gv_samples.py new file mode 100755 index 0000000..dd7c82e --- /dev/null +++ b/tests/gv_samples/generate_gv_samples.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python + +import os +import urllib +import urllib2 +import traceback +import warnings + +import sys +sys.path.append("../../src") + +import browser_emu +import gv_backend + +webpages = [ + ("login", gv_backend.GVDialer._loginURL), + ("contacts", gv_backend.GVDialer._contactsURL), + ("voicemail", gv_backend.GVDialer._voicemailURL), + ("sms", gv_backend.GVDialer._smsURL), + ("forward", gv_backend.GVDialer._forwardURL), + ("recent", gv_backend.GVDialer._recentCallsURL), + ("placed", gv_backend.GVDialer._placedCallsURL), + ("recieved", gv_backend.GVDialer._receivedCallsURL), + ("missed", gv_backend.GVDialer._missedCallsURL), +] + + +# Create Browser +browser = browser_emu.MozillaEmulator(1) +cookieFile = os.path.join(".", ".gv_cookies.txt") +browser.cookies.filename = cookieFile + +# Get Pages +for name, url in webpages: + try: + page = browser.download(url) + except StandardError, e: + print e.message + continue + with open("not_loggedin_%s.txt" % name, "w") as f: + f.write(page) + +# Login +username = sys.argv[1] +password = sys.argv[2] + +loginPostData = urllib.urlencode({ + 'Email' : username, + 'Passwd' : password, + 'service': "grandcentral", + "ltmpl": "mobile", + "btmpl": "mobile", + "PersistentCookie": "yes", +}) + +try: + loginSuccessOrFailurePage = browser.download(gv_backend.GVDialer._loginURL, loginPostData) +except urllib2.URLError, e: + warnings.warn(traceback.format_exc()) + raise RuntimeError("%s is not accesible" % gv_backend.GVDialer._loginURL) +with open("loggingin.txt", "w") as f: + f.write(page) + +forwardPage = browser.download(gv_backend.GVDialer._forwardURL) + +tokenGroup = gv_backend.GVDialer._tokenRe.search(forwardPage) +if tokenGroup is None: + print forwardPage + raise RuntimeError("Could not extract authentication token from GoogleVoice") +token = tokenGroup.group(1) + +# Get Pages +for name, url in webpages: + try: + page = browser.download(url) + except StandardError, e: + warnings.warn(traceback.format_exc()) + continue + print "Writing to file" + with open("loggedin_%s.txt" % name, "w") as f: + f.write(page) diff --git a/tests/test_file_backend.py b/tests/test_file_backend.py new file mode 100644 index 0000000..edd6c85 --- /dev/null +++ b/tests/test_file_backend.py @@ -0,0 +1,155 @@ +from __future__ import with_statement + +import os +import warnings + +import test_utils + +import sys +sys.path.append("../src") + +import file_backend + + +def test_factory(): + warnings.simplefilter("always") + try: + csvPath = os.path.join(os.path.dirname(__file__), "basic_data") + factory = file_backend.FilesystemAddressBookFactory(csvPath) + assert factory.factory_name() == "File" + abooks = list(factory.get_addressbooks()) + abooks.sort() + assert len(abooks) == 4 + abookNames = [abook[2] for abook in abooks] + assert abookNames == ["basic", "empty", "google", "grandcentral"], "%s" % abookNames + + for abook_factory, abookId, abookName in abooks: + abook = abook_factory.open_addressbook(abookId) + assert isinstance(abook, file_backend.CsvAddressBook) + finally: + warnings.resetwarnings() + + +def test_nonexistent_csv(): + warnings.simplefilter("always") + try: + csvPath = os.path.join(os.path.dirname(__file__), "basic_data", "nonexistent.csv") + abook = file_backend.CsvAddressBook(csvPath) + + assert abook.factory_name() == "csv" + + contacts = list(abook.get_contacts()) + assert len(contacts) == 0 + finally: + warnings.resetwarnings() + + +def test_empty_csv(): + warnings.simplefilter("always") + try: + csvPath = os.path.join(os.path.dirname(__file__), "basic_data", "empty.csv") + abook = file_backend.CsvAddressBook(csvPath) + + assert abook.factory_name() == "csv" + + contacts = list(abook.get_contacts()) + assert len(contacts) == 0 + finally: + warnings.resetwarnings() + + +def test_basic_csv(): + warnings.simplefilter("always") + try: + csvPath = os.path.join(os.path.dirname(__file__), "basic_data", "basic.csv") + abook = file_backend.CsvAddressBook(csvPath) + + assert abook.factory_name() == "csv" + + contacts = list(abook.get_contacts()) + contacts.sort() + assert len(contacts) == 1 + + contactId, contactName = contacts[0] + assert contactName == "Last, First" + assert abook.contact_source_short_name(contactId) == "csv" + + details = list(abook.get_contact_details(contactId)) + assert len(details) == 1 + details.sort() + assert details == [("phone", "555-123-4567")], "%s" % details + finally: + warnings.resetwarnings() + + +def test_google_csv(): + warnings.simplefilter("always") + try: + csvPath = os.path.join(os.path.dirname(__file__), "basic_data", "google.csv") + abook = file_backend.CsvAddressBook(csvPath) + + assert abook.factory_name() == "csv" + + contacts = list(abook.get_contacts()) + contacts.sort() + assert len(contacts) == 2 + + contactId, contactName = contacts[0] + assert contactName == "First Last" + assert abook.contact_source_short_name(contactId) == "csv" + + details = list(abook.get_contact_details(contactId)) + assert len(details) == 2 + details.sort() + assert details == [ + ("Section 2 - Mobile", "5551234567"), + ("Section 2 - Phone", "17471234567"), + ], "%s" % details + + contactId, contactName = contacts[1] + assert contactName == "First1 Last" + assert abook.contact_source_short_name(contactId) == "csv" + + details = list(abook.get_contact_details(contactId)) + assert len(details) == 1 + details.sort() + assert details == [("Section 1 - Mobile", "5557654321")], "%s" % details + finally: + warnings.resetwarnings() + + +def test_grandcentral_csv(): + warnings.simplefilter("always") + try: + csvPath = os.path.join(os.path.dirname(__file__), "basic_data", "grandcentral.csv") + abook = file_backend.CsvAddressBook(csvPath) + + assert abook.factory_name() == "csv" + + contacts = list(abook.get_contacts()) + contacts.sort() + assert len(contacts) == 2 + + contactId, contactName = contacts[0] + assert contactName == "First Last" + assert abook.contact_source_short_name(contactId) == "csv" + + details = list(abook.get_contact_details(contactId)) + assert len(details) == 3 + details.sort() + assert details == [ + ("Business Phone", "5559988899"), + ("Home Phone", "5559983254"), + ("Mobile Phone", "5554023626"), + ], "%s" % details + + contactId, contactName = contacts[1] + assert contactName == "First1 Last" + assert abook.contact_source_short_name(contactId) == "csv" + + details = list(abook.get_contact_details(contactId)) + assert len(details) == 1 + details.sort() + assert details == [("Home Phone", "5556835460")], "%s" % details + finally: + warnings.resetwarnings() diff --git a/tests/test_gc_backend.py b/tests/test_gc_backend.py new file mode 100644 index 0000000..e60e777 --- /dev/null +++ b/tests/test_gc_backend.py @@ -0,0 +1,53 @@ +from __future__ import with_statement + +import os +import warnings +import cookielib + +import test_utils + +import sys +sys.path.append("../src") + +import gc_backend + + +def generate_mock(cookiesSucceed, username, password): + + class MockModule(object): + + class MozillaEmulator(object): + + def __init__(self, trycount = 1): + self.cookies = cookielib.LWPCookieJar() + self.trycount = trycount + + def download(self, url, + postdata = None, extraheaders = None, forbid_redirect = False, + trycount = None, only_head = False, + ): + return "" + + return MockModule + + +def test_not_logged_in(): + correctUsername, correctPassword = "", "" + MockBrowserModule = generate_mock(False, correctUsername, correctPassword) + gc_backend.browser_emu, RealBrowser = MockBrowserModule, gc_backend.browser_emu + try: + backend = gc_backend.GCDialer() + assert not backend.is_authed() + assert not backend.login("bad_name", "bad_password") + backend.logout() + with test_utils.expected(RuntimeError): + backend.dial("5551234567") + with test_utils.expected(RuntimeError): + backend.send_sms("5551234567", "Hello World") + assert backend.get_account_number() == "", "%s" % backend.get_account_number() + backend.set_sane_callback() + assert backend.get_callback_number() == "" + recent = list(backend.get_recent()) + messages = list(backend.get_messages()) + finally: + gc_backend.browser_emu = RealBrowser diff --git a/tests/test_gv_backend.py b/tests/test_gv_backend.py new file mode 100644 index 0000000..e43cb00 --- /dev/null +++ b/tests/test_gv_backend.py @@ -0,0 +1,55 @@ +from __future__ import with_statement + +import os +import warnings +import cookielib + +import test_utils + +import sys +sys.path.append("../src") + +import gv_backend + + +def generate_mock(cookiesSucceed, username, password): + + class MockModule(object): + + class MozillaEmulator(object): + + def __init__(self, trycount = 1): + self.cookies = cookielib.LWPCookieJar() + self.trycount = trycount + + def download(self, url, + postdata = None, extraheaders = None, forbid_redirect = False, + trycount = None, only_head = False, + ): + return "" + + return MockModule + + +def test_not_logged_in(): + correctUsername, correctPassword = "", "" + MockBrowserModule = generate_mock(False, correctUsername, correctPassword) + gv_backend.browser_emu, RealBrowser = MockBrowserModule, gv_backend.browser_emu + try: + backend = gv_backend.GVDialer() + assert not backend.is_authed() + assert not backend.login("bad_name", "bad_password") + backend.logout() + with test_utils.expected(RuntimeError): + backend.dial("5551234567") + with test_utils.expected(RuntimeError): + backend.send_sms("5551234567", "Hello World") + assert backend.get_account_number() == "", "%s" % backend.get_account_number() + backend.set_sane_callback() + assert backend.get_callback_number() == "" + with test_utils.expected(Exception): + recent = list(backend.get_recent()) + with test_utils.expected(Exception): + messages = list(backend.get_messages()) + finally: + gv_backend.browser_emu = RealBrowser diff --git a/tests/test_startup.py b/tests/test_startup.py new file mode 100644 index 0000000..5058fc0 --- /dev/null +++ b/tests/test_startup.py @@ -0,0 +1,108 @@ +from __future__ import with_statement + +import os +import time +import warnings + +import test_utils + +import sys +sys.path.append("../src") + +import dc_glade + + +def startup(factory): + handle = factory() + with test_utils.expected(AssertionError("Attempting login before app is fully loaded")): + handle.refresh_session() + + for i in xrange(10): + if handle._initDone: + print "Completed init on iteration %d" % i + break + time.sleep(1) + assert handle._initDone + + with test_utils.expected(RuntimeError("Login Failed")): + handle.refresh_session() + + handle._save_settings() + + del handle + + +def atest_startup_with_no_data_dir_with_dummy_hildon(): + warnings.simplefilter("always") + hildonPath = os.path.join(os.path.dirname(__file__), "dummy_hildon") + sys.path.insert(0, hildonPath) + import hildon + dc_glade.hildon = hildon + try: + dc_glade.Dialcentral._data_path = os.path.join(os.path.dirname(__file__), "notexistent_data") + dc_glade.Dialcentral._user_settings = "%s/settings.ini" % dc_glade.Dialcentral._data_path + + try: + startup(dc_glade.Dialcentral) + finally: + try: + os.remove(dc_glade.Dialcentral._user_settings) + except: + pass + try: + os.removedirs(dc_glade.Dialcentral._data_path) + except: + pass + finally: + dc_glade.hildon = None + sys.path.remove(hildonPath) + warnings.resetwarnings() + + +def atest_startup_with_no_data_dir(): + warnings.simplefilter("always") + dc_glade.Dialcentral._data_path = os.path.join(os.path.dirname(__file__), "notexistent_data") + dc_glade.Dialcentral._user_settings = "%s/settings.ini" % dc_glade.Dialcentral._data_path + + try: + startup(dc_glade.Dialcentral) + finally: + try: + os.remove(dc_glade.Dialcentral._user_settings) + except: + pass + try: + os.removedirs(dc_glade.Dialcentral._data_path) + except: + pass + warnings.resetwarnings() + + +def atest_startup_with_empty_data_dir(): + warnings.simplefilter("always") + dc_glade.Dialcentral._data_path = os.path.join(os.path.dirname(__file__), "empty_data") + dc_glade.Dialcentral._user_settings = "%s/settings.ini" % dc_glade.Dialcentral._data_path + + try: + startup(dc_glade.Dialcentral) + finally: + try: + os.remove(dc_glade.Dialcentral._user_settings) + except: + pass + try: + os.removedirs(dc_glade.Dialcentral._data_path) + except: + pass + warnings.resetwarnings() + + +def atest_startup_with_basic_data_dir(): + warnings.simplefilter("always") + try: + dc_glade.Dialcentral._data_path = os.path.join(os.path.dirname(__file__), "basic_data") + dc_glade.Dialcentral._user_settings = "%s/settings.ini" % dc_glade.Dialcentral._data_path + + startup(dc_glade.Dialcentral) + finally: + warnings.resetwarnings() diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..a2da797 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python + + +from __future__ import with_statement + +import inspect +import contextlib +import functools + + +def TODO(func): + """ + unittest test method decorator that ignores + exceptions raised by test + + Used to annotate test methods for code that may + not be written yet. Ignores failures in the + annotated test method; fails if the text + unexpectedly succeeds. + !author http://kbyanc.blogspot.com/2007/06/pythons-unittest-module-aint-that-bad.html + + Example: + >>> import unittest + >>> class ExampleTestCase(unittest.TestCase): + ... @TODO + ... def testToDo(self): + ... MyModule.DoesNotExistYet('boo') + ... + """ + + @functools.wraps(func) + def wrapper(*args, **kw): + try: + func(*args, **kw) + succeeded = True + except: + succeeded = False + assert succeeded is False, \ + "%s marked TODO but passed" % func.__name__ + return wrapper + + +def PlatformSpecific(platformList): + """ + unittest test method decorator that only + runs test method if os.name is in the + given list of platforms + !author http://kbyanc.blogspot.com/2007/06/pythons-unittest-module-aint-that-bad.html + Example: + >>> import unittest + >>> class ExampleTestCase(unittest.TestCase): + ... @PlatformSpecific(('mac', )) + ... def testMacOnly(self): + ... MyModule.SomeMacSpecificFunction() + ... + """ + + def decorator(func): + import os + + @functools.wraps(func) + def wrapper(*args, **kw): + if os.name in platformList: + return func(*args, **kw) + return wrapper + return decorator + + +def CheckReferences(func): + """ + !author http://kbyanc.blogspot.com/2007/06/pythons-unittest-module-aint-that-bad.html + """ + + @functools.wraps(func) + def wrapper(*args, **kw): + refCounts = [] + for i in range(5): + func(*args, **kw) + refCounts.append(XXXGetRefCount()) + assert min(refCounts) != max(refCounts), "Reference counts changed - %r" % refCounts + + return wrapper + + +@contextlib.contextmanager +def expected(exception): + """ + >>> with expected2(ZeroDivisionError): + ... 1 / 0 + >>> with expected2(AssertionError("expected ZeroDivisionError to have been thrown")): + ... with expected(ZeroDivisionError): + ... 1 / 2 + Traceback (most recent call last): + File "/usr/lib/python2.5/doctest.py", line 1228, in __run + compileflags, 1) in test.globs + File "", line 3, in + 1 / 2 + File "/media/data/Personal/Development/bzr/Recollection-trunk/src/libraries/recipes/context.py", line 139, in __exit__ + assert t is not None, ("expected {0:%s} to have been thrown" % (self._t.__name__)) + AssertionError: expected {0:ZeroDivisionError} to have been thrown + >>> with expected2(Exception("foo")): + ... raise Exception("foo") + >>> with expected2(Exception("bar")): + ... with expected(Exception("foo")): # this won't catch it + ... raise Exception("bar") + ... assert False, "should not see me" + >>> with expected2(Exception("can specify")): + ... raise Exception("can specify prefixes") + >>> with expected2(Exception("Base class fun")): + True + >>> True + False + """ + if isinstance(exception, Exception): + excType, excValue = type(exception), str(exception) + elif isinstance(exception, type): + excType, excValue = exception, "" + + try: + yield + except Exception, e: + if not (excType in inspect.getmro(type(e)) and str(e).startswith(excValue)): + raise + else: + raise AssertionError("expected {0:%s} to have been thrown" % excType.__name__) + + +if __name__ == "__main__": + import doctest + doctest.testmod() -- 1.7.9.5