Baseline for work is Dialcentral 1.0.6-10
authorEd Page <eopage@byu.net>
Fri, 25 Sep 2009 18:00:41 +0000 (13:00 -0500)
committerEd Page <eopage@byu.net>
Fri, 25 Sep 2009 18:00:41 +0000 (13:00 -0500)
49 files changed:
LICENSE [new file with mode: 0644]
Makefile [new file with mode: 0644]
README [new file with mode: 0644]
TODO [new file with mode: 0644]
src/__init__.py [new file with mode: 0644]
src/alarm_handler.py [new file with mode: 0644]
src/alarm_notify.py [new file with mode: 0755]
src/browser_emu.py [new file with mode: 0644]
src/constants.py [new file with mode: 0644]
src/dc_glade.py [new file with mode: 0755]
src/dialcentral.glade [new file with mode: 0644]
src/dialcentral.py [new file with mode: 0755]
src/example_custom_notifier.py [new file with mode: 0755]
src/file_backend.py [new file with mode: 0644]
src/gtk_toolbox.py [new file with mode: 0644]
src/gv_backend.py [new file with mode: 0644]
src/gv_views.py [new file with mode: 0644]
src/hildonize.py [new file with mode: 0755]
src/led_handler.py [new file with mode: 0755]
src/null_backend.py [new file with mode: 0644]
src/null_views.py [new file with mode: 0644]
support/builddeb.py [new file with mode: 0755]
support/dialcentral.desktop [new file with mode: 0644]
support/fake_py2deb.py [new file with mode: 0644]
support/icons/hicolor/26x26/hildon/dialcentral.png [new file with mode: 0644]
support/icons/hicolor/64x64/hildon/dialcentral.png [new file with mode: 0644]
support/icons/hicolor/scalable/hildon/dialcentral.png [new file with mode: 0644]
support/py2deb.py [new file with mode: 0644]
support/pylint.rc [new file with mode: 0644]
support/test_syntax.py [new file with mode: 0755]
support/todo.py [new file with mode: 0755]
tests/basic_data/basic.csv [new file with mode: 0644]
tests/basic_data/empty.csv [new file with mode: 0644]
tests/basic_data/google.csv [new file with mode: 0644]
tests/basic_data/grandcentral.csv [new file with mode: 0644]
tests/basic_data/settings.ini [new file with mode: 0644]
tests/dummy_hildon/__init__.py [new file with mode: 0644]
tests/dummy_hildon/hildon.py [new file with mode: 0644]
tests/gc_samples/__init__.py [new file with mode: 0644]
tests/gc_samples/dump_cookies.py [new file with mode: 0755]
tests/gc_samples/generate_gc_samples.py [new file with mode: 0755]
tests/gv_samples/__init__.py [new file with mode: 0644]
tests/gv_samples/dump_cookies.py [new file with mode: 0755]
tests/gv_samples/generate_gv_samples.py [new file with mode: 0755]
tests/test_file_backend.py [new file with mode: 0644]
tests/test_gc_backend.py [new file with mode: 0644]
tests/test_gv_backend.py [new file with mode: 0644]
tests/test_startup.py [new file with mode: 0644]
tests/test_utils.py [new file with mode: 0644]

diff --git a/LICENSE b/LICENSE
new file mode 100644 (file)
index 0000000..5ab7695
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,504 @@
+                 GNU LESSER GENERAL PUBLIC LICENSE
+                      Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+ 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL.  It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+                           Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+  This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it.  You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+  When we speak of free software, we are referring to freedom of use,
+not price.  Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+  To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights.  These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+  For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you.  You must make sure that they, too, receive or can get the source
+code.  If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it.  And you must show them these terms so they know their rights.
+
+  We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+  To protect each distributor, we want to make it very clear that
+there is no warranty for the free library.  Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+\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!
+
+
diff --git a/Makefile b/Makefile
new file mode 100644 (file)
index 0000000..9721ca8
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,102 @@
+PROJECT_NAME=dialcentral
+SOURCE_PATH=src
+SOURCE=$(shell find $(SOURCE_PATH) -iname "*.py")
+PROGRAM=$(SOURCE_PATH)/$(PROJECT_NAME).py
+DATA_TYPES=*.ini *.map *.glade *.png
+DATA=$(foreach type, $(DATA_TYPES), $(shell find $(SOURCE_PATH) -iname "$(type)"))
+OBJ=$(SOURCE:.py=.pyc)
+BUILD_PATH=./build
+TAG_FILE=~/.ctags/$(PROJECT_NAME).tags
+TODO_FILE=./TODO
+
+DEBUGGER=winpdb
+UNIT_TEST=nosetests --with-doctest -w .
+SYNTAX_TEST=support/test_syntax.py
+STYLE_TEST=../../Python/tools/pep8.py --ignore=W191,E501
+LINT_RC=./support/pylint.rc
+LINT=pylint --rcfile=$(LINT_RC)
+PROFILE_GEN=python -m cProfile -o .profile
+PROFILE_VIEW=python -m pstats .profile
+TODO_FINDER=support/todo.py
+CTAGS=ctags-exuberant
+
+.PHONY: all run profile debug test build lint tags todo clean distclean
+
+all: test
+
+run: $(OBJ)
+       $(SOURCE_PATH)/dc_glade.py
+
+profile: $(OBJ)
+       $(PROFILE_GEN) $(PROGRAM)
+       $(PROFILE_VIEW)
+
+debug: $(OBJ)
+       $(DEBUGGER) $(PROGRAM)
+
+test: $(OBJ)
+       $(UNIT_TEST)
+
+package: $(OBJ)
+       rm -Rf $(BUILD_PATH)
+       mkdir -p $(BUILD_PATH)/generic
+       cp $(SOURCE_PATH)/constants.py  $(BUILD_PATH)/generic
+       cp $(SOURCE_PATH)/$(PROJECT_NAME).py  $(BUILD_PATH)/generic
+       $(foreach file, $(DATA), cp $(file) $(BUILD_PATH)/generic/$(subst /,-,$(file)) ; )
+       $(foreach file, $(SOURCE), cp $(file) $(BUILD_PATH)/generic/$(subst /,-,$(file)) ; )
+       #$(foreach file, $(OBJ), cp $(file) $(BUILD_PATH)/generic/$(subst /,-,$(file)) ; )
+       cp support/$(PROJECT_NAME).desktop $(BUILD_PATH)/generic
+       cp support/icons/hicolor/26x26/hildon/$(PROJECT_NAME).png $(BUILD_PATH)/generic/26x26-$(PROJECT_NAME).png
+       cp support/icons/hicolor/64x64/hildon/$(PROJECT_NAME).png $(BUILD_PATH)/generic/64x64-$(PROJECT_NAME).png
+       cp support/icons/hicolor/scalable/hildon/$(PROJECT_NAME).png $(BUILD_PATH)/generic/scale-$(PROJECT_NAME).png
+       cp support/builddeb.py $(BUILD_PATH)/generic
+       cp support/py2deb.py $(BUILD_PATH)/generic
+       cp support/fake_py2deb.py $(BUILD_PATH)/generic
+       mkdir -p $(BUILD_PATH)/chinook
+       cp -R $(BUILD_PATH)/generic/* $(BUILD_PATH)/chinook
+       cd $(BUILD_PATH)/chinook ; python builddeb.py chinook
+       mkdir -p $(BUILD_PATH)/diablo
+       cp -R $(BUILD_PATH)/generic/* $(BUILD_PATH)/diablo
+       cd $(BUILD_PATH)/diablo ; python builddeb.py diablo
+       mkdir -p $(BUILD_PATH)/fremantle
+       cp -R $(BUILD_PATH)/generic/* $(BUILD_PATH)/fremantle
+       cd $(BUILD_PATH)/fremantle ; python builddeb.py fremantle
+       mkdir -p $(BUILD_PATH)/mer
+       cp -R $(BUILD_PATH)/generic/* $(BUILD_PATH)/mer
+       cd $(BUILD_PATH)/mer ; python builddeb.py mer
+
+lint: $(OBJ)
+       $(foreach file, $(SOURCE), $(LINT) $(file) ; )
+
+tags: $(TAG_FILE) 
+
+todo: $(TODO_FILE)
+
+clean:
+       rm -Rf $(OBJ)
+       rm -Rf $(BUILD_PATH)
+       rm -Rf $(TODO_FILE)
+
+distclean:
+       rm -Rf $(OBJ)
+       rm -Rf $(BUILD_PATH)
+       rm -Rf $(TAG_FILE)
+       find $(SOURCE_PATH) -name "*.*~" | xargs rm -f
+       find $(SOURCE_PATH) -name "*.swp" | xargs rm -f
+       find $(SOURCE_PATH) -name "*.bak" | xargs rm -f
+       find $(SOURCE_PATH) -name ".*.swp" | xargs rm -f
+
+$(TAG_FILE): $(OBJ)
+       mkdir -p $(dir $(TAG_FILE))
+       $(CTAGS) -o $(TAG_FILE) $(SOURCE)
+
+$(TODO_FILE): $(SOURCE)
+       @- $(TODO_FINDER) $(SOURCE) > $(TODO_FILE)
+
+%.pyc: %.py
+       $(SYNTAX_TEST) $<
+
+#Makefile Debugging
+#Target to print any variable, can be added to the dependencies of any other target
+#Userfule flags for make, -d, -p, -n
+print-%: ; @$(error $* is $($*) ($(value $*)) (from $(origin $*)))
diff --git a/README b/README
new file mode 100644 (file)
index 0000000..b87ce9c
--- /dev/null
+++ b/README
@@ -0,0 +1,37 @@
+Building a package
+===================
+Run
+       make PLATFORM=... package
+which will create a "./pkg-.../..." heirarchy.  Move this structure to somewhere on the tablet, then run pypackager. 
+
+Supported PLATFORMs include
+       desktop
+       os2007
+       os2008
+
+SDK Enviroment
+===================
+
+Native
+
+Follow install instructions
+       Ubuntu: http://www.linuxuk.org/node/38
+Install Nokia stuff (for each target)
+       fakeroot apt-get install maemo-explicit
+
+Userful commands
+Login
+       /scratchbox/login
+Change targets
+       sb-conf select DIABLO_ARMEL
+       sb-conf select DIABLO_X86
+Fixing it
+       fakeroot apt-get -f install
+
+Starting scratchbox
+       Xephyr :2 -host-cursor -screen 800x480x16 -dpi 96 -ac -extension Composite
+       scratchbox
+       export DISPLAY=:2
+       af-sb-init.sh start
+Then running a command in the "Maemo" terminal will launch it in the Xephyr session
+       Tip: run with "run-standalone.sh" for niceness?
diff --git a/TODO b/TODO
new file mode 100644 (file)
index 0000000..25bc1a1
--- /dev/null
+++ b/TODO
@@ -0,0 +1,5 @@
+src/gv_views.py:
+21: @todo Alternate UI for dialogs (stackables)
+416: @todo Add multi-SMS messages like GoogleVoice
+
+
diff --git a/src/__init__.py b/src/__init__.py
new file mode 100644 (file)
index 0000000..4265cc3
--- /dev/null
@@ -0,0 +1 @@
+#!/usr/bin/env python
diff --git a/src/alarm_handler.py b/src/alarm_handler.py
new file mode 100644 (file)
index 0000000..d3a9bb9
--- /dev/null
@@ -0,0 +1,149 @@
+#!/usr/bin/env python
+
+import os
+import time
+import datetime
+import ConfigParser
+
+import dbus
+import osso.alarmd as alarmd
+
+
+class AlarmHandler(object):
+
+       _INVALID_COOKIE = -1
+       _TITLE = "Dialcentral Notifications"
+       _LAUNCHER = os.path.abspath(os.path.join(os.path.dirname(__file__), "alarm_notify.py"))
+       _REPEAT_FOREVER = -1
+       _DEFAULT_FLAGS = (
+               alarmd.ALARM_EVENT_NO_DIALOG |
+               alarmd.ALARM_EVENT_NO_SNOOZE |
+               alarmd.ALARM_EVENT_CONNECTED
+       )
+
+       def __init__(self):
+               self._recurrence = 5
+
+               bus = dbus.SystemBus()
+               self._alarmdDBus = bus.get_object("com.nokia.alarmd", "/com/nokia/alarmd");
+               self._alarmCookie = self._INVALID_COOKIE
+               self._launcher = self._LAUNCHER
+
+       def load_settings(self, config, sectionName):
+               try:
+                       self._recurrence = config.getint(sectionName, "recurrence")
+                       self._alarmCookie = config.getint(sectionName, "alarmCookie")
+                       launcher = config.get(sectionName, "notifier")
+                       if launcher:
+                               self._launcher = launcher
+               except ConfigParser.NoOptionError:
+                       pass
+
+       def save_settings(self, config, sectionName):
+               config.set(sectionName, "recurrence", str(self._recurrence))
+               config.set(sectionName, "alarmCookie", str(self._alarmCookie))
+               launcher = self._launcher if self._launcher != self._LAUNCHER else ""
+               config.set(sectionName, "notifier", launcher)
+
+       def apply_settings(self, enabled, recurrence):
+               if recurrence != self._recurrence or enabled != self.isEnabled:
+                       if self.isEnabled:
+                               self._clear_alarm()
+                       if enabled:
+                               self._set_alarm(recurrence)
+               self._recurrence = int(recurrence)
+
+       @property
+       def recurrence(self):
+               return self._recurrence
+
+       @property
+       def isEnabled(self):
+               return self._alarmCookie != self._INVALID_COOKIE
+
+       def _get_start_time(self, recurrence):
+               now = datetime.datetime.now()
+               startTimeMinute = now.minute + max(recurrence, 5) # being safe
+               startTimeHour = now.hour + int(startTimeMinute / 60)
+               startTimeMinute = startTimeMinute % 59
+               now.replace(minute=startTimeMinute)
+               timestamp = int(time.mktime(now.timetuple()))
+               return timestamp
+
+       def _set_alarm(self, recurrence):
+               assert 1 <= recurrence, "Notifications set to occur too frequently: %d" % recurrence
+               alarmTime = self._get_start_time(recurrence)
+
+               #Setup the alarm arguments so that they can be passed to the D-Bus add_event method
+               action = []
+               action.extend(['flags', self._DEFAULT_FLAGS])
+               action.extend(['title', self._TITLE])
+               action.extend(['path', self._launcher])
+               action.extend([
+                       'arguments',
+                       dbus.Array(
+                               [alarmTime, int(27)],
+                               signature=dbus.Signature('v')
+                       )
+               ])  #int(27) used in place of alarm_index
+
+               event = []
+               event.extend([dbus.ObjectPath('/AlarmdEventRecurring'), dbus.UInt32(4)])
+               event.extend(['action', dbus.ObjectPath('/AlarmdActionExec')])  #use AlarmdActionExec instead of AlarmdActionDbus
+               event.append(dbus.UInt32(len(action) / 2))
+               event.extend(action)
+               event.extend(['time', dbus.Int64(alarmTime)])
+               event.extend(['recurr_interval', dbus.UInt32(recurrence)])
+               event.extend(['recurr_count', dbus.Int32(self._REPEAT_FOREVER)])
+
+               self._alarmCookie = self._alarmdDBus.add_event(*event);
+
+       def _clear_alarm(self):
+               if self._alarmCookie == self._INVALID_COOKIE:
+                       return
+               deleteResult = self._alarmdDBus.del_event(dbus.Int32(self._alarmCookie))
+               self._alarmCookie = self._INVALID_COOKIE
+               assert deleteResult != -1, "Deleting of alarm event failed"
+
+
+def main():
+       import ConfigParser
+       import constants
+       try:
+               import optparse
+       except ImportError:
+               return
+
+       parser = optparse.OptionParser()
+       parser.add_option("-x", "--display", action="store_true", dest="display", help="Display data")
+       parser.add_option("-e", "--enable", action="store_true", dest="enabled", help="Whether the alarm should be enabled or not", default=False)
+       parser.add_option("-d", "--disable", action="store_false", dest="enabled", help="Whether the alarm should be enabled or not", default=False)
+       parser.add_option("-r", "--recurrence", action="store", type="int", dest="recurrence", help="How often the alarm occurs", default=5)
+       (commandOptions, commandArgs) = parser.parse_args()
+
+       alarmHandler = AlarmHandler()
+       config = ConfigParser.SafeConfigParser()
+       config.read(constants._user_settings_)
+       alarmHandler.load_settings(config, "alarm")
+
+       if commandOptions.display:
+               print "Alarm (%s) is %s for every %d minutes" % (
+                       alarmHandler._alarmCookie,
+                       "enabled" if alarmHandler.isEnabled else "disabled",
+                       alarmHandler.recurrence,
+               )
+       else:
+               isEnabled = commandOptions.enabled
+               recurrence = commandOptions.recurrence
+               alarmHandler.apply_settings(isEnabled, recurrence)
+
+               alarmHandler.save_settings(config, "alarm")
+               configFile = open(constants._user_settings_, "wb")
+               try:
+                       config.write(configFile)
+               finally:
+                       configFile.close()
+
+
+if __name__ == "__main__":
+       main()
diff --git a/src/alarm_notify.py b/src/alarm_notify.py
new file mode 100755 (executable)
index 0000000..9cb42b8
--- /dev/null
@@ -0,0 +1,140 @@
+#!/usr/bin/env python
+
+import os
+import filecmp
+import ConfigParser
+import pprint
+
+import constants
+import gv_backend
+
+
+def get_missed(backend):
+       missedPage = backend._browser.download(backend._missedCallsURL)
+       missedJson = backend._grab_json(missedPage)
+       return missedJson
+
+
+def get_voicemail(backend):
+       voicemailPage = backend._browser.download(backend._voicemailURL)
+       voicemailJson = backend._grab_json(voicemailPage)
+       return voicemailJson
+
+
+def get_sms(backend):
+       smsPage = backend._browser.download(backend._smsURL)
+       smsJson = backend._grab_json(smsPage)
+       return smsJson
+
+
+def remove_reltime(data):
+       for messageData in data["messages"].itervalues():
+               del messageData["relativeStartTime"]
+
+
+def is_type_changed(backend, type, get_material):
+       jsonMaterial = get_material(backend)
+       unreadCount = jsonMaterial["unreadCounts"][type]
+
+       previousSnapshotPath = os.path.join(constants._data_path_, "snapshot_%s.old.json" % type)
+       currentSnapshotPath = os.path.join(constants._data_path_, "snapshot_%s.json" % type)
+
+       try:
+               os.remove(previousSnapshotPath)
+       except OSError, e:
+               # check if failed purely because the old file didn't exist, which is fine
+               if e.errno != 2:
+                       raise
+       try:
+               os.rename(currentSnapshotPath, previousSnapshotPath)
+               previousExists = True
+       except OSError, e:
+               # check if failed purely because the new old file didn't exist, which is fine
+               if e.errno != 2:
+                       raise
+               previousExists = False
+
+       remove_reltime(jsonMaterial)
+       textMaterial = pprint.pformat(jsonMaterial)
+       currentSnapshot = file(currentSnapshotPath, "w")
+       try:
+               currentSnapshot.write(textMaterial)
+       finally:
+               currentSnapshot.close()
+
+       if unreadCount == 0 or not previousExists:
+               return False
+
+       seemEqual = filecmp.cmp(previousSnapshotPath, currentSnapshotPath)
+       return not seemEqual
+
+
+def is_changed():
+       gvCookiePath = os.path.join(constants._data_path_, "gv_cookies.txt")
+       backend = gv_backend.GVDialer(gvCookiePath)
+
+       loggedIn = False
+
+       if not loggedIn:
+               loggedIn = backend.is_authed()
+
+       config = ConfigParser.SafeConfigParser()
+       config.read(constants._user_settings_)
+       if not loggedIn:
+               import base64
+               try:
+                       blobs = (
+                               config.get(constants.__pretty_app_name__, "bin_blob_%i" % i)
+                               for i in xrange(2)
+                       )
+                       creds = (
+                               base64.b64decode(blob)
+                               for blob in blobs
+                       )
+                       username, password = tuple(creds)
+                       loggedIn = backend.login(username, password)
+               except ConfigParser.NoOptionError, e:
+                       pass
+               except ConfigParser.NoSectionError, e:
+                       pass
+
+       try:
+               notifyOnMissed = config.getboolean("2 - Account Info", "notifyOnMissed")
+               notifyOnVoicemail = config.getboolean("2 - Account Info", "notifyOnVoicemail")
+               notifyOnSms = config.getboolean("2 - Account Info", "notifyOnSms")
+       except ConfigParser.NoOptionError, e:
+               notifyOnMissed = False
+               notifyOnVoicemail = False
+               notifyOnSms = False
+       except ConfigParser.NoSectionError, e:
+               notifyOnMissed = False
+               notifyOnVoicemail = False
+               notifyOnSms = False
+
+       assert loggedIn
+       notifySources = []
+       if notifyOnMissed:
+               notifySources.append(("missed", get_missed))
+       if notifyOnVoicemail:
+               notifySources.append(("voicemail", get_voicemail))
+       if notifyOnSms:
+               notifySources.append(("sms", get_sms))
+
+       notifyUser = False
+       for type, get_material in notifySources:
+               if is_type_changed(backend, type, get_material):
+                       notifyUser = True
+       return notifyUser
+
+
+def notify_on_change():
+       notifyUser = is_changed()
+
+       if notifyUser:
+               import led_handler
+               led = led_handler.LedHandler()
+               led.on()
+
+
+if __name__ == "__main__":
+       notify_on_change()
diff --git a/src/browser_emu.py b/src/browser_emu.py
new file mode 100644 (file)
index 0000000..88a0b62
--- /dev/null
@@ -0,0 +1,166 @@
+"""
+@author:         Laszlo Nagy
+@copyright:   (c) 2005 by Szoftver Messias Bt.
+@licence:       BSD style
+
+Objects of the MozillaEmulator class can emulate a browser that is capable of:
+
+       - cookie management
+       - configurable user agent string
+       - GET and POST
+       - multipart POST (send files)
+       - receive content into file
+
+I have seen many requests on the python mailing list about how to emulate a browser. I'm using this class for years now, without any problems. This is how you can use it:
+
+       1. Use firefox
+       2. Install and open the livehttpheaders plugin
+       3. Use the website manually with firefox
+       4. Check the GET and POST requests in the livehttpheaders capture window
+       5. Create an instance of the above class and send the same GET and POST requests to the server.
+
+Optional steps:
+
+       - You can change user agent string in the build_opened method
+       - The "encode_multipart_formdata" function can be used alone to create POST data from a list of field values and files
+"""
+
+import urllib2
+import cookielib
+import logging
+
+import socket
+
+
+socket.setdefaulttimeout(10)
+
+
+class MozillaEmulator(object):
+
+       def __init__(self, trycount = 1):
+               """Create a new MozillaEmulator object.
+
+               @param trycount: The download() method will retry the operation if it fails. You can specify -1 for infinite retrying.
+                        A value of 0 means no retrying. A value of 1 means one retry. etc."""
+               self.cookies = cookielib.LWPCookieJar()
+               self.debug = False
+               self.trycount = trycount
+
+       def build_opener(self, url, postdata = None, extraheaders = None, forbid_redirect = False):
+               if extraheaders is None:
+                       extraheaders = {}
+
+               txheaders = {
+                       'Accept': 'text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png',
+                       'Accept-Language': 'en,en-us;q=0.5',
+                       'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',
+               }
+               for key, value in extraheaders.iteritems():
+                       txheaders[key] = value
+               req = urllib2.Request(url, postdata, txheaders)
+               self.cookies.add_cookie_header(req)
+               if forbid_redirect:
+                       redirector = HTTPNoRedirector()
+               else:
+                       redirector = urllib2.HTTPRedirectHandler()
+
+               http_handler = urllib2.HTTPHandler(debuglevel=self.debug)
+               https_handler = urllib2.HTTPSHandler(debuglevel=self.debug)
+
+               u = urllib2.build_opener(
+                       http_handler,
+                       https_handler,
+                       urllib2.HTTPCookieProcessor(self.cookies),
+                       redirector
+               )
+               u.addheaders = [(
+                       'User-Agent',
+                       'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7.8) Gecko/20050511 Firefox/1.0.4'
+               )]
+               if not postdata is None:
+                       req.add_data(postdata)
+               return (req, u)
+
+       def download(self, url,
+                       postdata = None, extraheaders = None, forbid_redirect = False,
+                       trycount = None, only_head = False,
+               ):
+               """Download an URL with GET or POST methods.
+
+               @param postdata: It can be a string that will be POST-ed to the URL.
+                       When None is given, the method will be GET instead.
+               @param extraheaders: You can add/modify HTTP headers with a dict here.
+               @param forbid_redirect: Set this flag if you do not want to handle
+                       HTTP 301 and 302 redirects.
+               @param trycount: Specify the maximum number of retries here.
+                       0 means no retry on error. Using -1 means infinite retring.
+                       None means the default value (that is self.trycount).
+               @param only_head: Create the openerdirector and return it. In other
+                       words, this will not retrieve any content except HTTP headers.
+
+               @return: The raw HTML page data
+               """
+               logging.warning("Performing download of %s" % url)
+
+               if extraheaders is None:
+                       extraheaders = {}
+               if trycount is None:
+                       trycount = self.trycount
+               cnt = 0
+
+               while True:
+                       try:
+                               req, u = self.build_opener(url, postdata, extraheaders, forbid_redirect)
+                               openerdirector = u.open(req)
+                               if self.debug:
+                                       print req.get_method(), url
+                                       print openerdirector.code, openerdirector.msg
+                                       print openerdirector.headers
+                               self.cookies.extract_cookies(openerdirector, req)
+                               if only_head:
+                                       return openerdirector
+
+                               return self._read(openerdirector, trycount)
+                       except urllib2.URLError:
+                               cnt += 1
+                               if (-1 < trycount) and (trycount < cnt):
+                                       raise
+
+                       # Retry :-)
+                       if self.debug:
+                               print "MozillaEmulator: urllib2.URLError, retryting ", cnt
+
+       def _read(self, openerdirector, trycount):
+               chunks = []
+
+               chunk = openerdirector.read()
+               chunks.append(chunk)
+               #while chunk and cnt < trycount:
+               #       time.sleep(1)
+               #       cnt += 1
+               #       chunk = openerdirector.read()
+               #       chunks.append(chunk)
+
+               data = "".join(chunks)
+
+               if "Content-Length" in openerdirector.info():
+                       assert len(data) == int(openerdirector.info()["Content-Length"]), "The packet header promised %s of data but only was able to read %s of data" % (
+                               openerdirector.info()["Content-Length"],
+                               len(data),
+                       )
+
+               return data
+
+
+class HTTPNoRedirector(urllib2.HTTPRedirectHandler):
+       """This is a custom http redirect handler that FORBIDS redirection."""
+
+       def http_error_302(self, req, fp, code, msg, headers):
+               e = urllib2.HTTPError(req.get_full_url(), code, msg, headers, fp)
+               if e.code in (301, 302):
+                       if 'location' in headers:
+                               newurl = headers.getheaders('location')[0]
+                       elif 'uri' in headers:
+                               newurl = headers.getheaders('uri')[0]
+                       e.newurl = newurl
+               raise e
diff --git a/src/constants.py b/src/constants.py
new file mode 100644 (file)
index 0000000..8d1359b
--- /dev/null
@@ -0,0 +1,9 @@
+import os
+
+__pretty_app_name__ = "DialCentral"
+__app_name__ = "dialcentral"
+__version__ = "1.0.6"
+__build__ = 10
+__app_magic__ = 0xdeadbeef
+_data_path_ = os.path.join(os.path.expanduser("~"), ".dialcentral")
+_user_settings_ = "%s/settings.ini" % _data_path_
diff --git a/src/dc_glade.py b/src/dc_glade.py
new file mode 100755 (executable)
index 0000000..b23521a
--- /dev/null
@@ -0,0 +1,868 @@
+#!/usr/bin/python2.5
+
+"""
+DialCentral - Front end for Google's GoogleVoice service.
+Copyright (C) 2008  Mark Bergman bergman AT merctech DOT com
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 2.1 of the License, or (at your option) any later version.
+
+This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+
+@todo Add "login failed" and "attempting login" notifications
+"""
+
+
+from __future__ import with_statement
+
+import sys
+import gc
+import os
+import threading
+import base64
+import ConfigParser
+import itertools
+import logging
+
+import gtk
+import gtk.glade
+
+import constants
+import hildonize
+import gtk_toolbox
+
+
+def getmtime_nothrow(path):
+       try:
+               return os.path.getmtime(path)
+       except Exception:
+               return 0
+
+
+def display_error_message(msg):
+       error_dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg)
+
+       def close(dialog, response):
+               dialog.destroy()
+       error_dialog.connect("response", close)
+       error_dialog.run()
+
+
+class Dialcentral(object):
+
+       _glade_files = [
+               os.path.join(os.path.dirname(__file__), "dialcentral.glade"),
+               os.path.join(os.path.dirname(__file__), "../lib/dialcentral.glade"),
+               '/usr/lib/dialcentral/dialcentral.glade',
+       ]
+
+       KEYPAD_TAB = 0
+       RECENT_TAB = 1
+       MESSAGES_TAB = 2
+       CONTACTS_TAB = 3
+       ACCOUNT_TAB = 4
+
+       NULL_BACKEND = 0
+       GV_BACKEND = 2
+       BACKENDS = (NULL_BACKEND, GV_BACKEND)
+
+       def __init__(self):
+               self._initDone = False
+               self._connection = None
+               self._osso = None
+               self._clipboard = gtk.clipboard_get()
+
+               self._credentials = ("", "")
+               self._selectedBackendId = self.NULL_BACKEND
+               self._defaultBackendId = self.GV_BACKEND
+               self._phoneBackends = None
+               self._dialpads = None
+               self._accountViews = None
+               self._messagesViews = None
+               self._recentViews = None
+               self._contactsViews = None
+               self._alarmHandler = None
+               self._ledHandler = None
+               self._originalCurrentLabels = []
+
+               for path in self._glade_files:
+                       if os.path.isfile(path):
+                               self._widgetTree = gtk.glade.XML(path)
+                               break
+               else:
+                       display_error_message("Cannot find dialcentral.glade")
+                       gtk.main_quit()
+                       return
+
+               self._window = self._widgetTree.get_widget("mainWindow")
+               self._notebook = self._widgetTree.get_widget("notebook")
+               self._errorDisplay = gtk_toolbox.ErrorDisplay(self._widgetTree)
+               self._credentialsDialog = gtk_toolbox.LoginWindow(self._widgetTree)
+
+               self._isFullScreen = False
+               self._app = hildonize.get_app_class()()
+               self._window = hildonize.hildonize_window(self._app, self._window)
+               hildonize.hildonize_text_entry(self._widgetTree.get_widget("usernameentry"))
+               hildonize.hildonize_password_entry(self._widgetTree.get_widget("passwordentry"))
+
+               for scrollingWidget in (
+                       'recent_scrolledwindow',
+                       'message_scrolledwindow',
+                       'contacts_scrolledwindow',
+                       "phoneSelectionMessages_scrolledwindow",
+                       "smsMessages_scrolledwindow",
+               ):
+                       hildonize.hildonize_scrollwindow(self._widgetTree.get_widget(scrollingWidget))
+               for scrollingWidget in (
+                       "phonetypes_scrolledwindow",
+                       "smsMessage_scrolledEntry",
+               ):
+                       hildonize.hildonize_scrollwindow_with_viewport(self._widgetTree.get_widget(scrollingWidget))
+
+               replacementButtons = [gtk.Button("Test")]
+               menu = hildonize.hildonize_menu(
+                       self._window,
+                       self._widgetTree.get_widget("dialpad_menubar"),
+                       replacementButtons
+               )
+
+               self._window.connect("key-press-event", self._on_key_press)
+               self._window.connect("window-state-event", self._on_window_state_change)
+               if not hildonize.IS_HILDON_SUPPORTED:
+                       logging.warning("No hildonization support")
+
+               hildonize.set_application_title(self._window, "%s" % constants.__pretty_app_name__)
+
+               self._window.connect("destroy", self._on_close)
+               self._window.set_default_size(800, 300)
+               self._window.show_all()
+
+               self._loginSink = gtk_toolbox.threaded_stage(
+                       gtk_toolbox.comap(
+                               self._attempt_login,
+                               gtk_toolbox.null_sink(),
+                       )
+               )
+
+               backgroundSetup = threading.Thread(target=self._idle_setup)
+               backgroundSetup.setDaemon(True)
+               backgroundSetup.start()
+
+       def _idle_setup(self):
+               """
+               If something can be done after the UI loads, push it here so it's not blocking the UI
+               """
+               # Barebones UI handlers
+               try:
+                       import null_backend
+                       import null_views
+
+                       self._phoneBackends = {self.NULL_BACKEND: null_backend.NullDialer()}
+                       with gtk_toolbox.gtk_lock():
+                               self._dialpads = {self.NULL_BACKEND: null_views.Dialpad(self._widgetTree)}
+                               self._accountViews = {self.NULL_BACKEND: null_views.AccountInfo(self._widgetTree)}
+                               self._recentViews = {self.NULL_BACKEND: null_views.RecentCallsView(self._widgetTree)}
+                               self._messagesViews = {self.NULL_BACKEND: null_views.MessagesView(self._widgetTree)}
+                               self._contactsViews = {self.NULL_BACKEND: null_views.ContactsView(self._widgetTree)}
+
+                               self._dialpads[self._selectedBackendId].enable()
+                               self._accountViews[self._selectedBackendId].enable()
+                               self._recentViews[self._selectedBackendId].enable()
+                               self._messagesViews[self._selectedBackendId].enable()
+                               self._contactsViews[self._selectedBackendId].enable()
+               except Exception, e:
+                       with gtk_toolbox.gtk_lock():
+                               self._errorDisplay.push_exception()
+
+               # Setup maemo specifics
+               try:
+                       try:
+                               import osso
+                       except (ImportError, OSError):
+                               osso = None
+                       self._osso = None
+                       if osso is not None:
+                               self._osso = osso.Context(constants.__app_name__, constants.__version__, False)
+                               device = osso.DeviceState(self._osso)
+                               device.set_device_state_callback(self._on_device_state_change, 0)
+                       else:
+                               logging.warning("No device state support")
+
+                       try:
+                               import alarm_handler
+                               self._alarmHandler = alarm_handler.AlarmHandler()
+                       except (ImportError, OSError):
+                               alarm_handler = None
+                       except Exception:
+                               with gtk_toolbox.gtk_lock():
+                                       self._errorDisplay.push_exception()
+                               alarm_handler = None
+                               logging.warning("No notification support")
+                       if hildonize.IS_HILDON_SUPPORTED:
+                               try:
+                                       import led_handler
+                                       self._ledHandler = led_handler.LedHandler()
+                               except Exception, e:
+                                       logging.exception('LED Handling failed: "%s"' % str(e))
+                                       self._ledHandler = None
+                       else:
+                               self._ledHandler = None
+
+                       try:
+                               import conic
+                       except (ImportError, OSError):
+                               conic = None
+                       self._connection = None
+                       if conic is not None:
+                               self._connection = conic.Connection()
+                               self._connection.connect("connection-event", self._on_connection_change, constants.__app_magic__)
+                               self._connection.request_connection(conic.CONNECT_FLAG_NONE)
+                       else:
+                               logging.warning("No connection support")
+               except Exception, e:
+                       with gtk_toolbox.gtk_lock():
+                               self._errorDisplay.push_exception()
+
+               # Setup costly backends
+               try:
+                       import gv_backend
+                       import file_backend
+                       import gv_views
+
+                       try:
+                               os.makedirs(constants._data_path_)
+                       except OSError, e:
+                               if e.errno != 17:
+                                       raise
+                       gvCookiePath = os.path.join(constants._data_path_, "gv_cookies.txt")
+
+                       self._phoneBackends.update({
+                               self.GV_BACKEND: gv_backend.GVDialer(gvCookiePath),
+                       })
+                       with gtk_toolbox.gtk_lock():
+                               unifiedDialpad = gv_views.Dialpad(self._widgetTree, self._errorDisplay)
+                               self._dialpads.update({
+                                       self.GV_BACKEND: unifiedDialpad,
+                               })
+                               self._accountViews.update({
+                                       self.GV_BACKEND: gv_views.AccountInfo(
+                                               self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._alarmHandler, self._errorDisplay
+                                       ),
+                               })
+                               self._accountViews[self.GV_BACKEND].save_everything = self._save_settings
+                               self._recentViews.update({
+                                       self.GV_BACKEND: gv_views.RecentCallsView(
+                                               self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
+                                       ),
+                               })
+                               self._messagesViews.update({
+                                       self.GV_BACKEND: gv_views.MessagesView(
+                                               self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
+                                       ),
+                               })
+                               self._contactsViews.update({
+                                       self.GV_BACKEND: gv_views.ContactsView(
+                                               self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
+                                       ),
+                               })
+
+                       fsContactsPath = os.path.join(constants._data_path_, "contacts")
+                       fileBackend = file_backend.FilesystemAddressBookFactory(fsContactsPath)
+
+                       self._dialpads[self.GV_BACKEND].number_selected = self._select_action
+                       self._recentViews[self.GV_BACKEND].number_selected = self._select_action
+                       self._messagesViews[self.GV_BACKEND].number_selected = self._select_action
+                       self._contactsViews[self.GV_BACKEND].number_selected = self._select_action
+
+                       addressBooks = [
+                               self._phoneBackends[self.GV_BACKEND],
+                               fileBackend,
+                       ]
+                       mergedBook = gv_views.MergedAddressBook(addressBooks, gv_views.MergedAddressBook.advanced_lastname_sorter)
+                       self._contactsViews[self.GV_BACKEND].append(mergedBook)
+                       self._contactsViews[self.GV_BACKEND].extend(addressBooks)
+                       self._contactsViews[self.GV_BACKEND].open_addressbook(*self._contactsViews[self.GV_BACKEND].get_addressbooks().next()[0][0:2])
+
+                       callbackMapping = {
+                               "on_paste": self._on_paste,
+                               "on_refresh": self._on_menu_refresh,
+                               "on_clearcookies_clicked": self._on_clearcookies_clicked,
+                               "on_about_activate": self._on_about_activate,
+                       }
+                       if hildonize.GTK_MENU_USED:
+                               self._widgetTree.signal_autoconnect(callbackMapping)
+                       self._notebook.connect("switch-page", self._on_notebook_switch_page)
+                       self._widgetTree.get_widget("clearcookies").connect("clicked", self._on_clearcookies_clicked)
+
+                       with gtk_toolbox.gtk_lock():
+                               self._originalCurrentLabels = [
+                                       self._notebook.get_tab_label(self._notebook.get_nth_page(pageIndex)).get_text()
+                                       for pageIndex in xrange(self._notebook.get_n_pages())
+                               ]
+                               self._notebookTapHandler = gtk_toolbox.TapOrHold(self._notebook)
+                               self._notebookTapHandler.enable()
+                       self._notebookTapHandler.on_tap = self._reset_tab_refresh
+                       self._notebookTapHandler.on_hold = self._on_tab_refresh
+                       self._notebookTapHandler.on_holding = self._set_tab_refresh
+                       self._notebookTapHandler.on_cancel = self._reset_tab_refresh
+
+                       self._initDone = True
+
+                       config = ConfigParser.SafeConfigParser()
+                       config.read(constants._user_settings_)
+                       with gtk_toolbox.gtk_lock():
+                               self.load_settings(config)
+               except Exception, e:
+                       with gtk_toolbox.gtk_lock():
+                               self._errorDisplay.push_exception()
+               finally:
+                       self._spawn_attempt_login(2)
+
+       def _spawn_attempt_login(self, *args):
+               self._loginSink.send(args)
+
+       def _attempt_login(self, numOfAttempts = 10, force = False):
+               """
+               @note This must be run outside of the UI lock
+               """
+               try:
+                       assert 0 <= numOfAttempts, "That was pointless having 0 or less login attempts"
+                       assert self._initDone, "Attempting login before app is fully loaded"
+
+                       serviceId = self.NULL_BACKEND
+                       loggedIn = False
+                       if not force:
+                               try:
+                                       self.refresh_session()
+                                       serviceId = self._defaultBackendId
+                                       loggedIn = True
+                               except Exception, e:
+                                       logging.exception('Session refresh failed with the following message "%s"' % str(e))
+
+                       if not loggedIn:
+                               loggedIn, serviceId = self._login_by_user(numOfAttempts)
+
+                       with gtk_toolbox.gtk_lock():
+                               self._change_loggedin_status(serviceId)
+                               if loggedIn:
+                                       hildonize.show_information_banner(self._window, "Logged In")
+               except Exception, e:
+                       with gtk_toolbox.gtk_lock():
+                               self._errorDisplay.push_exception()
+
+       def refresh_session(self):
+               """
+               @note Thread agnostic
+               """
+               assert self._initDone, "Attempting login before app is fully loaded"
+
+               loggedIn = False
+               if not loggedIn:
+                       loggedIn = self._login_by_cookie()
+               if not loggedIn:
+                       loggedIn = self._login_by_settings()
+
+               if not loggedIn:
+                       raise RuntimeError("Login Failed")
+
+       def _login_by_cookie(self):
+               """
+               @note Thread agnostic
+               """
+               loggedIn = self._phoneBackends[self._defaultBackendId].is_authed()
+               if loggedIn:
+                       logging.info("Logged into %r through cookies" % self._phoneBackends[self._defaultBackendId])
+               return loggedIn
+
+       def _login_by_settings(self):
+               """
+               @note Thread agnostic
+               """
+               username, password = self._credentials
+               loggedIn = self._phoneBackends[self._defaultBackendId].login(username, password)
+               if loggedIn:
+                       self._credentials = username, password
+                       logging.info("Logged into %r through settings" % self._phoneBackends[self._defaultBackendId])
+               return loggedIn
+
+       def _login_by_user(self, numOfAttempts):
+               """
+               @note This must be run outside of the UI lock
+               """
+               loggedIn, (username, password) = False, self._credentials
+               tmpServiceId = self.GV_BACKEND
+               for attemptCount in xrange(numOfAttempts):
+                       if loggedIn:
+                               break
+                       with gtk_toolbox.gtk_lock():
+                               credentials = self._credentialsDialog.request_credentials(
+                                       defaultCredentials = self._credentials
+                               )
+                               if not self._phoneBackends[tmpServiceId].get_callback_number():
+                                       # subtle reminder to the users to configure things
+                                       self._notebook.set_current_page(self.ACCOUNT_TAB)
+                       username, password = credentials
+                       loggedIn = self._phoneBackends[tmpServiceId].login(username, password)
+
+               if loggedIn:
+                       serviceId = tmpServiceId
+                       self._credentials = username, password
+                       logging.info("Logged into %r through user request" % self._phoneBackends[serviceId])
+               else:
+                       serviceId = self.NULL_BACKEND
+                       self._notebook.set_current_page(self.ACCOUNT_TAB)
+
+               return loggedIn, serviceId
+
+       def _select_action(self, action, number, message):
+               self.refresh_session()
+               if action == "select":
+                       self._dialpads[self._selectedBackendId].set_number(number)
+                       self._notebook.set_current_page(self.KEYPAD_TAB)
+               elif action == "dial":
+                       self._on_dial_clicked(number)
+               elif action == "sms":
+                       self._on_sms_clicked(number, message)
+               else:
+                       assert False, "Unknown action: %s" % action
+
+       def _change_loggedin_status(self, newStatus):
+               oldStatus = self._selectedBackendId
+               if oldStatus == newStatus:
+                       return
+
+               self._dialpads[oldStatus].disable()
+               self._accountViews[oldStatus].disable()
+               self._recentViews[oldStatus].disable()
+               self._messagesViews[oldStatus].disable()
+               self._contactsViews[oldStatus].disable()
+
+               self._dialpads[newStatus].enable()
+               self._accountViews[newStatus].enable()
+               self._recentViews[newStatus].enable()
+               self._messagesViews[newStatus].enable()
+               self._contactsViews[newStatus].enable()
+
+               if self._phoneBackends[self._selectedBackendId].get_callback_number() is None:
+                       self._phoneBackends[self._selectedBackendId].set_sane_callback()
+
+               self._selectedBackendId = newStatus
+
+               self._accountViews[self._selectedBackendId].update()
+               self._refresh_active_tab()
+
+       def load_settings(self, config):
+               """
+               @note UI Thread
+               """
+               try:
+                       self._defaultBackendId = config.getint(constants.__pretty_app_name__, "active")
+                       blobs = (
+                               config.get(constants.__pretty_app_name__, "bin_blob_%i" % i)
+                               for i in xrange(len(self._credentials))
+                       )
+                       creds = (
+                               base64.b64decode(blob)
+                               for blob in blobs
+                       )
+                       self._credentials = tuple(creds)
+
+                       if self._alarmHandler is not None:
+                               self._alarmHandler.load_settings(config, "alarm")
+               except ConfigParser.NoOptionError, e:
+                       logging.exception(
+                               "Settings file %s is missing section %s" % (
+                                       constants._user_settings_,
+                                       e.section,
+                               ),
+                       )
+               except ConfigParser.NoSectionError, e:
+                       logging.exception(
+                               "Settings file %s is missing section %s" % (
+                                       constants._user_settings_,
+                                       e.section,
+                               ),
+                       )
+
+               for backendId, view in itertools.chain(
+                       self._dialpads.iteritems(),
+                       self._accountViews.iteritems(),
+                       self._messagesViews.iteritems(),
+                       self._recentViews.iteritems(),
+                       self._contactsViews.iteritems(),
+               ):
+                       sectionName = "%s - %s" % (backendId, view.name())
+                       try:
+                               view.load_settings(config, sectionName)
+                       except ConfigParser.NoOptionError, e:
+                               logging.exception(
+                                       "Settings file %s is missing section %s" % (
+                                               constants._user_settings_,
+                                               e.section,
+                                       ),
+                               )
+                       except ConfigParser.NoSectionError, e:
+                               logging.exception(
+                                       "Settings file %s is missing section %s" % (
+                                               constants._user_settings_,
+                                               e.section,
+                                       ),
+                               )
+
+               try:
+                       previousOrientation = config.getint(constants.__pretty_app_name__, "orientation")
+                       if previousOrientation == gtk.ORIENTATION_HORIZONTAL:
+                               hildonize.window_to_landscape(self._window)
+                       elif previousOrientation == gtk.ORIENTATION_VERTICAL:
+                               hildonize.window_to_portrait(self._window)
+               except ConfigParser.NoOptionError, e:
+                       logging.exception(
+                               "Settings file %s is missing section %s" % (
+                                       constants._user_settings_,
+                                       e.section,
+                               ),
+                       )
+               except ConfigParser.NoSectionError, e:
+                       logging.exception(
+                               "Settings file %s is missing section %s" % (
+                                       constants._user_settings_,
+                                       e.section,
+                               ),
+                       )
+
+       def save_settings(self, config):
+               """
+               @note Thread Agnostic
+               """
+               config.add_section(constants.__pretty_app_name__)
+               config.set(constants.__pretty_app_name__, "active", str(self._selectedBackendId))
+               config.set(constants.__pretty_app_name__, "orientation", str(int(gtk_toolbox.get_screen_orientation())))
+               for i, value in enumerate(self._credentials):
+                       blob = base64.b64encode(value)
+                       config.set(constants.__pretty_app_name__, "bin_blob_%i" % i, blob)
+               config.add_section("alarm")
+               if self._alarmHandler is not None:
+                       self._alarmHandler.save_settings(config, "alarm")
+
+               for backendId, view in itertools.chain(
+                       self._dialpads.iteritems(),
+                       self._accountViews.iteritems(),
+                       self._messagesViews.iteritems(),
+                       self._recentViews.iteritems(),
+                       self._contactsViews.iteritems(),
+               ):
+                       sectionName = "%s - %s" % (backendId, view.name())
+                       config.add_section(sectionName)
+                       view.save_settings(config, sectionName)
+
+       def _save_settings(self):
+               """
+               @note Thread Agnostic
+               """
+               config = ConfigParser.SafeConfigParser()
+               self.save_settings(config)
+               with open(constants._user_settings_, "wb") as configFile:
+                       config.write(configFile)
+
+       def _refresh_active_tab(self):
+               pageIndex = self._notebook.get_current_page()
+               if pageIndex == self.CONTACTS_TAB:
+                       self._contactsViews[self._selectedBackendId].update(force=True)
+               elif pageIndex == self.RECENT_TAB:
+                       self._recentViews[self._selectedBackendId].update(force=True)
+               elif pageIndex == self.MESSAGES_TAB:
+                       self._messagesViews[self._selectedBackendId].update(force=True)
+
+               if pageIndex in (self.RECENT_TAB, self.MESSAGES_TAB):
+                       if self._ledHandler is not None:
+                               self._ledHandler.off()
+
+       def _on_close(self, *args, **kwds):
+               try:
+                       if self._osso is not None:
+                               self._osso.close()
+
+                       if self._initDone:
+                               self._save_settings()
+               finally:
+                       gtk.main_quit()
+
+       def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
+               """
+               For shutdown or save_unsaved_data, our only state is cookies and I think the cookie manager handles that for us.
+               For system_inactivity, we have no background tasks to pause
+
+               @note Hildon specific
+               """
+               try:
+                       if memory_low:
+                               for backendId in self.BACKENDS:
+                                       self._phoneBackends[backendId].clear_caches()
+                               self._contactsViews[self._selectedBackendId].clear_caches()
+                               gc.collect()
+
+                       if save_unsaved_data or shutdown:
+                               self._save_settings()
+               except Exception, e:
+                       self._errorDisplay.push_exception()
+
+       def _on_connection_change(self, connection, event, magicIdentifier):
+               """
+               @note Hildon specific
+               """
+               try:
+                       import conic
+
+                       status = event.get_status()
+                       error = event.get_error()
+                       iap_id = event.get_iap_id()
+                       bearer = event.get_bearer_type()
+
+                       if status == conic.STATUS_CONNECTED:
+                               if self._initDone:
+                                       self._spawn_attempt_login(2)
+                       elif status == conic.STATUS_DISCONNECTED:
+                               if self._initDone:
+                                       self._defaultBackendId = self._selectedBackendId
+                                       self._change_loggedin_status(self.NULL_BACKEND)
+               except Exception, e:
+                       self._errorDisplay.push_exception()
+
+       def _on_window_state_change(self, widget, event, *args):
+               """
+               @note Hildon specific
+               """
+               try:
+                       if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
+                               self._isFullScreen = True
+                       else:
+                               self._isFullScreen = False
+               except Exception, e:
+                       self._errorDisplay.push_exception()
+
+       def _on_key_press(self, widget, event, *args):
+               """
+               @note Hildon specific
+               """
+               try:
+                       if (
+                               event.keyval == gtk.keysyms.F6 or
+                               event.keyval == gtk.keysyms.Return and event.get_state() & gtk.gdk.CONTROL_MASK
+                       ):
+                               if self._isFullScreen:
+                                       self._window.unfullscreen()
+                               else:
+                                       self._window.fullscreen()
+               except Exception, e:
+                       self._errorDisplay.push_exception()
+
+       def _on_clearcookies_clicked(self, *args):
+               try:
+                       self._phoneBackends[self._selectedBackendId].logout()
+                       self._accountViews[self._selectedBackendId].clear()
+                       self._recentViews[self._selectedBackendId].clear()
+                       self._messagesViews[self._selectedBackendId].clear()
+                       self._contactsViews[self._selectedBackendId].clear()
+                       self._change_loggedin_status(self.NULL_BACKEND)
+
+                       self._spawn_attempt_login(2, True)
+               except Exception, e:
+                       self._errorDisplay.push_exception()
+
+       def _on_notebook_switch_page(self, notebook, page, pageIndex):
+               try:
+                       self._reset_tab_refresh()
+
+                       didRecentUpdate = False
+                       didMessagesUpdate = False
+
+                       if pageIndex == self.RECENT_TAB:
+                               didRecentUpdate = self._recentViews[self._selectedBackendId].update()
+                       elif pageIndex == self.MESSAGES_TAB:
+                               didMessagesUpdate = self._messagesViews[self._selectedBackendId].update()
+                       elif pageIndex == self.CONTACTS_TAB:
+                               self._contactsViews[self._selectedBackendId].update()
+                       elif pageIndex == self.ACCOUNT_TAB:
+                               self._accountViews[self._selectedBackendId].update()
+
+                       if didRecentUpdate or didMessagesUpdate:
+                               if self._ledHandler is not None:
+                                       self._ledHandler.off()
+               except Exception, e:
+                       self._errorDisplay.push_exception()
+
+       def _set_tab_refresh(self, *args):
+               try:
+                       pageIndex = self._notebook.get_current_page()
+                       child = self._notebook.get_nth_page(pageIndex)
+                       self._notebook.get_tab_label(child).set_text("Refresh?")
+               except Exception, e:
+                       self._errorDisplay.push_exception()
+               return False
+
+       def _reset_tab_refresh(self, *args):
+               try:
+                       pageIndex = self._notebook.get_current_page()
+                       child = self._notebook.get_nth_page(pageIndex)
+                       self._notebook.get_tab_label(child).set_text(self._originalCurrentLabels[pageIndex])
+               except Exception, e:
+                       self._errorDisplay.push_exception()
+               return False
+
+       def _on_tab_refresh(self, *args):
+               try:
+                       self._refresh_active_tab()
+                       self._reset_tab_refresh()
+               except Exception, e:
+                       self._errorDisplay.push_exception()
+               return False
+
+       def _on_sms_clicked(self, number, message):
+               try:
+                       assert number, "No number specified"
+                       assert message, "Empty message"
+                       try:
+                               loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
+                       except Exception, e:
+                               loggedIn = False
+                               self._errorDisplay.push_exception()
+                               return
+
+                       if not loggedIn:
+                               self._errorDisplay.push_message(
+                                       "Backend link with GoogleVoice is not working, please try again"
+                               )
+                               return
+
+                       dialed = False
+                       try:
+                               self._phoneBackends[self._selectedBackendId].send_sms(number, message)
+                               hildonize.show_information_banner(self._window, "Sending to %s" % number)
+                               dialed = True
+                       except Exception, e:
+                               self._errorDisplay.push_exception()
+
+                       if dialed:
+                               self._dialpads[self._selectedBackendId].clear()
+               except Exception, e:
+                       self._errorDisplay.push_exception()
+
+       def _on_dial_clicked(self, number):
+               try:
+                       assert number, "No number to call"
+                       try:
+                               loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
+                       except Exception, e:
+                               loggedIn = False
+                               self._errorDisplay.push_exception()
+                               return
+
+                       if not loggedIn:
+                               self._errorDisplay.push_message(
+                                       "Backend link with GoogleVoice is not working, please try again"
+                               )
+                               return
+
+                       dialed = False
+                       try:
+                               assert self._phoneBackends[self._selectedBackendId].get_callback_number() != "", "No callback number specified"
+                               self._phoneBackends[self._selectedBackendId].dial(number)
+                               hildonize.show_information_banner(self._window, "Calling %s" % number)
+                               dialed = True
+                       except Exception, e:
+                               self._errorDisplay.push_exception()
+
+                       if dialed:
+                               self._dialpads[self._selectedBackendId].clear()
+               except Exception, e:
+                       self._errorDisplay.push_exception()
+
+       def _on_menu_refresh(self, *args):
+               try:
+                       self._refresh_active_tab()
+               except Exception, e:
+                       self._errorDisplay.push_exception()
+
+       def _on_paste(self, *args):
+               try:
+                       contents = self._clipboard.wait_for_text()
+                       if contents is not None:
+                               self._dialpads[self._selectedBackendId].set_number(contents)
+               except Exception, e:
+                       self._errorDisplay.push_exception()
+
+       def _on_about_activate(self, *args):
+               try:
+                       dlg = gtk.AboutDialog()
+                       dlg.set_name(constants.__pretty_app_name__)
+                       dlg.set_version("%s-%d" % (constants.__version__, constants.__build__))
+                       dlg.set_copyright("Copyright 2008 - LGPL")
+                       dlg.set_comments("Dialcentral is a touch screen enhanced interface to your GoogleVoice account.  This application is not affiliated with Google in any way")
+                       dlg.set_website("http://gc-dialer.garage.maemo.org/")
+                       dlg.set_authors(["<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()
diff --git a/src/dialcentral.glade b/src/dialcentral.glade
new file mode 100644 (file)
index 0000000..ba8d0ff
--- /dev/null
@@ -0,0 +1,1292 @@
+<?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">&lt;span size="35000" weight="bold"&gt;&lt;/span&gt;</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">&lt;span size="33000" weight="bold"&gt;1&lt;/span&gt;
+&lt;span size="9000"&gt;  &lt;/span&gt;</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">&lt;span size="30000" weight="bold"&gt;2&lt;/span&gt;
+&lt;span size="12000"&gt;ABC&lt;/span&gt;</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">&lt;span size="30000" weight="bold" stretch="ultraexpanded"&gt;3&lt;/span&gt;
+&lt;span size="12000"&gt;DEF&lt;/span&gt;</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">&lt;span size="30000" weight="bold"&gt;4&lt;/span&gt;
+&lt;span size="12000"&gt;GHI&lt;/span&gt;</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">&lt;span size="30000" weight="bold"&gt;5&lt;/span&gt;
+&lt;span size="12000"&gt;JKL&lt;/span&gt;</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">&lt;span size="30000" weight="bold"&gt;6&lt;/span&gt;
+&lt;span size="12000"&gt;MNO&lt;/span&gt;</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">&lt;span size="30000" weight="bold"&gt;7&lt;/span&gt;
+&lt;span size="12000"&gt;PQRS&lt;/span&gt;</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">&lt;span size="30000" weight="bold"&gt;8&lt;/span&gt;
+&lt;span size="12000"&gt;TUV&lt;/span&gt;</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">&lt;span size="30000" weight="bold"&gt;9&lt;/span&gt;
+&lt;span size="12000"&gt;WXYZ&lt;/span&gt;</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">&lt;span size="33000" weight="bold"&gt;0&lt;/span&gt;
+&lt;span size="9000"&gt;&lt;/span&gt;</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">&lt;span size="17000" weight="bold"&gt;Dial&lt;/span&gt;</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">&lt;span size="17000" weight="bold"&gt;SMS&lt;/span&gt;</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>
diff --git a/src/dialcentral.py b/src/dialcentral.py
new file mode 100755 (executable)
index 0000000..4f669ff
--- /dev/null
@@ -0,0 +1,28 @@
+#!/usr/bin/python
+
+import os
+import sys
+import logging
+
+
+sys.path.insert(0,"/usr/lib/dialcentral/")
+
+
+import constants
+import dc_glade
+
+
+try:
+       os.makedirs(constants._data_path_)
+except OSError, e:
+       if e.errno != 17:
+               raise
+
+userLogPath = "%s/dialcentral.log" % constants._data_path_
+logging.basicConfig(level=logging.DEBUG, filename=userLogPath)
+logging.info("Dialcentral %s-%s" % (constants.__version__, constants.__build__))
+
+try:
+       dc_glade.run_dialpad()
+finally:
+       logging.shutdown()
diff --git a/src/example_custom_notifier.py b/src/example_custom_notifier.py
new file mode 100755 (executable)
index 0000000..3679b5b
--- /dev/null
@@ -0,0 +1,24 @@
+#!/usr/bin/env python
+
+import sys
+
+
+sys.path.insert(0,"/usr/lib/dialcentral/")
+
+
+import alarm_notify
+
+
+def notify_on_change():
+       notifyUser = alarm_notify.is_changed()
+
+       if notifyUser:
+               import subprocess
+               import led_handler
+               led = led_handler.LedHandler()
+               led.on()
+               soundOn = subprocess.call("/usr/bin/dbus-send --dest=com.nokia.osso_media_server --print-reply /com/nokia/osso_media_server com.nokia.osso_media_server.music.play_media string:file:///usr/lib/gv-notifier/alert.mp3",shell=True)
+
+
+if __name__ == "__main__":
+       notify_on_change()
diff --git a/src/file_backend.py b/src/file_backend.py
new file mode 100644 (file)
index 0000000..b373561
--- /dev/null
@@ -0,0 +1,171 @@
+#!/usr/bin/python
+
+"""
+DialCentral - Front end for Google's Grand Central service.
+Copyright (C) 2008  Eric Warnke ericew AT gmail DOT com
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 2.1 of the License, or (at your option) any later version.
+
+This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+
+Filesystem backend for contact support
+"""
+
+
+import os
+import re
+import csv
+
+
+class CsvAddressBook(object):
+       """
+       Currently supported file format
+       @li Has the first line as a header
+       @li Escapes with quotes
+       @li Comma as delimiter
+       @li Column 0 is name, column 1 is number
+       """
+
+       _nameRe = re.compile("name", re.IGNORECASE)
+       _phoneRe = re.compile("phone", re.IGNORECASE)
+       _mobileRe = re.compile("mobile", re.IGNORECASE)
+
+       def __init__(self, csvPath):
+               self.__csvPath = csvPath
+               self.__contacts = list(
+                       self.read_csv(csvPath)
+               )
+
+       @classmethod
+       def read_csv(cls, csvPath):
+               try:
+                       csvReader = iter(csv.reader(open(csvPath, "rU")))
+               except IOError, e:
+                       if e.errno != 2:
+                               raise
+                       return
+
+               header = csvReader.next()
+               nameColumn, phoneColumns = cls._guess_columns(header)
+
+               yieldCount = 0
+               for row in csvReader:
+                       contactDetails = []
+                       for (phoneType, phoneColumn) in phoneColumns:
+                               try:
+                                       if len(row[phoneColumn]) == 0:
+                                               continue
+                                       contactDetails.append((phoneType, row[phoneColumn]))
+                               except IndexError:
+                                       pass
+                       if len(contactDetails) != 0:
+                               yield str(yieldCount), row[nameColumn], contactDetails
+                               yieldCount += 1
+
+       @classmethod
+       def _guess_columns(cls, row):
+               names = []
+               phones = []
+               for i, item in enumerate(row):
+                       if cls._nameRe.search(item) is not None:
+                               names.append((item, i))
+                       elif cls._phoneRe.search(item) is not None:
+                               phones.append((item, i))
+                       elif cls._mobileRe.search(item) is not None:
+                               phones.append((item, i))
+               if len(names) == 0:
+                       names.append(("Name", 0))
+               if len(phones) == 0:
+                       phones.append(("Phone", 1))
+
+               return names[0][1], phones
+
+       def clear_caches(self):
+               pass
+
+       @staticmethod
+       def factory_name():
+               return "csv"
+
+       @staticmethod
+       def contact_source_short_name(contactId):
+               return "csv"
+
+       def get_contacts(self):
+               """
+               @returns Iterable of (contact id, contact name)
+               """
+               for contact in self.__contacts:
+                       yield contact[0:2]
+
+       def get_contact_details(self, contactId):
+               """
+               @returns Iterable of (Phone Type, Phone Number)
+               """
+               contactId = int(contactId)
+               return iter(self.__contacts[contactId][2])
+
+
+class FilesystemAddressBookFactory(object):
+
+       FILETYPE_SUPPORT = {
+               "csv": CsvAddressBook,
+       }
+
+       def __init__(self, path):
+               self.__path = path
+
+       def clear_caches(self):
+               pass
+
+       def get_addressbooks(self):
+               """
+               @returns Iterable of (Address Book Factory, Book Id, Book Name)
+               """
+               for root, dirs, filenames in os.walk(self.__path):
+                       for filename in filenames:
+                               try:
+                                       name, ext = filename.rsplit(".", 1)
+                               except ValueError:
+                                       continue
+
+                               if ext in self.FILETYPE_SUPPORT:
+                                       yield self, os.path.join(root, filename), name
+
+       def open_addressbook(self, bookId):
+               name, ext = bookId.rsplit(".", 1)
+               assert ext in self.FILETYPE_SUPPORT, "Unsupported file extension %s" % ext
+               return self.FILETYPE_SUPPORT[ext](bookId)
+
+       @staticmethod
+       def factory_name():
+               return "File"
+
+
+def print_filebooks(contactPath = None):
+       """
+       Included here for debugging.
+
+       Either insert it into the code or launch python with the "-i" flag
+       """
+       if contactPath is None:
+               contactPath = os.path.join(os.path.expanduser("~"), ".dialcentral", "contacts")
+
+       abf = FilesystemAddressBookFactory(contactPath)
+       for book in abf.get_addressbooks():
+               ab = abf.open_addressbook(book[1])
+               print book
+               for contact in ab.get_contacts():
+                       print "\t", contact
+                       for details in ab.get_contact_details(contact[0]):
+                               print "\t\t", details
diff --git a/src/gtk_toolbox.py b/src/gtk_toolbox.py
new file mode 100644 (file)
index 0000000..26803d7
--- /dev/null
@@ -0,0 +1,741 @@
+#!/usr/bin/python
+
+from __future__ import with_statement
+
+import os
+import errno
+import sys
+import time
+import itertools
+import functools
+import contextlib
+import logging
+import threading
+import Queue
+
+import gobject
+import gtk
+
+
+def get_screen_orientation():
+       width, height = gtk.gdk.get_default_root_window().get_size()
+       if width < height:
+               return gtk.ORIENTATION_VERTICAL
+       else:
+               return gtk.ORIENTATION_HORIZONTAL
+
+
+def orientation_change_connect(handler, *args):
+       """
+       @param handler(orientation, *args) -> None(?)
+       """
+       initialScreenOrientation = get_screen_orientation()
+       orientationAndArgs = list(itertools.chain((initialScreenOrientation, ), args))
+
+       def _on_screen_size_changed(screen):
+               newScreenOrientation = get_screen_orientation()
+               if newScreenOrientation != orientationAndArgs[0]:
+                       orientationAndArgs[0] = newScreenOrientation
+                       handler(*orientationAndArgs)
+
+       rootScreen = gtk.gdk.get_default_root_window()
+       return gtk.connect(rootScreen, "size-changed", _on_screen_size_changed)
+
+
+@contextlib.contextmanager
+def flock(path, timeout=-1):
+       WAIT_FOREVER = -1
+       DELAY = 0.1
+       timeSpent = 0
+
+       acquired = False
+
+       while timeSpent <= timeout or timeout == WAIT_FOREVER:
+               try:
+                       fd = os.open(path, os.O_CREAT | os.O_EXCL | os.O_RDWR)
+                       acquired = True
+                       break
+               except OSError, e:
+                       if e.errno != errno.EEXIST:
+                               raise
+               time.sleep(DELAY)
+               timeSpent += DELAY
+
+       assert acquired, "Failed to grab file-lock %s within timeout %d" % (path, timeout)
+
+       try:
+               yield fd
+       finally:
+               os.unlink(path)
+
+
+@contextlib.contextmanager
+def gtk_lock():
+       gtk.gdk.threads_enter()
+       try:
+               yield
+       finally:
+               gtk.gdk.threads_leave()
+
+
+def find_parent_window(widget):
+       while True:
+               parent = widget.get_parent()
+               if isinstance(parent, gtk.Window):
+                       return parent
+               widget = parent
+
+
+def make_idler(func):
+       """
+       Decorator that makes a generator-function into a function that will continue execution on next call
+       """
+       a = []
+
+       @functools.wraps(func)
+       def decorated_func(*args, **kwds):
+               if not a:
+                       a.append(func(*args, **kwds))
+               try:
+                       a[0].next()
+                       return True
+               except StopIteration:
+                       del a[:]
+                       return False
+
+       return decorated_func
+
+
+def asynchronous_gtk_message(original_func):
+       """
+       @note Idea came from http://www.aclevername.com/articles/python-webgui/
+       """
+
+       def execute(allArgs):
+               args, kwargs = allArgs
+               with gtk_lock():
+                       original_func(*args, **kwargs)
+               return False
+
+       @functools.wraps(original_func)
+       def delayed_func(*args, **kwargs):
+               gobject.idle_add(execute, (args, kwargs))
+
+       return delayed_func
+
+
+def synchronous_gtk_message(original_func):
+       """
+       @note Idea came from http://www.aclevername.com/articles/python-webgui/
+       """
+
+       @functools.wraps(original_func)
+       def immediate_func(*args, **kwargs):
+               with gtk_lock():
+                       return original_func(*args, **kwargs)
+
+       return immediate_func
+
+
+def autostart(func):
+       """
+       >>> @autostart
+       ... def grep_sink(pattern):
+       ...     print "Looking for %s" % pattern
+       ...     while True:
+       ...             line = yield
+       ...             if pattern in line:
+       ...                     print line,
+       >>> g = grep_sink("python")
+       Looking for python
+       >>> g.send("Yeah but no but yeah but no")
+       >>> g.send("A series of tubes")
+       >>> g.send("python generators rock!")
+       python generators rock!
+       >>> g.close()
+       """
+
+       @functools.wraps(func)
+       def start(*args, **kwargs):
+               cr = func(*args, **kwargs)
+               cr.next()
+               return cr
+
+       return start
+
+
+@autostart
+def printer_sink(format = "%s"):
+       """
+       >>> pr = printer_sink("%r")
+       >>> pr.send("Hello")
+       'Hello'
+       >>> pr.send("5")
+       '5'
+       >>> pr.send(5)
+       5
+       >>> p = printer_sink()
+       >>> p.send("Hello")
+       Hello
+       >>> p.send("World")
+       World
+       >>> # p.throw(RuntimeError, "Goodbye")
+       >>> # p.send("Meh")
+       >>> # p.close()
+       """
+       while True:
+               item = yield
+               print format % (item, )
+
+
+@autostart
+def null_sink():
+       """
+       Good for uses like with cochain to pick up any slack
+       """
+       while True:
+               item = yield
+
+
+@autostart
+def comap(function, target):
+       """
+       >>> p = printer_sink()
+       >>> cm = comap(lambda x: x+1, p)
+       >>> cm.send((0, ))
+       1
+       >>> cm.send((1.0, ))
+       2.0
+       >>> cm.send((-2, ))
+       -1
+       """
+       while True:
+               try:
+                       item = yield
+                       mappedItem = function(*item)
+                       target.send(mappedItem)
+               except Exception, e:
+                       logging.exception("Forwarding exception!")
+                       target.throw(e.__class__, str(e))
+
+
+def _flush_queue(queue):
+       while not queue.empty():
+               yield queue.get()
+
+
+@autostart
+def queue_sink(queue):
+       """
+       >>> q = Queue.Queue()
+       >>> qs = queue_sink(q)
+       >>> qs.send("Hello")
+       >>> qs.send("World")
+       >>> qs.throw(RuntimeError, "Goodbye")
+       >>> qs.send("Meh")
+       >>> qs.close()
+       >>> print [i for i in _flush_queue(q)]
+       [(None, 'Hello'), (None, 'World'), (<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()
diff --git a/src/gv_backend.py b/src/gv_backend.py
new file mode 100644 (file)
index 0000000..0e5da67
--- /dev/null
@@ -0,0 +1,645 @@
+#!/usr/bin/python
+
+"""
+DialCentral - Front end for Google's GoogleVoice service.
+Copyright (C) 2008  Eric Warnke ericew AT gmail DOT com
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 2.1 of the License, or (at your option) any later version.
+
+This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+
+Google Voice backend code
+
+Resources
+       http://thatsmith.com/2009/03/google-voice-addon-for-firefox/
+       http://posttopic.com/topic/google-voice-add-on-development
+"""
+
+
+import os
+import re
+import urllib
+import urllib2
+import time
+import datetime
+import itertools
+import logging
+from xml.sax import saxutils
+
+from xml.etree import ElementTree
+
+import browser_emu
+
+try:
+       import simplejson
+except ImportError:
+       simplejson = None
+
+
+_TRUE_REGEX = re.compile("true")
+_FALSE_REGEX = re.compile("false")
+
+
+def safe_eval(s):
+       s = _TRUE_REGEX.sub("True", s)
+       s = _FALSE_REGEX.sub("False", s)
+       return eval(s, {}, {})
+
+
+if simplejson is None:
+       def parse_json(flattened):
+               return safe_eval(flattened)
+else:
+       def parse_json(flattened):
+               return simplejson.loads(flattened)
+
+
+def itergroup(iterator, count, padValue = None):
+       """
+       Iterate in groups of 'count' values. If there
+       aren't enough values, the last result is padded with
+       None.
+
+       >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
+       ...     print tuple(val)
+       (1, 2, 3)
+       (4, 5, 6)
+       >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
+       ...     print list(val)
+       [1, 2, 3]
+       [4, 5, 6]
+       >>> for val in itergroup([1, 2, 3, 4, 5, 6, 7], 3):
+       ...     print tuple(val)
+       (1, 2, 3)
+       (4, 5, 6)
+       (7, None, None)
+       >>> for val in itergroup("123456", 3):
+       ...     print tuple(val)
+       ('1', '2', '3')
+       ('4', '5', '6')
+       >>> for val in itergroup("123456", 3):
+       ...     print repr("".join(val))
+       '123'
+       '456'
+       """
+       paddedIterator = itertools.chain(iterator, itertools.repeat(padValue, count-1))
+       nIterators = (paddedIterator, ) * count
+       return itertools.izip(*nIterators)
+
+
+class GVDialer(object):
+       """
+       This class encapsulates all of the knowledge necessary to interact with the GoogleVoice servers
+       the functions include login, setting up a callback number, and initalting a callback
+       """
+
+       def __init__(self, cookieFile = None):
+               # Important items in this function are the setup of the browser emulation and cookie file
+               self._browser = browser_emu.MozillaEmulator(1)
+               if cookieFile is None:
+                       cookieFile = os.path.join(os.path.expanduser("~"), ".gv_cookies.txt")
+               self._browser.cookies.filename = cookieFile
+               if os.path.isfile(cookieFile):
+                       self._browser.cookies.load()
+
+               self._token = ""
+               self._accountNum = ""
+               self._lastAuthed = 0.0
+               self._callbackNumber = ""
+               self._callbackNumbers = {}
+
+               self.__contacts = None
+
+       def is_authed(self, force = False):
+               """
+               Attempts to detect a current session
+               @note Once logged in try not to reauth more than once a minute.
+               @returns If authenticated
+               """
+
+               if (time.time() - self._lastAuthed) < 120 and not force:
+                       return True
+
+               try:
+                       self._grab_account_info()
+               except Exception, e:
+                       logging.exception(str(e))
+                       return False
+
+               self._browser.cookies.save()
+               self._lastAuthed = time.time()
+               return True
+
+       _loginURL = "https://www.google.com/accounts/ServiceLoginAuth"
+
+       def login(self, username, password):
+               """
+               Attempt to login to GoogleVoice
+               @returns Whether login was successful or not
+               """
+               if self.is_authed():
+                       return True
+
+               loginPostData = urllib.urlencode({
+                       'Email' : username,
+                       'Passwd' : password,
+                       'service': "grandcentral",
+                       "ltmpl": "mobile",
+                       "btmpl": "mobile",
+                       "PersistentCookie": "yes",
+               })
+
+               try:
+                       loginSuccessOrFailurePage = self._browser.download(self._loginURL, loginPostData)
+               except urllib2.URLError, e:
+                       logging.exception(str(e))
+                       raise RuntimeError("%s is not accesible" % self._loginURL)
+
+               return self.is_authed()
+
+       def logout(self):
+               self._lastAuthed = 0.0
+               self._browser.cookies.clear()
+               self._browser.cookies.save()
+
+               self.clear_caches()
+
+       _gvDialingStrRe = re.compile("This may take a few seconds", re.M)
+       _clicktocallURL = "https://www.google.com/voice/m/sendcall"
+
+       def dial(self, number):
+               """
+               This is the main function responsible for initating the callback
+               """
+               number = self._send_validation(number)
+               try:
+                       clickToCallData = urllib.urlencode({
+                               "number": number,
+                               "phone": self._callbackNumber,
+                               "_rnr_se": self._token,
+                       })
+                       otherData = {
+                               'Referer' : 'https://google.com/voice/m/callsms',
+                       }
+                       callSuccessPage = self._browser.download(self._clicktocallURL, clickToCallData, None, otherData)
+               except urllib2.URLError, e:
+                       logging.exception(str(e))
+                       raise RuntimeError("%s is not accesible" % self._clicktocallURL)
+
+               if self._gvDialingStrRe.search(callSuccessPage) is None:
+                       raise RuntimeError("Google Voice returned an error")
+
+               return True
+
+       _sendSmsURL = "https://www.google.com/voice/m/sendsms"
+
+       def send_sms(self, number, message):
+               number = self._send_validation(number)
+               try:
+                       smsData = urllib.urlencode({
+                               "number": number,
+                               "smstext": message,
+                               "_rnr_se": self._token,
+                               "id": "undefined",
+                               "c": "undefined",
+                       })
+                       otherData = {
+                               'Referer' : 'https://google.com/voice/m/sms',
+                       }
+                       smsSuccessPage = self._browser.download(self._sendSmsURL, smsData, None, otherData)
+               except urllib2.URLError, e:
+                       logging.exception(str(e))
+                       raise RuntimeError("%s is not accesible" % self._sendSmsURL)
+
+               return True
+
+       def clear_caches(self):
+               self.__contacts = None
+
+       _validateRe = re.compile("^[0-9]{10,}$")
+
+       def is_valid_syntax(self, number):
+               """
+               @returns If This number be called ( syntax validation only )
+               """
+               return self._validateRe.match(number) is not None
+
+       def get_account_number(self):
+               """
+               @returns The GoogleVoice phone number
+               """
+               return self._accountNum
+
+       def set_sane_callback(self):
+               """
+               Try to set a sane default callback number on these preferences
+               1) 1747 numbers ( Gizmo )
+               2) anything with gizmo in the name
+               3) anything with computer in the name
+               4) the first value
+               """
+               numbers = self.get_callback_numbers()
+
+               for number, description in numbers.iteritems():
+                       if re.compile(r"""1747""").match(number) is not None:
+                               self.set_callback_number(number)
+                               return
+
+               for number, description in numbers.iteritems():
+                       if re.compile(r"""gizmo""", re.I).search(description) is not None:
+                               self.set_callback_number(number)
+                               return
+
+               for number, description in numbers.iteritems():
+                       if re.compile(r"""computer""", re.I).search(description) is not None:
+                               self.set_callback_number(number)
+                               return
+
+               for number, description in numbers.iteritems():
+                       self.set_callback_number(number)
+                       return
+
+       def get_callback_numbers(self):
+               """
+               @returns a dictionary mapping call back numbers to descriptions
+               @note These results are cached for 30 minutes.
+               """
+               if not self.is_authed():
+                       return {}
+               return self._callbackNumbers
+
+       _setforwardURL = "https://www.google.com//voice/m/setphone"
+
+       def set_callback_number(self, callbacknumber):
+               """
+               Set the number that GoogleVoice calls
+               @param callbacknumber should be a proper 10 digit number
+               """
+               self._callbackNumber = callbacknumber
+               return True
+
+       def get_callback_number(self):
+               """
+               @returns Current callback number or None
+               """
+               return self._callbackNumber
+
+       def get_recent(self):
+               """
+               @returns Iterable of (personsName, phoneNumber, date, action)
+               """
+               sortedRecent = [
+                       (exactDate, name, number, relativeDate, action)
+                       for (name, number, exactDate, relativeDate, action) in self._get_recent()
+               ]
+               sortedRecent.sort(reverse = True)
+               for exactDate, name, number, relativeDate, action in sortedRecent:
+                       yield name, number, relativeDate, action
+
+       def get_addressbooks(self):
+               """
+               @returns Iterable of (Address Book Factory, Book Id, Book Name)
+               """
+               yield self, "", ""
+
+       def open_addressbook(self, bookId):
+               return self
+
+       @staticmethod
+       def contact_source_short_name(contactId):
+               return "GV"
+
+       @staticmethod
+       def factory_name():
+               return "Google Voice"
+
+       _contactsRe = re.compile(r"""<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
diff --git a/src/gv_views.py b/src/gv_views.py
new file mode 100644 (file)
index 0000000..0eac88b
--- /dev/null
@@ -0,0 +1,1483 @@
+#!/usr/bin/python2.5
+
+"""
+DialCentral - Front end for Google's GoogleVoice service.
+Copyright (C) 2008  Mark Bergman bergman AT merctech DOT com
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 2.1 of the License, or (at your option) any later version.
+
+This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+
+@todo Alternate UI for dialogs (stackables)
+"""
+
+from __future__ import with_statement
+
+import ConfigParser
+import logging
+
+import gobject
+import pango
+import gtk
+
+import gtk_toolbox
+import hildonize
+import null_backend
+
+
+def make_ugly(prettynumber):
+       """
+       function to take a phone number and strip out all non-numeric
+       characters
+
+       >>> make_ugly("+012-(345)-678-90")
+       '01234567890'
+       """
+       import re
+       uglynumber = re.sub('\D', '', prettynumber)
+       return uglynumber
+
+
+def make_pretty(phonenumber):
+       """
+       Function to take a phone number and return the pretty version
+       pretty numbers:
+               if phonenumber begins with 0:
+                       ...-(...)-...-....
+               if phonenumber begins with 1: ( for gizmo callback numbers )
+                       1 (...)-...-....
+               if phonenumber is 13 digits:
+                       (...)-...-....
+               if phonenumber is 10 digits:
+                       ...-....
+       >>> make_pretty("12")
+       '12'
+       >>> make_pretty("1234567")
+       '123-4567'
+       >>> make_pretty("2345678901")
+       '(234)-567-8901'
+       >>> make_pretty("12345678901")
+       '1 (234)-567-8901'
+       >>> make_pretty("01234567890")
+       '+012-(345)-678-90'
+       """
+       if phonenumber is None or phonenumber is "":
+               return ""
+
+       phonenumber = make_ugly(phonenumber)
+
+       if len(phonenumber) < 3:
+               return phonenumber
+
+       if phonenumber[0] == "0":
+               prettynumber = ""
+               prettynumber += "+%s" % phonenumber[0:3]
+               if 3 < len(phonenumber):
+                       prettynumber += "-(%s)" % phonenumber[3:6]
+                       if 6 < len(phonenumber):
+                               prettynumber += "-%s" % phonenumber[6:9]
+                               if 9 < len(phonenumber):
+                                       prettynumber += "-%s" % phonenumber[9:]
+               return prettynumber
+       elif len(phonenumber) <= 7:
+               prettynumber = "%s-%s" % (phonenumber[0:3], phonenumber[3:])
+       elif len(phonenumber) > 8 and phonenumber[0] == "1":
+               prettynumber = "1 (%s)-%s-%s" % (phonenumber[1:4], phonenumber[4:7], phonenumber[7:])
+       elif len(phonenumber) > 7:
+               prettynumber = "(%s)-%s-%s" % (phonenumber[0:3], phonenumber[3:6], phonenumber[6:])
+       return prettynumber
+
+
+def abbrev_relative_date(date):
+       """
+       >>> abbrev_relative_date("42 hours ago")
+       '42 h'
+       >>> abbrev_relative_date("2 days ago")
+       '2 d'
+       >>> abbrev_relative_date("4 weeks ago")
+       '4 w'
+       """
+       parts = date.split(" ")
+       return "%s %s" % (parts[0], parts[1][0])
+
+
+class MergedAddressBook(object):
+       """
+       Merger of all addressbooks
+       """
+
+       def __init__(self, addressbookFactories, sorter = None):
+               self.__addressbookFactories = addressbookFactories
+               self.__addressbooks = None
+               self.__sort_contacts = sorter if sorter is not None else self.null_sorter
+
+       def clear_caches(self):
+               self.__addressbooks = None
+               for factory in self.__addressbookFactories:
+                       factory.clear_caches()
+
+       def get_addressbooks(self):
+               """
+               @returns Iterable of (Address Book Factory, Book Id, Book Name)
+               """
+               yield self, "", ""
+
+       def open_addressbook(self, bookId):
+               return self
+
+       def contact_source_short_name(self, contactId):
+               if self.__addressbooks is None:
+                       return ""
+               bookIndex, originalId = contactId.split("-", 1)
+               return self.__addressbooks[int(bookIndex)].contact_source_short_name(originalId)
+
+       @staticmethod
+       def factory_name():
+               return "All Contacts"
+
+       def get_contacts(self):
+               """
+               @returns Iterable of (contact id, contact name)
+               """
+               if self.__addressbooks is None:
+                       self.__addressbooks = list(
+                               factory.open_addressbook(id)
+                               for factory in self.__addressbookFactories
+                               for (f, id, name) in factory.get_addressbooks()
+                       )
+               contacts = (
+                       ("-".join([str(bookIndex), contactId]), contactName)
+                               for (bookIndex, addressbook) in enumerate(self.__addressbooks)
+                                       for (contactId, contactName) in addressbook.get_contacts()
+               )
+               sortedContacts = self.__sort_contacts(contacts)
+               return sortedContacts
+
+       def get_contact_details(self, contactId):
+               """
+               @returns Iterable of (Phone Type, Phone Number)
+               """
+               if self.__addressbooks is None:
+                       return []
+               bookIndex, originalId = contactId.split("-", 1)
+               return self.__addressbooks[int(bookIndex)].get_contact_details(originalId)
+
+       @staticmethod
+       def null_sorter(contacts):
+               """
+               Good for speed/low memory
+               """
+               return contacts
+
+       @staticmethod
+       def basic_firtname_sorter(contacts):
+               """
+               Expects names in "First Last" format
+               """
+               contactsWithKey = [
+                       (contactName.rsplit(" ", 1)[0], (contactId, contactName))
+                               for (contactId, contactName) in contacts
+               ]
+               contactsWithKey.sort()
+               return (contactData for (lastName, contactData) in contactsWithKey)
+
+       @staticmethod
+       def basic_lastname_sorter(contacts):
+               """
+               Expects names in "First Last" format
+               """
+               contactsWithKey = [
+                       (contactName.rsplit(" ", 1)[-1], (contactId, contactName))
+                               for (contactId, contactName) in contacts
+               ]
+               contactsWithKey.sort()
+               return (contactData for (lastName, contactData) in contactsWithKey)
+
+       @staticmethod
+       def reversed_firtname_sorter(contacts):
+               """
+               Expects names in "Last, First" format
+               """
+               contactsWithKey = [
+                       (contactName.split(", ", 1)[-1], (contactId, contactName))
+                               for (contactId, contactName) in contacts
+               ]
+               contactsWithKey.sort()
+               return (contactData for (lastName, contactData) in contactsWithKey)
+
+       @staticmethod
+       def reversed_lastname_sorter(contacts):
+               """
+               Expects names in "Last, First" format
+               """
+               contactsWithKey = [
+                       (contactName.split(", ", 1)[0], (contactId, contactName))
+                               for (contactId, contactName) in contacts
+               ]
+               contactsWithKey.sort()
+               return (contactData for (lastName, contactData) in contactsWithKey)
+
+       @staticmethod
+       def guess_firstname(name):
+               if ", " in name:
+                       return name.split(", ", 1)[-1]
+               else:
+                       return name.rsplit(" ", 1)[0]
+
+       @staticmethod
+       def guess_lastname(name):
+               if ", " in name:
+                       return name.split(", ", 1)[0]
+               else:
+                       return name.rsplit(" ", 1)[-1]
+
+       @classmethod
+       def advanced_firstname_sorter(cls, contacts):
+               contactsWithKey = [
+                       (cls.guess_firstname(contactName), (contactId, contactName))
+                               for (contactId, contactName) in contacts
+               ]
+               contactsWithKey.sort()
+               return (contactData for (lastName, contactData) in contactsWithKey)
+
+       @classmethod
+       def advanced_lastname_sorter(cls, contacts):
+               contactsWithKey = [
+                       (cls.guess_lastname(contactName), (contactId, contactName))
+                               for (contactId, contactName) in contacts
+               ]
+               contactsWithKey.sort()
+               return (contactData for (lastName, contactData) in contactsWithKey)
+
+
+class PhoneTypeSelector(object):
+
+       ACTION_CANCEL = "cancel"
+       ACTION_SELECT = "select"
+       ACTION_DIAL = "dial"
+       ACTION_SEND_SMS = "sms"
+
+       def __init__(self, widgetTree, gcBackend):
+               self._gcBackend = gcBackend
+               self._widgetTree = widgetTree
+
+               self._dialog = self._widgetTree.get_widget("phonetype_dialog")
+               self._smsDialog = SmsEntryDialog(self._widgetTree)
+
+               self._smsButton = self._widgetTree.get_widget("sms_button")
+               self._smsButton.connect("clicked", self._on_phonetype_send_sms)
+
+               self._dialButton = self._widgetTree.get_widget("dial_button")
+               self._dialButton.connect("clicked", self._on_phonetype_dial)
+
+               self._selectButton = self._widgetTree.get_widget("select_button")
+               self._selectButton.connect("clicked", self._on_phonetype_select)
+
+               self._cancelButton = self._widgetTree.get_widget("cancel_button")
+               self._cancelButton.connect("clicked", self._on_phonetype_cancel)
+
+               self._messagemodel = gtk.ListStore(gobject.TYPE_STRING)
+               self._messagesView = self._widgetTree.get_widget("phoneSelectionMessages")
+               self._scrollWindow = self._messagesView.get_parent()
+
+               self._typemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
+               self._typeviewselection = None
+               self._typeview = self._widgetTree.get_widget("phonetypes")
+               self._typeview.connect("row-activated", self._on_phonetype_select)
+
+               self._action = self.ACTION_CANCEL
+
+       def run(self, contactDetails, messages = (), parent = None):
+               self._action = self.ACTION_CANCEL
+
+               # Add the column to the phone selection tree view
+               self._typemodel.clear()
+               self._typeview.set_model(self._typemodel)
+
+               textrenderer = gtk.CellRendererText()
+               numberColumn = gtk.TreeViewColumn("Phone Numbers", textrenderer, text=0)
+               self._typeview.append_column(numberColumn)
+
+               textrenderer = gtk.CellRendererText()
+               typeColumn = gtk.TreeViewColumn("Phone Type", textrenderer, text=1)
+               self._typeview.append_column(typeColumn)
+
+               for phoneType, phoneNumber in contactDetails:
+                       display = " - ".join((phoneNumber, phoneType))
+                       display = phoneType
+                       row = (phoneNumber, display)
+                       self._typemodel.append(row)
+
+               self._typeviewselection = self._typeview.get_selection()
+               self._typeviewselection.set_mode(gtk.SELECTION_SINGLE)
+               self._typeviewselection.select_iter(self._typemodel.get_iter_first())
+
+               # Add the column to the messages tree view
+               self._messagemodel.clear()
+               self._messagesView.set_model(self._messagemodel)
+
+               textrenderer = gtk.CellRendererText()
+               textrenderer.set_property("wrap-mode", pango.WRAP_WORD)
+               textrenderer.set_property("wrap-width", 450)
+               messageColumn = gtk.TreeViewColumn("")
+               messageColumn.pack_start(textrenderer, expand=True)
+               messageColumn.add_attribute(textrenderer, "markup", 0)
+               messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
+               self._messagesView.append_column(messageColumn)
+               self._messagesView.set_headers_visible(False)
+
+               if messages:
+                       for message in messages:
+                               row = (message, )
+                               self._messagemodel.append(row)
+                       self._messagesView.show()
+                       self._scrollWindow.show()
+                       messagesSelection = self._messagesView.get_selection()
+                       messagesSelection.select_path((len(messages)-1, ))
+               else:
+                       self._messagesView.hide()
+                       self._scrollWindow.hide()
+
+               if parent is not None:
+                       self._dialog.set_transient_for(parent)
+
+               try:
+                       self._dialog.show()
+                       if messages:
+                               self._messagesView.scroll_to_cell((len(messages)-1, ))
+
+                       userResponse = self._dialog.run()
+               finally:
+                       self._dialog.hide()
+
+               if userResponse == gtk.RESPONSE_OK:
+                       phoneNumber = self._get_number()
+                       phoneNumber = make_ugly(phoneNumber)
+               else:
+                       phoneNumber = ""
+               if not phoneNumber:
+                       self._action = self.ACTION_CANCEL
+
+               if self._action == self.ACTION_SEND_SMS:
+                       smsMessage = self._smsDialog.run(phoneNumber, messages, parent)
+                       if not smsMessage:
+                               phoneNumber = ""
+                               self._action = self.ACTION_CANCEL
+               else:
+                       smsMessage = ""
+
+               self._messagesView.remove_column(messageColumn)
+               self._messagesView.set_model(None)
+
+               self._typeviewselection.unselect_all()
+               self._typeview.remove_column(numberColumn)
+               self._typeview.remove_column(typeColumn)
+               self._typeview.set_model(None)
+
+               return self._action, phoneNumber, smsMessage
+
+       def _get_number(self):
+               model, itr = self._typeviewselection.get_selected()
+               if not itr:
+                       return ""
+
+               phoneNumber = self._typemodel.get_value(itr, 0)
+               return phoneNumber
+
+       def _on_phonetype_dial(self, *args):
+               self._dialog.response(gtk.RESPONSE_OK)
+               self._action = self.ACTION_DIAL
+
+       def _on_phonetype_send_sms(self, *args):
+               self._dialog.response(gtk.RESPONSE_OK)
+               self._action = self.ACTION_SEND_SMS
+
+       def _on_phonetype_select(self, *args):
+               self._dialog.response(gtk.RESPONSE_OK)
+               self._action = self.ACTION_SELECT
+
+       def _on_phonetype_cancel(self, *args):
+               self._dialog.response(gtk.RESPONSE_CANCEL)
+               self._action = self.ACTION_CANCEL
+
+
+class SmsEntryDialog(object):
+       """
+       @todo Add multi-SMS messages like GoogleVoice
+       """
+
+       MAX_CHAR = 160
+
+       def __init__(self, widgetTree):
+               self._widgetTree = widgetTree
+               self._dialog = self._widgetTree.get_widget("smsDialog")
+
+               self._smsButton = self._widgetTree.get_widget("sendSmsButton")
+               self._smsButton.connect("clicked", self._on_send)
+
+               self._cancelButton = self._widgetTree.get_widget("cancelSmsButton")
+               self._cancelButton.connect("clicked", self._on_cancel)
+
+               self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
+
+               self._messagemodel = gtk.ListStore(gobject.TYPE_STRING)
+               self._messagesView = self._widgetTree.get_widget("smsMessages")
+               self._scrollWindow = self._messagesView.get_parent()
+
+               self._smsEntry = self._widgetTree.get_widget("smsEntry")
+               self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
+
+       def run(self, number, messages = (), parent = None):
+               # Add the column to the messages tree view
+               self._messagemodel.clear()
+               self._messagesView.set_model(self._messagemodel)
+
+               textrenderer = gtk.CellRendererText()
+               textrenderer.set_property("wrap-mode", pango.WRAP_WORD)
+               textrenderer.set_property("wrap-width", 450)
+               messageColumn = gtk.TreeViewColumn("")
+               messageColumn.pack_start(textrenderer, expand=True)
+               messageColumn.add_attribute(textrenderer, "markup", 0)
+               messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
+               self._messagesView.append_column(messageColumn)
+               self._messagesView.set_headers_visible(False)
+
+               if messages:
+                       for message in messages:
+                               row = (message, )
+                               self._messagemodel.append(row)
+                       self._messagesView.show()
+                       self._scrollWindow.show()
+                       messagesSelection = self._messagesView.get_selection()
+                       messagesSelection.select_path((len(messages)-1, ))
+               else:
+                       self._messagesView.hide()
+                       self._scrollWindow.hide()
+
+               self._smsEntry.get_buffer().set_text("")
+               self._update_letter_count()
+
+               if parent is not None:
+                       self._dialog.set_transient_for(parent)
+
+               try:
+                       self._dialog.show()
+                       if messages:
+                               self._messagesView.scroll_to_cell((len(messages)-1, ))
+                       self._smsEntry.grab_focus()
+
+                       userResponse = self._dialog.run()
+               finally:
+                       self._dialog.hide()
+
+               if userResponse == gtk.RESPONSE_OK:
+                       entryBuffer = self._smsEntry.get_buffer()
+                       enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
+                       enteredMessage = enteredMessage[0:self.MAX_CHAR]
+               else:
+                       enteredMessage = ""
+
+               self._messagesView.remove_column(messageColumn)
+               self._messagesView.set_model(None)
+
+               return enteredMessage.strip()
+
+       def _update_letter_count(self, *args):
+               entryLength = self._smsEntry.get_buffer().get_char_count()
+               charsLeft = self.MAX_CHAR - entryLength
+               self._letterCountLabel.set_text(str(charsLeft))
+               if charsLeft < 0:
+                       self._smsButton.set_sensitive(False)
+               else:
+                       self._smsButton.set_sensitive(True)
+
+       def _on_entry_changed(self, *args):
+               self._update_letter_count()
+
+       def _on_send(self, *args):
+               self._dialog.response(gtk.RESPONSE_OK)
+
+       def _on_cancel(self, *args):
+               self._dialog.response(gtk.RESPONSE_CANCEL)
+
+
+class Dialpad(object):
+
+       def __init__(self, widgetTree, errorDisplay):
+               self._clipboard = gtk.clipboard_get()
+               self._errorDisplay = errorDisplay
+               self._smsDialog = SmsEntryDialog(widgetTree)
+
+               self._numberdisplay = widgetTree.get_widget("numberdisplay")
+               self._smsButton = widgetTree.get_widget("sms")
+               self._dialButton = widgetTree.get_widget("dial")
+               self._backButton = widgetTree.get_widget("back")
+               self._phonenumber = ""
+               self._prettynumber = ""
+
+               callbackMapping = {
+                       "on_digit_clicked": self._on_digit_clicked,
+               }
+               widgetTree.signal_autoconnect(callbackMapping)
+               self._dialButton.connect("clicked", self._on_dial_clicked)
+               self._smsButton.connect("clicked", self._on_sms_clicked)
+
+               self._originalLabel = self._backButton.get_label()
+               self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton)
+               self._backTapHandler.on_tap = self._on_backspace
+               self._backTapHandler.on_hold = self._on_clearall
+               self._backTapHandler.on_holding = self._set_clear_button
+               self._backTapHandler.on_cancel = self._reset_back_button
+
+               self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
+               self._keyPressEventId = 0
+
+       def enable(self):
+               self._dialButton.grab_focus()
+               self._backTapHandler.enable()
+               self._keyPressEventId = self._window.connect("key-press-event", self._on_key_press)
+
+       def disable(self):
+               self._window.disconnect(self._keyPressEventId)
+               self._keyPressEventId = 0
+               self._reset_back_button()
+               self._backTapHandler.disable()
+
+       def number_selected(self, action, number, message):
+               """
+               @note Actual dial function is patched in later
+               """
+               raise NotImplementedError("Horrible unknown error has occurred")
+
+       def get_number(self):
+               return self._phonenumber
+
+       def set_number(self, number):
+               """
+               Set the number to dial
+               """
+               try:
+                       self._phonenumber = make_ugly(number)
+                       self._prettynumber = make_pretty(self._phonenumber)
+                       self._numberdisplay.set_label("<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()
diff --git a/src/hildonize.py b/src/hildonize.py
new file mode 100755 (executable)
index 0000000..391b365
--- /dev/null
@@ -0,0 +1,646 @@
+#!/usr/bin/env python
+
+
+import gobject
+import gtk
+import dbus
+
+
+class _NullHildonModule(object):
+       pass
+
+
+try:
+       import hildon as _hildon
+       hildon  = _hildon # Dumb but gets around pyflakiness
+except (ImportError, OSError):
+       hildon = _NullHildonModule
+
+
+IS_HILDON_SUPPORTED = hildon is not _NullHildonModule
+
+
+class _NullHildonProgram(object):
+
+       def add_window(self, window):
+               pass
+
+
+def _hildon_get_app_class():
+       return hildon.Program
+
+
+def _null_get_app_class():
+       return _NullHildonProgram
+
+
+try:
+       hildon.Program
+       get_app_class = _hildon_get_app_class
+except AttributeError:
+       get_app_class = _null_get_app_class
+
+
+def _hildon_set_application_title(window, title):
+       pass
+
+
+def _null_set_application_title(window, title):
+       window.set_title(title)
+
+
+if IS_HILDON_SUPPORTED:
+       set_application_title = _hildon_set_application_title
+else:
+       set_application_title = _null_set_application_title
+
+
+def _fremantle_hildonize_window(app, window):
+       oldWindow = window
+       newWindow = hildon.StackableWindow()
+       oldWindow.get_child().reparent(newWindow)
+       app.add_window(newWindow)
+       return newWindow
+
+
+def _hildon_hildonize_window(app, window):
+       oldWindow = window
+       newWindow = hildon.Window()
+       oldWindow.get_child().reparent(newWindow)
+       app.add_window(newWindow)
+       return newWindow
+
+
+def _null_hildonize_window(app, window):
+       return window
+
+
+try:
+       hildon.StackableWindow
+       hildonize_window = _fremantle_hildonize_window
+except AttributeError:
+       try:
+               hildon.Window
+               hildonize_window = _hildon_hildonize_window
+       except AttributeError:
+               hildonize_window = _null_hildonize_window
+
+
+def _fremantle_hildonize_menu(window, gtkMenu, buttons):
+       appMenu = hildon.AppMenu()
+       for button in buttons:
+               appMenu.append(button)
+       window.set_app_menu(appMenu)
+       gtkMenu.get_parent().remove(gtkMenu)
+       return appMenu
+
+
+def _hildon_hildonize_menu(window, gtkMenu, ignoredButtons):
+       hildonMenu = gtk.Menu()
+       for child in gtkMenu.get_children():
+               child.reparent(hildonMenu)
+       window.set_menu(hildonMenu)
+       gtkMenu.destroy()
+       return hildonMenu
+
+
+def _null_hildonize_menu(window, gtkMenu, ignoredButtons):
+       return gtkMenu
+
+
+try:
+       hildon.AppMenu
+       GTK_MENU_USED = False
+       IS_FREMANTLE_SUPPORTED = True
+       hildonize_menu = _fremantle_hildonize_menu
+except AttributeError:
+       GTK_MENU_USED = True
+       IS_FREMANTLE_SUPPORTED = False
+       if IS_HILDON_SUPPORTED:
+               hildonize_menu = _hildon_hildonize_menu
+       else:
+               hildonize_menu = _null_hildonize_menu
+
+
+def _hildon_set_cell_thumb_selectable(renderer):
+       renderer.set_property("scale", 1.5)
+
+
+def _null_set_cell_thumb_selectable(renderer):
+       pass
+
+
+if IS_HILDON_SUPPORTED:
+       set_cell_thumb_selectable = _hildon_set_cell_thumb_selectable
+else:
+       set_cell_thumb_selectable = _null_set_cell_thumb_selectable
+
+
+def _fremantle_show_information_banner(parent, message):
+       hildon.hildon_banner_show_information(parent, "", message)
+
+
+def _hildon_show_information_banner(parent, message):
+       hildon.hildon_banner_show_information(parent, None, message)
+
+
+def _null_show_information_banner(parent, message):
+       pass
+
+
+if IS_FREMANTLE_SUPPORTED:
+       show_information_banner = _fremantle_show_information_banner
+else:
+       try:
+               hildon.hildon_banner_show_information
+               show_information_banner = _hildon_show_information_banner
+       except AttributeError:
+               show_information_banner = _null_show_information_banner
+
+
+def _fremantle_show_busy_banner_start(parent, message):
+       hildon.hildon_gtk_window_set_progress_indicator(parent, True)
+       return parent
+
+
+def _fremantle_show_busy_banner_end(banner):
+       hildon.hildon_gtk_window_set_progress_indicator(banner, False)
+
+
+def _hildon_show_busy_banner_start(parent, message):
+       return hildon.hildon_banner_show_animation(parent, None, message)
+
+
+def _hildon_show_busy_banner_end(banner):
+       banner.destroy()
+
+
+def _null_show_busy_banner_start(parent, message):
+       return None
+
+
+def _null_show_busy_banner_end(banner):
+       assert banner is None
+
+
+try:
+       hildon.hildon_gtk_window_set_progress_indicator
+       show_busy_banner_start = _fremantle_show_busy_banner_start
+       show_busy_banner_end = _fremantle_show_busy_banner_end
+except AttributeError:
+       try:
+               hildon.hildon_banner_show_animation
+               show_busy_banner_start = _hildon_show_busy_banner_start
+               show_busy_banner_end = _hildon_show_busy_banner_end
+       except AttributeError:
+               show_busy_banner_start = _null_show_busy_banner_start
+               show_busy_banner_end = _null_show_busy_banner_end
+
+
+def _hildon_hildonize_text_entry(textEntry):
+       textEntry.set_property('hildon-input-mode', 7)
+
+
+def _null_hildonize_text_entry(textEntry):
+       pass
+
+
+if IS_HILDON_SUPPORTED:
+       hildonize_text_entry = _hildon_hildonize_text_entry
+else:
+       hildonize_text_entry = _null_hildonize_text_entry
+
+
+def _hildon_mark_window_rotatable(window):
+       # gtk documentation is unclear whether this does a "=" or a "|="
+       window.set_flags(hildon.HILDON_PORTRAIT_MODE_SUPPORT)
+
+
+def _null_mark_window_rotatable(window):
+       pass
+
+
+try:
+       hildon.HILDON_PORTRAIT_MODE_SUPPORT
+       mark_window_rotatable = _hildon_mark_window_rotatable
+except AttributeError:
+       mark_window_rotatable = _null_mark_window_rotatable
+
+
+def _hildon_window_to_portrait(window):
+       # gtk documentation is unclear whether this does a "=" or a "|="
+       window.set_flags(hildon.HILDON_PORTRAIT_MODE_SUPPORT)
+
+
+def _hildon_window_to_landscape(window):
+       # gtk documentation is unclear whether this does a "=" or a "&= ~"
+       window.unset_flags(hildon.HILDON_PORTRAIT_MODE_REQUEST)
+
+
+def _null_window_to_portrait(window):
+       pass
+
+
<