--- /dev/null
+ 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.
+\f
+ 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.
+\f
+ 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.
+\f
+ 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.
+\f
+ 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.
+\f
+ 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.
+\f
+ 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.
+\f
+ 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.
+\f
+ 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
+\f
+ 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.
+
+ <one line to give the library's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ 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.
+
+ <signature of Ty Coon>, 1 April 1990
+ Ty Coon, President of Vice
+
+That's all there is to it!
+
+
--- /dev/null
+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 $*)))
--- /dev/null
+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?
--- /dev/null
+src/gv_views.py:
+21: @todo Alternate UI for dialogs (stackables)
+416: @todo Add multi-SMS messages like GoogleVoice
+
+
--- /dev/null
+#!/usr/bin/env python
--- /dev/null
+#!/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()
--- /dev/null
+#!/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()
--- /dev/null
+"""
+@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
--- /dev/null
+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_
--- /dev/null
+#!/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(["<z2n@merctech.com>", "Eric Warnke <ericew@gmail.com>", "Ed Page <edpage@byu.net>"])
+ 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()
--- /dev/null
+<?xml version="1.0"?>
+<glade-interface>
+ <!-- interface-requires gtk+ 2.16 -->
+ <!-- interface-naming-policy toplevel-contextual -->
+ <widget class="GtkWindow" id="mainWindow">
+ <property name="title" translatable="yes">Dialer</property>
+ <property name="default_width">800</property>
+ <property name="default_height">480</property>
+ <child>
+ <widget class="GtkVBox" id="mainLayout">
+ <property name="visible">True</property>
+ <child>
+ <widget class="GtkMenuBar" id="dialpad_menubar">
+ <property name="visible">True</property>
+ <child>
+ <widget class="GtkMenuItem" id="login_menu_item">
+ <property name="visible">True</property>
+ <property name="label">_Login</property>
+ <property name="use_underline">True</property>
+ <signal name="activate" handler="on_clearcookies_clicked"/>
+ </widget>
+ </child>
+ <child>
+ <widget class="GtkMenuItem" id="paste_menu_item">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">Paste</property>
+ <property name="use_underline">True</property>
+ <signal name="activate" handler="on_paste"/>
+ </widget>
+ </child>
+ <child>
+ <widget class="GtkMenuItem" id="refreshMenuItem">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">Refresh</property>
+ <property name="use_underline">True</property>
+ <signal name="activate" handler="on_refresh"/>
+ </widget>
+ </child>
+ <child>
+ <widget class="GtkMenuItem" id="about">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">_About</property>
+ <property name="use_underline">True</property>
+ <signal name="activate" handler="on_about_activate"/>
+ </widget>
+ </child>
+ </widget>
+ <packing>
+ <property name="expand">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkEventBox" id="errorEventBox">
+ <property name="visible">True</property>
+ <child>
+ <widget class="GtkHBox" id="errorBox">
+ <property name="visible">True</property>
+ <child>
+ <widget class="GtkImage" id="errorImage">
+ <property name="visible">True</property>
+ <property name="stock">gtk-dialog-error</property>
+ </widget>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkLabel" id="errorDescription">
+ <property name="visible">True</property>
+ <property name="use_markup">True</property>
+ <property name="ellipsize">end</property>
+ <property name="single_line_mode">True</property>
+ </widget>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkImage" id="errorClose">
+ <property name="visible">True</property>
+ <property name="stock">gtk-close</property>
+ </widget>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </widget>
+ </child>
+ </widget>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkNotebook" id="notebook">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="tab_pos">left</property>
+ <property name="show_border">False</property>
+ <property name="homogeneous">True</property>
+ <child>
+ <widget class="GtkVBox" id="keypad_vbox">
+ <property name="visible">True</property>
+ <child>
+ <widget class="GtkHBox" id="hbox3">
+ <property name="visible">True</property>
+ <child>
+ <widget class="GtkLabel" id="numberdisplay">
+ <property name="height_request">50</property>
+ <property name="visible">True</property>
+ <property name="label" translatable="yes"><span size="35000" weight="bold"></span></property>
+ <property name="use_markup">True</property>
+ <property name="justify">center</property>
+ </widget>
+ <packing>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkButton" id="back">
+ <property name="label" translatable="yes">gtk-go-back</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_stock">True</property>
+ <property name="focus_on_click">False</property>
+ <accelerator key="BackSpace" signal="clicked"/>
+ </widget>
+ <packing>
+ <property name="expand">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </widget>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkTable" id="keypadview">
+ <property name="visible">True</property>
+ <property name="n_rows">4</property>
+ <property name="n_columns">3</property>
+ <property name="homogeneous">True</property>
+ <child>
+ <widget class="GtkButton" id="digit1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="receives_default">False</property>
+ <property name="focus_on_click">False</property>
+ <signal name="clicked" handler="on_digit_clicked"/>
+ <accelerator key="1" signal="clicked"/>
+ <child>
+ <widget class="GtkLabel" id="label12">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes"><span size="33000" weight="bold">1</span>
+<span size="9000"> </span></property>
+ <property name="use_markup">True</property>
+ </widget>
+ </child>
+ </widget>
+ </child>
+ <child>
+ <widget class="GtkButton" id="digit2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="receives_default">False</property>
+ <property name="focus_on_click">False</property>
+ <signal name="clicked" handler="on_digit_clicked"/>
+ <accelerator key="2" signal="clicked"/>
+ <accelerator key="a" signal="clicked"/>
+ <accelerator key="b" signal="clicked"/>
+ <accelerator key="c" signal="clicked"/>
+ <child>
+ <widget class="GtkLabel" id="label10">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes"><span size="30000" weight="bold">2</span>
+<span size="12000">ABC</span></property>
+ <property name="use_markup">True</property>
+ <property name="justify">center</property>
+ </widget>
+ </child>
+ </widget>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkButton" id="digit3">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="receives_default">False</property>
+ <property name="focus_on_click">False</property>
+ <signal name="clicked" handler="on_digit_clicked"/>
+ <accelerator key="3" signal="clicked"/>
+ <accelerator key="d" signal="clicked"/>
+ <accelerator key="e" signal="clicked"/>
+ <accelerator key="f" signal="clicked"/>
+ <child>
+ <widget class="GtkLabel" id="label11">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes"><span size="30000" weight="bold" stretch="ultraexpanded">3</span>
+<span size="12000">DEF</span></property>
+ <property name="use_markup">True</property>
+ <property name="justify">center</property>
+ </widget>
+ </child>
+ </widget>
+ <packing>
+ <property name="left_attach">2</property>
+ <property name="right_attach">3</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkButton" id="digit4">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="receives_default">False</property>
+ <property name="focus_on_click">False</property>
+ <signal name="clicked" handler="on_digit_clicked"/>
+ <accelerator key="4" signal="clicked"/>
+ <accelerator key="g" signal="clicked"/>
+ <accelerator key="h" signal="clicked"/>
+ <accelerator key="i" signal="clicked"/>
+ <child>
+ <widget class="GtkLabel" id="label13">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes"><span size="30000" weight="bold">4</span>
+<span size="12000">GHI</span></property>
+ <property name="use_markup">True</property>
+ <property name="justify">center</property>
+ </widget>
+ </child>
+ </widget>
+ <packing>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkButton" id="digit5">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="receives_default">False</property>
+ <property name="focus_on_click">False</property>
+ <signal name="clicked" handler="on_digit_clicked"/>
+ <accelerator key="5" signal="clicked"/>
+ <accelerator key="j" signal="clicked"/>
+ <accelerator key="k" signal="clicked"/>
+ <accelerator key="l" signal="clicked"/>
+ <child>
+ <widget class="GtkLabel" id="label14">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes"><span size="30000" weight="bold">5</span>
+<span size="12000">JKL</span></property>
+ <property name="use_markup">True</property>
+ <property name="justify">center</property>
+ </widget>
+ </child>
+ </widget>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkButton" id="digit6">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="receives_default">False</property>
+ <property name="focus_on_click">False</property>
+ <signal name="clicked" handler="on_digit_clicked"/>
+ <accelerator key="6" signal="clicked"/>
+ <accelerator key="m" signal="clicked"/>
+ <accelerator key="n" signal="clicked"/>
+ <accelerator key="o" signal="clicked"/>
+ <child>
+ <widget class="GtkLabel" id="label15">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes"><span size="30000" weight="bold">6</span>
+<span size="12000">MNO</span></property>
+ <property name="use_markup">True</property>
+ <property name="justify">center</property>
+ </widget>
+ </child>
+ </widget>
+ <packing>
+ <property name="left_attach">2</property>
+ <property name="right_attach">3</property>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkButton" id="digit7">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="receives_default">False</property>
+ <property name="focus_on_click">False</property>
+ <signal name="clicked" handler="on_digit_clicked"/>
+ <accelerator key="7" signal="clicked"/>
+ <accelerator key="p" signal="clicked"/>
+ <accelerator key="q" signal="clicked"/>
+ <accelerator key="r" signal="clicked"/>
+ <accelerator key="s" signal="clicked"/>
+ <child>
+ <widget class="GtkLabel" id="label16">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes"><span size="30000" weight="bold">7</span>
+<span size="12000">PQRS</span></property>
+ <property name="use_markup">True</property>
+ <property name="justify">center</property>
+ </widget>
+ </child>
+ </widget>
+ <packing>
+ <property name="top_attach">2</property>
+ <property name="bottom_attach">3</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkButton" id="digit8">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="receives_default">False</property>
+ <property name="focus_on_click">False</property>
+ <signal name="clicked" handler="on_digit_clicked"/>
+ <accelerator key="8" signal="clicked"/>
+ <accelerator key="t" signal="clicked"/>
+ <accelerator key="u" signal="clicked"/>
+ <accelerator key="v" signal="clicked"/>
+ <child>
+ <widget class="GtkLabel" id="label17">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes"><span size="30000" weight="bold">8</span>
+<span size="12000">TUV</span></property>
+ <property name="use_markup">True</property>
+ <property name="justify">center</property>
+ </widget>
+ </child>
+ </widget>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">2</property>
+ <property name="bottom_attach">3</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkButton" id="digit9">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="receives_default">False</property>
+ <property name="focus_on_click">False</property>
+ <signal name="clicked" handler="on_digit_clicked"/>
+ <accelerator key="9" signal="clicked"/>
+ <accelerator key="w" signal="clicked"/>
+ <accelerator key="x" signal="clicked"/>
+ <accelerator key="y" signal="clicked"/>
+ <accelerator key="z" signal="clicked"/>
+ <child>
+ <widget class="GtkLabel" id="label18">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes"><span size="30000" weight="bold">9</span>
+<span size="12000">WXYZ</span></property>
+ <property name="use_markup">True</property>
+ <property name="justify">center</property>
+ </widget>
+ </child>
+ </widget>
+ <packing>
+ <property name="left_attach">2</property>
+ <property name="right_attach">3</property>
+ <property name="top_attach">2</property>
+ <property name="bottom_attach">3</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkButton" id="digit0">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="receives_default">False</property>
+ <property name="focus_on_click">False</property>
+ <signal name="clicked" handler="on_digit_clicked"/>
+ <accelerator key="0" signal="clicked"/>
+ <child>
+ <widget class="GtkLabel" id="label19">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes"><span size="33000" weight="bold">0</span>
+<span size="9000"></span></property>
+ <property name="use_markup">True</property>
+ <property name="justify">center</property>
+ </widget>
+ </child>
+ </widget>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">3</property>
+ <property name="bottom_attach">4</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkButton" id="dial">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="receives_default">False</property>
+ <accelerator key="Return" signal="clicked"/>
+ <child>
+ <widget class="GtkHBox" id="hbox1">
+ <property name="visible">True</property>
+ <child>
+ <widget class="GtkImage" id="image1">
+ <property name="visible">True</property>
+ <property name="xalign">1</property>
+ <property name="stock">gtk-yes</property>
+ </widget>
+ <packing>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkLabel" id="label8">
+ <property name="visible">True</property>
+ <property name="xalign">0</property>
+ <property name="xpad">5</property>
+ <property name="label" translatable="yes"><span size="17000" weight="bold">Dial</span></property>
+ <property name="use_markup">True</property>
+ </widget>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </widget>
+ </child>
+ </widget>
+ <packing>
+ <property name="left_attach">2</property>
+ <property name="right_attach">3</property>
+ <property name="top_attach">3</property>
+ <property name="bottom_attach">4</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkButton" id="sms">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <accelerator key="Return" signal="clicked"/>
+ <child>
+ <widget class="GtkHBox" id="hbox2">
+ <property name="visible">True</property>
+ <child>
+ <widget class="GtkImage" id="image2">
+ <property name="visible">True</property>
+ <property name="xalign">1</property>
+ <property name="stock">gtk-file</property>
+ </widget>
+ <packing>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkLabel" id="label1">
+ <property name="visible">True</property>
+ <property name="xalign">0</property>
+ <property name="xpad">5</property>
+ <property name="label" translatable="yes"><span size="17000" weight="bold">SMS</span></property>
+ <property name="use_markup">True</property>
+ </widget>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </widget>
+ </child>
+ </widget>
+ <packing>
+ <property name="top_attach">3</property>
+ <property name="bottom_attach">4</property>
+ </packing>
+ </child>
+ </widget>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </widget>
+ <packing>
+ <property name="tab_expand">True</property>
+ <property name="tab_fill">False</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkLabel" id="keypad">
+ <property name="height_request">30</property>
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">Keypad</property>
+ </widget>
+ <packing>
+ <property name="tab_expand">True</property>
+ <property name="tab_fill">False</property>
+ <property name="type">tab</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkVBox" id="vbox2">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <widget class="GtkScrolledWindow" id="recent_scrolledwindow">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="hscrollbar_policy">never</property>
+ <child>
+ <widget class="GtkTreeView" id="recentview">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="rules_hint">True</property>
+ <property name="enable_grid_lines">horizontal</property>
+ <property name="enable_tree_lines">True</property>
+ </widget>
+ </child>
+ </widget>
+ <packing>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </widget>
+ <packing>
+ <property name="position">1</property>
+ <property name="tab_expand">True</property>
+ <property name="tab_fill">False</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkLabel" id="recent">
+ <property name="height_request">30</property>
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">Recent</property>
+ </widget>
+ <packing>
+ <property name="position">1</property>
+ <property name="tab_expand">True</property>
+ <property name="tab_fill">False</property>
+ <property name="type">tab</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkVBox" id="vbox3">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <widget class="GtkScrolledWindow" id="message_scrolledwindow">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="hscrollbar_policy">never</property>
+ <property name="vscrollbar_policy">automatic</property>
+ <child>
+ <widget class="GtkTreeView" id="messages_view">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="rules_hint">True</property>
+ <property name="enable_grid_lines">horizontal</property>
+ <property name="enable_tree_lines">True</property>
+ </widget>
+ </child>
+ </widget>
+ <packing>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </widget>
+ <packing>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkLabel" id="messages">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">Messages</property>
+ </widget>
+ <packing>
+ <property name="position">2</property>
+ <property name="tab_expand">True</property>
+ <property name="tab_fill">False</property>
+ <property name="type">tab</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkVBox" id="vbox5">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <widget class="GtkButton" id="addressbookSelectButton">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ </widget>
+ <packing>
+ <property name="expand">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkScrolledWindow" id="contacts_scrolledwindow">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="hscrollbar_policy">never</property>
+ <child>
+ <widget class="GtkTreeView" id="contactsview">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="headers_visible">False</property>
+ <property name="rules_hint">True</property>
+ <property name="fixed_height_mode">True</property>
+ <property name="enable_grid_lines">horizontal</property>
+ <property name="enable_tree_lines">True</property>
+ </widget>
+ </child>
+ </widget>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </widget>
+ <packing>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkLabel" id="contacts">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">Contacts</property>
+ </widget>
+ <packing>
+ <property name="position">3</property>
+ <property name="tab_expand">True</property>
+ <property name="tab_fill">False</property>
+ <property name="type">tab</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkTable" id="accountview">
+ <property name="visible">True</property>
+ <property name="border_width">11</property>
+ <property name="n_rows">7</property>
+ <property name="n_columns">2</property>
+ <child>
+ <widget class="GtkLabel" id="gcnumber_display">
+ <property name="visible">True</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">No Number Available</property>
+ <property name="use_markup">True</property>
+ </widget>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkLabel" id="gcnumber_label">
+ <property name="visible">True</property>
+ <property name="xalign">0</property>
+ <property name="yalign">0</property>
+ <property name="ypad">10</property>
+ <property name="label" translatable="yes">Account Number:</property>
+ </widget>
+ <packing>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkLabel" id="callback_number_label">
+ <property name="visible">True</property>
+ <property name="xalign">0</property>
+ <property name="yalign">0</property>
+ <property name="ypad">10</property>
+ <property name="label" translatable="yes">Callback Number:</property>
+ </widget>
+ <packing>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkLabel" id="label4">
+ <property name="visible">True</property>
+ <property name="xalign">0</property>
+ </widget>
+ <packing>
+ <property name="top_attach">2</property>
+ <property name="bottom_attach">3</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkLabel" id="label5">
+ <property name="visible">True</property>
+ <property name="xalign">0</property>
+ </widget>
+ <packing>
+ <property name="top_attach">4</property>
+ <property name="bottom_attach">5</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkVBox" id="vbox1">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <widget class="GtkCheckButton" id="missedCheckbox">
+ <property name="label" translatable="yes">Missed Calls</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="draw_indicator">True</property>
+ </widget>
+ <packing>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkCheckButton" id="voicemailCheckbox">
+ <property name="label" translatable="yes">Voicemail</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="draw_indicator">True</property>
+ </widget>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkCheckButton" id="smsCheckbox">
+ <property name="label" translatable="yes">SMS</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="draw_indicator">True</property>
+ </widget>
+ <packing>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </widget>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">4</property>
+ <property name="bottom_attach">5</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkCheckButton" id="notifyCheckbox">
+ <property name="label" translatable="yes">Notifications</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="yalign">0</property>
+ <property name="draw_indicator">True</property>
+ </widget>
+ <packing>
+ <property name="top_attach">3</property>
+ <property name="bottom_attach">4</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkButton" id="clearcookies">
+ <property name="label" translatable="yes">Login</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="focus_on_click">False</property>
+ </widget>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">6</property>
+ <property name="bottom_attach">7</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkLabel" id="label3">
+ <property name="visible">True</property>
+ <property name="xalign">0</property>
+ </widget>
+ <packing>
+ <property name="top_attach">5</property>
+ <property name="bottom_attach">6</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkButton" id="minutesEntryButton">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ </widget>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">3</property>
+ <property name="bottom_attach">4</property>
+ <property name="y_options">GTK_FILL</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkButton" id="callbackSelectButton">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ </widget>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ </widget>
+ <packing>
+ <property name="position">4</property>
+ <property name="tab_expand">True</property>
+ <property name="tab_fill">False</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkLabel" id="account">
+ <property name="height_request">30</property>
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">Account</property>
+ </widget>
+ <packing>
+ <property name="position">4</property>
+ <property name="tab_fill">False</property>
+ <property name="type">tab</property>
+ </packing>
+ </child>
+ </widget>
+ <packing>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </widget>
+ </child>
+ </widget>
+ <widget class="GtkDialog" id="loginDialog">
+ <property name="border_width">5</property>
+ <property name="title" translatable="yes">Login</property>
+ <property name="resizable">False</property>
+ <property name="modal">True</property>
+ <property name="window_position">center-on-parent</property>
+ <property name="destroy_with_parent">True</property>
+ <property name="type_hint">dialog</property>
+ <property name="skip_taskbar_hint">True</property>
+ <property name="skip_pager_hint">True</property>
+ <property name="deletable">False</property>
+ <property name="transient_for">mainWindow</property>
+ <property name="has_separator">False</property>
+ <child internal-child="vbox">
+ <widget class="GtkVBox" id="loginLayout">
+ <property name="visible">True</property>
+ <property name="spacing">2</property>
+ <child>
+ <widget class="GtkComboBox" id="serviceCombo">
+ <property name="visible">True</property>
+ </widget>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkTable" id="table1">
+ <property name="visible">True</property>
+ <property name="n_rows">2</property>
+ <property name="n_columns">2</property>
+ <child>
+ <widget class="GtkLabel" id="username_label">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">Username</property>
+ </widget>
+ </child>
+ <child>
+ <widget class="GtkLabel" id="password_label">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">Password</property>
+ </widget>
+ <packing>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkEntry" id="usernameentry">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ </widget>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkEntry" id="passwordentry">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="visibility">False</property>
+ </widget>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ </packing>
+ </child>
+ </widget>
+ <packing>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child internal-child="action_area">
+ <widget class="GtkHButtonBox" id="dialog-action_area1">
+ <property name="visible">True</property>
+ <property name="layout_style">end</property>
+ <child>
+ <widget class="GtkButton" id="logins_close_button">
+ <property name="label" translatable="yes">gtk-close</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_stock">True</property>
+ </widget>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkButton" id="loginbutton">
+ <property name="label" translatable="yes">gtk-ok</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="can_default">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_stock">True</property>
+ </widget>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </widget>
+ <packing>
+ <property name="expand">False</property>
+ <property name="pack_type">end</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </widget>
+ </child>
+ </widget>
+ <widget class="GtkDialog" id="phonetype_dialog">
+ <property name="border_width">5</property>
+ <property name="title" translatable="yes">Select Phone Type</property>
+ <property name="modal">True</property>
+ <property name="window_position">center-on-parent</property>
+ <property name="default_width">500</property>
+ <property name="default_height">300</property>
+ <property name="destroy_with_parent">True</property>
+ <property name="type_hint">dialog</property>
+ <property name="skip_taskbar_hint">True</property>
+ <property name="skip_pager_hint">True</property>
+ <property name="has_separator">False</property>
+ <child internal-child="vbox">
+ <widget class="GtkVBox" id="phoneTypeLayout">
+ <property name="visible">True</property>
+ <property name="spacing">2</property>
+ <child>
+ <widget class="GtkVBox" id="vbox1">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <widget class="GtkScrolledWindow" id="phoneSelectionMessages_scrolledwindow">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="hscrollbar_policy">never</property>
+ <property name="vscrollbar_policy">automatic</property>
+ <child>
+ <widget class="GtkTreeView" id="phoneSelectionMessages">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ </widget>
+ </child>
+ </widget>
+ <packing>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </widget>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkHSeparator" id="hseparator1">
+ <property name="visible">True</property>
+ </widget>
+ <packing>
+ <property name="expand">False</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkVBox" id="vbox2">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <widget class="GtkScrolledWindow" id="phonetypes_scrolledwindow">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="hscrollbar_policy">never</property>
+ <property name="vscrollbar_policy">automatic</property>
+ <child>
+ <widget class="GtkTreeView" id="phonetypes">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="rules_hint">True</property>
+ <property name="enable_search">False</property>
+ </widget>
+ </child>
+ </widget>
+ <packing>
+ <property name="expand">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </widget>
+ <packing>
+ <property name="expand">False</property>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ <child internal-child="action_area">
+ <widget class="GtkHButtonBox" id="dialog-action_area3">
+ <property name="visible">True</property>
+ <property name="layout_style">end</property>
+ <child>
+ <widget class="GtkButton" id="sms_button">
+ <property name="label" translatable="yes">SMS</property>
+ <property name="response_id">-4</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ </widget>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkButton" id="dial_button">
+ <property name="label" translatable="yes">Dial</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ </widget>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkButton" id="select_button">
+ <property name="label" translatable="yes">Select</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ </widget>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkButton" id="cancel_button">
+ <property name="label" translatable="yes">gtk-cancel</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_stock">True</property>
+ </widget>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ </widget>
+ <packing>
+ <property name="expand">False</property>
+ <property name="pack_type">end</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </widget>
+ </child>
+ </widget>
+ <widget class="GtkDialog" id="smsDialog">
+ <property name="border_width">5</property>
+ <property name="title" translatable="yes">Send SMS</property>
+ <property name="modal">True</property>
+ <property name="window_position">center-on-parent</property>
+ <property name="default_width">500</property>
+ <property name="default_height">300</property>
+ <property name="destroy_with_parent">True</property>
+ <property name="type_hint">dialog</property>
+ <property name="skip_taskbar_hint">True</property>
+ <property name="skip_pager_hint">True</property>
+ <property name="has_separator">False</property>
+ <child internal-child="vbox">
+ <widget class="GtkVBox" id="smsLayout">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">2</property>
+ <child>
+ <widget class="GtkVBox" id="vbox1">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <widget class="GtkScrolledWindow" id="smsMessages_scrolledwindow">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="hscrollbar_policy">never</property>
+ <property name="vscrollbar_policy">automatic</property>
+ <child>
+ <widget class="GtkTreeView" id="smsMessages">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ </widget>
+ </child>
+ </widget>
+ <packing>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </widget>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkHSeparator" id="hseparator1">
+ <property name="visible">True</property>
+ </widget>
+ <packing>
+ <property name="expand">False</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkVBox" id="vbox2">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <widget class="GtkScrolledWindow" id="smsMessage_scrolledEntry">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="hscrollbar_policy">never</property>
+ <property name="vscrollbar_policy">automatic</property>
+ <child>
+ <widget class="GtkTextView" id="smsEntry">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="wrap_mode">word</property>
+ </widget>
+ </child>
+ </widget>
+ <packing>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </widget>
+ <packing>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkHBox" id="smsCountBox">
+ <property name="visible">True</property>
+ <child>
+ <widget class="GtkLabel" id="smsLetterCount1">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">Letters Left:</property>
+ <property name="use_markup">True</property>
+ </widget>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkLabel" id="smsLetterCount">
+ <property name="visible">True</property>
+ <property name="use_markup">True</property>
+ </widget>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </widget>
+ <packing>
+ <property name="expand">False</property>
+ <property name="position">4</property>
+ </packing>
+ </child>
+ <child internal-child="action_area">
+ <widget class="GtkHButtonBox" id="dialog-action_area3">
+ <property name="visible">True</property>
+ <property name="layout_style">end</property>
+ <child>
+ <widget class="GtkButton" id="sendSmsButton">
+ <property name="label" translatable="yes">Send</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ </widget>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkButton" id="cancelSmsButton">
+ <property name="label" translatable="yes">gtk-cancel</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_stock">True</property>
+ </widget>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </widget>
+ <packing>
+ <property name="expand">False</property>
+ <property name="pack_type">end</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </widget>
+ </child>
+ </widget>
+</glade-interface>
--- /dev/null
+#!/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()
--- /dev/null
+#!/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()
--- /dev/null
+#!/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
--- /dev/null
+#!/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'), (<type 'exceptions.RuntimeError'>, 'Goodbye'), (None, 'Meh'), (<type 'exceptions.GeneratorExit'>, 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()
--- /dev/null
+#!/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"""<a href="/voice/m/contact/(\d+)">(.*?)</a>""", re.S)
+ _contactsNextRe = re.compile(r""".*<a href="/voice/m/contacts(\?p=\d+)">Next.*?</a>""", 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"""<div.*?>([0-9+\-\(\) \t]+?)<span.*?>\((\w+)\)</span>""", 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"""<input.*?name="_rnr_se".*?value="(.*?)"\s*/>""")
+ _accountNumRe = re.compile(r"""<b class="ms\d">(.{14})</b></div>""")
+ _callbackRe = re.compile(r"""\s+(.*?):\s*(.*?)<br\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*<div id="(\w+)"\s* class=".*?gc-message.*?">""", re.MULTILINE | re.DOTALL)
+ _exactVoicemailTimeRegex = re.compile(r"""<span class="gc-message-time">(.*?)</span>""", re.MULTILINE)
+ _relativeVoicemailTimeRegex = re.compile(r"""<span class="gc-message-relative">(.*?)</span>""", re.MULTILINE)
+ _voicemailNameRegex = re.compile(r"""<a class=.*?gc-message-name-link.*?>(.*?)</a>""", re.MULTILINE | re.DOTALL)
+ _voicemailNumberRegex = re.compile(r"""<input type="hidden" class="gc-text gc-quickcall-ac" value="(.*?)"/>""", re.MULTILINE)
+ _prettyVoicemailNumberRegex = re.compile(r"""<span class="gc-message-type">(.*?)</span>""", re.MULTILINE)
+ _voicemailLocationRegex = re.compile(r"""<span class="gc-message-location">.*?<a.*?>(.*?)</a></span>""", re.MULTILINE)
+ #_voicemailMessageRegex = re.compile(r"""<span id="\d+-\d+" class="gc-word-(.*?)">(.*?)</span>""", re.MULTILINE)
+ #_voicemailMessageRegex = re.compile(r"""<a .*? class="gc-message-mni">(.*?)</a>""", re.MULTILINE)
+ _voicemailMessageRegex = re.compile(r"""(<span id="\d+-\d+" class="gc-word-(.*?)">(.*?)</span>|<a .*? class="gc-message-mni">(.*?)</a>)""", 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": "<i>%s</i>",
+ "med2": "%s",
+ "high": "<b>%s</b>",
+ }
+ 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"""<span class="gc-message-sms-from">(.*?)</span>""", re.MULTILINE | re.DOTALL)
+ _smsTextRegex = re.compile(r"""<span class="gc-message-sms-time">(.*?)</span>""", re.MULTILINE | re.DOTALL)
+ _smsTimeRegex = re.compile(r"""<span class="gc-message-sms-text">(.*?)</span>""", 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 = [
+ "<b>%s</b>: %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
--- /dev/null
+#!/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("<span size='30000' weight='bold'>%s</span>" % (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("<span size='23000' weight='bold'>%s</span>" % (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 = "<b>%s - %s</b> <i>(%s)</i>" % (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()
--- /dev/null
+#!/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)
--- /dev/null
+#!/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()
--- /dev/null
+#!/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 []
--- /dev/null
+#!/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
--- /dev/null
+#!/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])
--- /dev/null
+[Desktop Entry]
+Encoding=UTF-8
+Version=1.0
+Type=Application
+Name=DialCentral
+Exec=/usr/bin/run-standalone.sh /usr/bin/dialcentral.py
+Icon=dialcentral
--- /dev/null
+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
--- /dev/null
+#!/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)
--- /dev/null
+# 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 <file or directory> 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'
--- /dev/null
+#!/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);
--- /dev/null
+#!/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);
--- /dev/null
+familyname,phone,addr1,addr2,addr3,addr4,name1,name2,name3,name4\r
+"Last, First","555-123-4567","1234 Foo St","Austin, Texas 78727","","","First <name@domain.com>"\r
--- /dev/null
+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\r
+First Last,name@domain.com,"Categories: Others\r
+\r
+\r
+Categories: Others",Other,name2@domain.com,,,,,,,,,,Personal,name3@domain.com; name4@domain.com; name5@domain.com; name6@domain.com,,17471234567,5551234567,,,,,,\r
+First1 Last,,"Categories: Friends\r
+",Personal,,,,5557654321,,,,,,\r
--- /dev/null
+Name,E-mail Address,Categories,Company,Job Title,Home Address,Home Phone,Mobile Phone,Business Phone,GrandCentral,Suffix,Title,Initials,Web Page,Notes\r
+First Last,,Family,,,1234567 Foo St Austn Tx 78727,5559983254,5554023626,5559988899,,,,,,\r
+First1 Last,,Others,,,,5556835460,,,,,,,,\r
--- /dev/null
+[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]
+
--- /dev/null
+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
--- /dev/null
+#!/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
+ )
--- /dev/null
+#!/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)
--- /dev/null
+#!/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
+ )
--- /dev/null
+#!/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)
--- /dev/null
+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()
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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()
--- /dev/null
+#!/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 "<doctest libraries.recipes.context.expected[1]>", line 3, in <module>
+ 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()