Trying to fix some title issues and use Python Launcher
[gc-dialer] / src / gc_dialer.py
1 #!/usr/bin/python
2
3 # GC Dialer - Front end for Google's Grand Central service.
4 # Copyright (C) 2008  Mark Bergman bergman AT merctech DOT com
5
6 # This library is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU Lesser General Public
8 # License as published by the Free Software Foundation; either
9 # version 2.1 of the License, or (at your option) any later version.
10
11 # This library is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 # Lesser General Public License for more details.
15
16 # You should have received a copy of the GNU Lesser General Public
17 # License along with this library; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
19
20
21 """
22 Grandcentral Dialer
23 """
24
25
26 import sys
27 import gc
28 import os
29 import threading
30 import time
31 import re
32 import warnings
33
34 import gobject
35 import gtk
36 import gtk.glade
37
38 try:
39         import hildon
40 except ImportError:
41         hildon = None
42
43 try:
44         import osso
45         try:
46                 import abook
47                 import evolution.ebook as evobook
48         except ImportError:
49                 abook = None
50                 evobook = None
51 except ImportError:
52         osso = None
53
54 try:
55         import conic
56 except ImportError:
57         conic = None
58
59 try:
60         import doctest
61         import optparse
62 except ImportError:
63         doctest = None
64         optparse = None
65
66 from gcbackend import GCDialer
67
68 import socket
69
70
71 socket.setdefaulttimeout(5)
72
73
74 def make_ugly(prettynumber):
75         """
76         function to take a phone number and strip out all non-numeric
77         characters
78
79         >>> make_ugly("+012-(345)-678-90")
80         '01234567890'
81         """
82         uglynumber = re.sub('\D', '', prettynumber)
83         return uglynumber
84
85
86 def make_pretty(phonenumber):
87         """
88         Function to take a phone number and return the pretty version
89         pretty numbers:
90                 if phonenumber begins with 0:
91                         ...-(...)-...-....
92                 if phonenumber begins with 1: ( for gizmo callback numbers )
93                         1 (...)-...-....
94                 if phonenumber is 13 digits:
95                         (...)-...-....
96                 if phonenumber is 10 digits:
97                         ...-....
98         >>> make_pretty("12")
99         '12'
100         >>> make_pretty("1234567")
101         '123-4567'
102         >>> make_pretty("2345678901")
103         '(234)-567-8901'
104         >>> make_pretty("12345678901")
105         '1 (234)-567-8901'
106         >>> make_pretty("01234567890")
107         '+012-(345)-678-90'
108         """
109         if phonenumber is None:
110                 return ""
111
112         if len(phonenumber) < 3:
113                 return phonenumber
114
115         if phonenumber[0] == "0":
116                 prettynumber = ""
117                 prettynumber += "+%s" % phonenumber[0:3]
118                 if 3 < len(phonenumber):
119                         prettynumber += "-(%s)" % phonenumber[3:6]
120                         if 6 < len(phonenumber):
121                                 prettynumber += "-%s" % phonenumber[6:9]
122                                 if 9 < len(phonenumber):
123                                         prettynumber += "-%s" % phonenumber[9:]
124                 return prettynumber
125         elif len(phonenumber) <= 7:
126                 prettynumber = "%s-%s" % (phonenumber[0:3], phonenumber[3:])
127         elif len(phonenumber) > 8 and phonenumber[0] == "1":
128                 prettynumber = "1 (%s)-%s-%s" % (phonenumber[1:4], phonenumber[4:7], phonenumber[7:])
129         elif len(phonenumber) > 7:
130                 prettynumber = "(%s)-%s-%s" % (phonenumber[0:3], phonenumber[3:6], phonenumber[6:])
131         return prettynumber
132
133
134 def make_idler(func):
135         """
136         Decorator that makes a generator-function into a function that will continue execution on next call
137         """
138         a = []
139
140         def callable(*args, **kwds):
141                 if not a:
142                         a.append(func(*args, **kwds))
143                 try:
144                         a[0].next()
145                         return True
146                 except StopIteration:
147                         return False
148         
149         callable.__name__ = func.__name__
150         callable.__doc__ = func.__doc__
151         callable.__dict__.update(func.__dict__)
152
153         return callable
154
155
156 class PhoneTypeSelector(object):
157
158         def __init__(self, widgetTree, gcBackend):
159                 self._gcBackend = gcBackend
160                 self._widgetTree = widgetTree
161                 self._dialog = self._widgetTree.get_widget("phonetype_dialog")
162
163                 self._selectButton = self._widgetTree.get_widget("select_button")
164                 self._selectButton.connect("clicked", self._on_phonetype_select)
165
166                 self._cancelButton = self._widgetTree.get_widget("cancel_button")
167                 self._cancelButton.connect("clicked", self._on_phonetype_cancel)
168
169                 self._typemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
170                 self._typeviewselection = None
171
172                 typeview = self._widgetTree.get_widget("phonetypes")
173                 typeview.connect("row-activated", self._on_phonetype_select)
174                 typeview.set_model(self._typemodel)
175                 textrenderer = gtk.CellRendererText()
176
177                 # Add the column to the treeview
178                 column = gtk.TreeViewColumn("Phone Numbers", textrenderer, text=1)
179                 column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
180
181                 typeview.append_column(column)
182
183                 self._typeviewselection = typeview.get_selection()
184                 self._typeviewselection.set_mode(gtk.SELECTION_SINGLE)
185
186         def run(self, contactDetails):
187                 self._typemodel.clear()
188
189                 for phoneType, phoneNumber in contactDetails:
190                         self._typemodel.append((phoneNumber, "%s - %s" % (make_pretty(phoneNumber), phoneType)))
191
192                 userResponse = self._dialog.run()
193
194                 if userResponse == gtk.RESPONSE_OK:
195                         model, itr = self._typeviewselection.get_selected()
196                         if itr:
197                                 phoneNumber = self._typemodel.get_value(itr, 0)
198                 else:
199                         phoneNumber = ""
200
201                 self._typeviewselection.unselect_all()
202                 self._dialog.hide()
203                 return phoneNumber
204         
205         def _on_phonetype_select(self, *args):
206                 self._dialog.response(gtk.RESPONSE_OK)
207
208         def _on_phonetype_cancel(self, *args):
209                 self._dialog.response(gtk.RESPONSE_CANCEL)
210
211
212 class Dialpad(object):
213
214         __pretty_app_name__ = "Dialer"
215         __app_name__ = "gc_dialer"
216         __version__ = "0.8.0"
217         __app_magic__ = 0xdeadbeef
218
219         _glade_files = [
220                 './gc_dialer.glade',
221                 '../lib/gc_dialer.glade',
222                 '/usr/local/lib/gc_dialer.glade',
223         ]
224
225         def __init__(self):
226                 self._phonenumber = ""
227                 self._prettynumber = ""
228                 self._areacode = "518"
229
230                 self._clipboard = gtk.clipboard_get()
231
232                 self._deviceIsOnline = True
233                 self._callbackNeedsSetup = True
234
235                 self._recenttime = 0.0
236                 self._recentmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
237                 self._recentviewselection = None
238
239                 self._gcContactText = "GC"
240                 try:
241                         self._gcContactIcon = gtk.gdk.pixbuf_new_from_file_at_size('gc_contact.png', 16, 16)
242                 except gobject.GError:
243                         self._gcContactIcon = None
244                 self._contactstime = 0.0
245                 if self._gcContactIcon is not None:
246                         self._contactsmodel = gtk.ListStore(
247                                 gtk.gdk.Pixbuf,
248                                 gobject.TYPE_STRING,
249                                 gobject.TYPE_STRING,
250                                 gobject.TYPE_STRING,
251                                 gobject.TYPE_STRING
252                         )
253                 else:
254                         self._contactsmodel = gtk.ListStore(
255                                 gobject.TYPE_STRING,
256                                 gobject.TYPE_STRING,
257                                 gobject.TYPE_STRING,
258                                 gobject.TYPE_STRING,
259                                 gobject.TYPE_STRING
260                         )
261                 self._contactsviewselection = None
262
263                 for path in Dialpad._glade_files:
264                         if os.path.isfile(path):
265                                 self._widgetTree = gtk.glade.XML(path)
266                                 break
267                 else:
268                         self.display_error_message("Cannot find gc_dialer.glade")
269                         gtk.main_quit()
270                         return
271
272                 aboutHeader = self._widgetTree.get_widget("about_title")
273                 aboutHeader.set_label("%s\nVersion %s" % (aboutHeader.get_label(), Dialpad.__version__))
274
275                 #Get the buffer associated with the number display
276                 self._numberdisplay = self._widgetTree.get_widget("numberdisplay")
277                 self.set_number("")
278                 self._notebook = self._widgetTree.get_widget("notebook")
279
280                 self._window = self._widgetTree.get_widget("Dialpad")
281
282                 global hildon
283                 self._app = None
284                 self._isFullScreen = False
285                 if hildon is not None and self._window is gtk.Window:
286                         warnings.warn("Hildon installed but glade file not updated to work with hildon", UserWarning, 2)
287                         hildon = None
288                 elif hildon is not None:
289                         self._app = hildon.Program()
290                         self._app.add_window(self._window)
291                         self._widgetTree.get_widget("callbackcombo").get_child().set_property('hildon-input-mode', (1 << 4))
292                         self._widgetTree.get_widget("usernameentry").set_property('hildon-input-mode', 7)
293                         self._widgetTree.get_widget("passwordentry").set_property('hildon-input-mode', 7|(1 << 29))
294
295                         gtkMenu = self._widgetTree.get_widget("menubar1")
296                         menu = gtk.Menu()
297                         for child in gtkMenu.get_children():
298                                 child.reparent(menu)
299                         self._window.set_menu(menu)
300                         gtkMenu.destroy()
301
302                         self._window.connect("key-press-event", self._on_key_press)
303                         self._window.connect("window-state-event", self._on_window_state_change)
304                 else:
305                         warnings.warn("No Hildon", UserWarning, 2)
306
307                 if hildon is not None:
308                         self._window.set_title("Keypad")
309                 else:
310                         self._window.set_title("%s - Keypad" % self.__pretty_app_name__)
311
312                 self._osso = None
313                 self._ebook = None
314                 if osso is not None:
315                         self._osso = osso.Context(Dialpad.__app_name__, Dialpad.__version__, False)
316                         device = osso.DeviceState(self._osso)
317                         device.set_device_state_callback(self._on_device_state_change, 0)
318                         if abook is not None and evobook is not None:
319                                 abook.init_with_name(Dialpad.__app_name__, self._osso)
320                                 self._ebook = evobook.open_addressbook("default")
321                         else:
322                                 warnings.warn("No abook and No evolution address book support", UserWarning, 2)
323                 else:
324                         warnings.warn("No OSSO", UserWarning, 2)
325
326                 self._connection = None
327                 if conic is not None:
328                         self._connection = conic.Connection()
329                         self._connection.connect("connection-event", self._on_connection_change, Dialpad.__app_magic__)
330                         self._connection.request_connection(conic.CONNECT_FLAG_NONE)
331                 else:
332                         warnings.warn("No Internet Connectivity API ", UserWarning, 2)
333
334                 callbackMapping = {
335                         # Process signals from buttons
336                         "on_loginbutton_clicked": self._on_loginbutton_clicked,
337                         "on_loginclose_clicked": self._on_loginclose_clicked,
338
339                         "on_dialpad_quit": self._on_close,
340                         "on_paste": self._on_paste,
341                         "on_clear_number": self._on_clear_number,
342
343                         "on_clearcookies_clicked": self._on_clearcookies_clicked,
344                         "on_notebook_switch_page": self._on_notebook_switch_page,
345                         "on_recentview_row_activated": self._on_recentview_row_activated,
346                         "on_contactsview_row_activated" : self._on_contactsview_row_activated,
347
348                         "on_digit_clicked": self._on_digit_clicked,
349                         "on_back_clicked": self._on_backspace,
350                         "on_dial_clicked": self._on_dial_clicked,
351                 }
352                 self._widgetTree.signal_autoconnect(callbackMapping)
353                 self._widgetTree.get_widget("callbackcombo").get_child().connect("changed", self._on_callbackentry_changed)
354
355                 if self._window:
356                         self._window.connect("destroy", gtk.main_quit)
357                         self._window.show_all()
358
359                 self._gcBackend = GCDialer()
360
361                 self._phoneTypeSelector = PhoneTypeSelector(self._widgetTree, self._gcBackend)
362
363                 if not self._gcBackend.is_authed():
364                         self.attempt_login(2)
365                 else:
366                         self.set_account_number()
367                 gobject.idle_add(self._idly_init_recent_view)
368                 gobject.idle_add(self._idly_init_contacts_view)
369
370         def _idly_init_recent_view(self):
371                 """ Deferred initalization of the recent view treeview """
372
373                 recentview = self._widgetTree.get_widget("recentview")
374                 recentview.set_model(self._recentmodel)
375                 textrenderer = gtk.CellRendererText()
376
377                 # Add the column to the treeview
378                 column = gtk.TreeViewColumn("Calls", textrenderer, text=1)
379                 column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
380
381                 recentview.append_column(column)
382
383                 self._recentviewselection = recentview.get_selection()
384                 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
385
386                 return False
387
388         def _idly_init_contacts_view(self):
389                 """ deferred initalization of the contacts view treeview """
390
391                 contactsview = self._widgetTree.get_widget("contactsview")
392                 contactsview.set_model(self._contactsmodel)
393
394                 # Add the column to the treeview
395                 column = gtk.TreeViewColumn("Contact")
396
397                 if self._gcContactIcon is not None:
398                         iconrenderer = gtk.CellRendererPixbuf()
399                         column.pack_start(iconrenderer, expand=False)
400                         column.add_attribute(iconrenderer, 'pixbuf', 0)
401                 else:
402                         warnings.warn("Contact icon unavailable", UserWarning, 1)
403                         textrenderer = gtk.CellRendererText()
404                         column.pack_start(textrenderer, expand=False)
405                         column.add_attribute(textrenderer, 'text', 0)
406
407                 textrenderer = gtk.CellRendererText()
408                 column.pack_start(textrenderer, expand=True)
409                 column.add_attribute(textrenderer, 'text', 1)
410
411                 textrenderer = gtk.CellRendererText()
412                 column.pack_start(textrenderer, expand=True)
413                 column.add_attribute(textrenderer, 'text', 4)
414
415                 column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
416                 column.set_sort_column_id(1)
417                 column.set_visible(True)
418                 contactsview.append_column(column)
419
420                 #textrenderer = gtk.CellRendererText()
421                 #column = gtk.TreeViewColumn("Location", textrenderer, text=2)
422                 #column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
423                 #column.set_sort_column_id(2)
424                 #column.set_visible(True)
425                 #contactsview.append_column(column)
426
427                 #textrenderer = gtk.CellRendererText()
428                 #column = gtk.TreeViewColumn("Phone", textrenderer, text=3)
429                 #column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
430                 #column.set_sort_column_id(3)
431                 #column.set_visible(True)
432                 #contactsview.append_column(column)
433
434                 self._contactsviewselection = contactsview.get_selection()
435                 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
436
437                 return False
438
439         def _idly_setup_callback_combo(self):
440                 combobox = self._widgetTree.get_widget("callbackcombo")
441                 self.callbacklist = gtk.ListStore(gobject.TYPE_STRING)
442                 combobox.set_model(self.callbacklist)
443                 combobox.set_text_column(0)
444                 for number, description in self._gcBackend.get_callback_numbers().iteritems():
445                         self.callbacklist.append([make_pretty(number)])
446
447                 combobox.get_child().set_text(make_pretty(self._gcBackend.get_callback_number()))
448                 self._callbackNeedsSetup = False
449
450         def _idly_populate_recentview(self):
451                 self._recentmodel.clear()
452
453                 for personsName, phoneNumber, date, action in self._gcBackend.get_recent():
454                         description = "%s on %s from/to %s - %s" % (action.capitalize(), date, personsName, phoneNumber)
455                         item = (phoneNumber, description)
456                         self._recentmodel.append(item)
457
458                 self._recenttime = time.time()
459                 return False
460
461         @make_idler
462         def _idly_populate_contactsview(self):
463                 self._contactsmodel.clear()
464
465                 # completely disable updating the treeview while we populate the data
466                 contactsview = self._widgetTree.get_widget("contactsview")
467                 contactsview.freeze_child_notify()
468                 contactsview.set_model(None)
469
470                 # get gc icon
471                 if self._gcContactIcon is not None:
472                         contactType = (self._gcContactIcon,)
473                 else:
474                         contactType = (self._gcContactText,)
475                 for contactId, contactName in self._gcBackend.get_contacts():
476                         self._contactsmodel.append(contactType + (contactName, "", contactId) + ("",))
477                         yield
478
479                 # restart the treeview data rendering
480                 contactsview.set_model(self._contactsmodel)
481                 contactsview.thaw_child_notify()
482
483                 self._contactstime = time.time()
484
485         def attempt_login(self, numOfAttempts = 1):
486                 """
487                 @note Assumes that you are already logged in
488                 """
489                 assert 0 < numOfAttempts, "That was pointless having 0 or less login attempts"
490                 dialog = self._widgetTree.get_widget("login_dialog")
491
492                 for i in range(numOfAttempts):
493                         dialog.run()
494
495                         username = self._widgetTree.get_widget("usernameentry").get_text()
496                         password = self._widgetTree.get_widget("passwordentry").get_text()
497                         self._widgetTree.get_widget("passwordentry").set_text("")
498
499                         loggedIn = self._gcBackend.login(username, password)
500                         dialog.hide()
501                         if loggedIn:
502                                 if self._gcBackend.get_callback_number() is None:
503                                         self._gcBackend.set_sane_callback()
504                                 self.set_account_number()
505                                 return True
506
507                 return False
508
509         def display_error_message(self, msg):
510                 error_dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg)
511
512                 def close(dialog, response, editor):
513                         editor.about_dialog = None
514                         dialog.destroy()
515                 error_dialog.connect("response", close, self)
516                 error_dialog.run()
517
518         def set_number(self, number):
519                 """
520                 Set the callback phonenumber
521                 """
522                 self._phonenumber = make_ugly(number)
523                 self._prettynumber = make_pretty(self._phonenumber)
524                 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % ( self._prettynumber ) )
525
526         def set_account_number(self):
527                 """
528                 Displays current account number
529                 """
530                 accountnumber = self._gcBackend.get_account_number()
531                 self._widgetTree.get_widget("gcnumberlabel").set_label("<span size='23000' weight='bold'>%s</span>" % (accountnumber))
532
533         def _on_close(self, *args):
534                 gtk.main_quit()
535
536         def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
537                 """
538                 For shutdown or save_unsaved_data, our only state is cookies and I think the cookie manager handles that for us.
539                 For system_inactivity, we have no background tasks to pause
540
541                 @note Hildon specific
542                 """
543                 if memory_low:
544                         self._gcBackend.clear_caches()
545                         re.purge()
546                         gc.collect()
547
548         def _on_connection_change(self, connection, event, magicIdentifier):
549                 """
550                 @note Hildon specific
551                 """
552                 status = event.get_status()
553                 error = event.get_error()
554                 iap_id = event.get_iap_id()
555                 bearer = event.get_bearer_type()
556
557                 if status == conic.STATUS_CONNECTED:
558                         self._window.set_sensitive(True)
559                         self._deviceIsOnline = True
560                 elif status == conic.STATUS_DISCONNECTED:
561                         self._window.set_sensitive(False)
562                         self._deviceIsOnline = False
563
564         def _on_window_state_change(self, widget, event, *args):
565                 """
566                 @note Hildon specific
567                 """
568                 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
569                         self._isFullScreen = True
570                 else:
571                         self._isFullScreen = False
572
573         def _on_key_press(self, widget, event, *args):
574                 """
575                 @note Hildon specific
576                 """
577                 if event.keyval == gtk.keysyms.F6:
578                         if self._isFullScreen:
579                                 self._window.unfullscreen()
580                         else:
581                                 self._window.fullscreen()
582
583         def _on_loginbutton_clicked(self, *args):
584                 self._widgetTree.get_widget("login_dialog").response(gtk.RESPONSE_OK)
585
586         def _on_loginclose_clicked(self, *args):
587                 self._on_close()
588                 sys.exit(0)
589
590         def _on_clearcookies_clicked(self, *args):
591                 self._gcBackend.logout()
592                 self._callbackNeedsSetup = True
593                 self._recenttime = 0.0
594                 self._contactstime = 0.0
595                 self._recentmodel.clear()
596                 self._widgetTree.get_widget("callbackcombo").get_child().set_text("")
597
598                 # re-run the inital grandcentral setup
599                 self.attempt_login(2)
600                 gobject.idle_add(self._idly_setup_callback_combo)
601
602         def _on_callbackentry_changed(self, *args):
603                 """
604                 @todo Potential blocking on web access, maybe we should defer this or put up a dialog?
605                 """
606                 text = make_ugly(self._widgetTree.get_widget("callbackcombo").get_child().get_text())
607                 if not self._gcBackend.is_valid_syntax(text):
608                         warnings.warn("%s is not a valid callback number" % text, UserWarning, 2)
609                 elif text != self._gcBackend.get_callback_number():
610                         self._gcBackend.set_callback_number(text)
611                 else:
612                         warnings.warn("Callback number already is %s" % self._gcBackend.get_callback_number(), UserWarning, 2)
613
614         def _on_recentview_row_activated(self, treeview, path, view_column):
615                 model, itr = self._recentviewselection.get_selected()
616                 if not itr:
617                         return
618
619                 self.set_number(self._recentmodel.get_value(itr, 0))
620                 self._notebook.set_current_page(0)
621                 self._recentviewselection.unselect_all()
622
623         def _on_contactsview_row_activated(self, treeview, path, view_column):
624                 model, itr = self._contactsviewselection.get_selected()
625                 if not itr:
626                         return
627
628                 contactId = self._contactsmodel.get_value(itr, 3)
629                 contactDetails = self._gcBackend.get_contact_details(contactId)
630                 contactDetails = [phoneNumber for phoneNumber in contactDetails]
631
632                 if len(contactDetails) == 0:
633                         phoneNumber = ""
634                 elif len(contactDetails) == 1:
635                         phoneNumber = contactDetails[0][1]
636                 else:
637                         phoneNumber = self._phoneTypeSelector.run(contactDetails)
638
639                 if 0 < len(phoneNumber):
640                         self.set_number(phoneNumber)
641                         self._notebook.set_current_page(0)
642
643                 self._contactsviewselection.unselect_all()
644
645         def _on_notebook_switch_page(self, notebook, page, page_num):
646                 if page_num == 1 and 300 < (time.time() - self._contactstime):
647                         gobject.idle_add(self._idly_populate_contactsview)
648                 elif page_num == 2 and 300 < (time.time() - self._recenttime):
649                         gobject.idle_add(self._idly_populate_recentview)
650                 elif page_num == 3 and self._callbackNeedsSetup:
651                         gobject.idle_add(self._idly_setup_callback_combo)
652
653                 tabTitle = self._notebook.get_tab_label(self._notebook.get_nth_page(page_num)).get_text()
654                 if hildon is not None:
655                         self._window.set_title(tabTitle)
656                 else:
657                         self._window.set_title("%s - %s" % (self.__pretty_app_name__, tabTitle))
658
659         def _on_dial_clicked(self, widget):
660                 """
661                 @todo Potential blocking on web access, maybe we should defer parts of this or put up a dialog?
662                 """
663                 loggedIn = self._gcBackend.is_authed()
664                 if not loggedIn:
665                         loggedIn = self.attempt_login(2)
666
667                 if not loggedIn or not self._gcBackend.is_authed() or self._gcBackend.get_callback_number() == "":
668                         self.display_error_message("Backend link with grandcentral is not working, please try again")
669                         warnings.warn("Backend Status: Logged in? %s, Authenticated? %s, Callback=%s" % (loggedIn, self._gcBackend.is_authed(), self._gcBackend.get_callback_number()), UserWarning, 2)
670                         return
671
672                 try:
673                         callSuccess = self._gcBackend.dial(self._phonenumber)
674                 except ValueError, e:
675                         self._gcBackend._msg = e.message
676                         callSuccess = False
677
678                 if not callSuccess:
679                         self.display_error_message(self._gcBackend._msg)
680                 else:
681                         self.set_number("")
682
683                 self._recentmodel.clear()
684                 self._recenttime = 0.0
685
686         def _on_paste(self, *args):
687                 contents = self._clipboard.wait_for_text()
688                 phoneNumber = re.sub('\D', '', contents)
689                 self.set_number(phoneNumber)
690
691         def _on_clear_number(self, *args):
692                 self.set_number("")
693
694         def _on_digit_clicked(self, widget):
695                 self.set_number(self._phonenumber + widget.get_name()[5])
696
697         def _on_backspace(self, widget):
698                 self.set_number(self._phonenumber[:-1])
699
700
701 def run_doctest():
702         failureCount, testCount = doctest.testmod()
703         if not failureCount:
704                 print "Tests Successful"
705                 sys.exit(0)
706         else:
707                 sys.exit(1)
708
709
710 def run_dialpad():
711         gtk.gdk.threads_init()
712         title = 'Dialpad'
713         handle = Dialpad()
714         gtk.main()
715
716
717 class DummyOptions(object):
718
719         def __init__(self):
720                 self.test = False
721
722
723 if __name__ == "__main__":
724         if hildon is not None:
725                 gtk.set_application_name(Dialpad.__pretty_app_name__)
726
727         if optparse is not None:
728                 parser = optparse.OptionParser()
729                 parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
730                 (commandOptions, commandArgs) = parser.parse_args()
731         else:
732                 commandOptions = DummyOptions()
733                 commandArgs = []
734
735         if commandOptions.test:
736                 run_doctest()
737         else:
738                 run_dialpad()