Added messages
[gc-dialer] / src / dc_glade.py
1 #!/usr/bin/python2.5
2
3 """
4 DialCentral - Front end for Google's Grand Central service.
5 Copyright (C) 2008  Mark Bergman bergman AT merctech DOT com
6
7 This library is free software; you can redistribute it and/or
8 modify it under the terms of the GNU Lesser General Public
9 License as published by the Free Software Foundation; either
10 version 2.1 of the License, or (at your option) any later version.
11
12 This library is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15 Lesser General Public License for more details.
16
17 You should have received a copy of the GNU Lesser General Public
18 License along with this library; if not, write to the Free Software
19 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
20
21 @bug SMS Dialog issues on Hildon
22 @bug Session timeouts are bad, possible solutions:
23         @li For every X minutes, if logged in, attempt login
24         @li Restructure so code handling login/dial/sms is beneath everything else and login attempts are made if those fail
25 @todo Force login on connect if not already done
26 @todo Can't text from dialpad (so can't do any arbitrary number texts)
27 @todo Add logging support to make debugging issues for people a lot easier
28 """
29
30
31 from __future__ import with_statement
32
33 import sys
34 import gc
35 import os
36 import threading
37 import base64
38 import ConfigParser
39 import itertools
40 import warnings
41 import traceback
42
43 import gtk
44 import gtk.glade
45
46 try:
47         import hildon
48 except ImportError:
49         hildon = None
50
51 import gtk_toolbox
52
53
54 def getmtime_nothrow(path):
55         try:
56                 return os.path.getmtime(path)
57         except StandardError:
58                 return 0
59
60
61 def display_error_message(msg):
62         error_dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg)
63
64         def close(dialog, response):
65                 dialog.destroy()
66         error_dialog.connect("response", close)
67         error_dialog.run()
68
69
70 class Dialcentral(object):
71
72         __pretty_app_name__ = "DialCentral"
73         __app_name__ = "dialcentral"
74         __version__ = "0.9.6"
75         __app_magic__ = 0xdeadbeef
76
77         _glade_files = [
78                 '/usr/lib/dialcentral/dialcentral.glade',
79                 os.path.join(os.path.dirname(__file__), "dialcentral.glade"),
80                 os.path.join(os.path.dirname(__file__), "../lib/dialcentral.glade"),
81         ]
82
83         KEYPAD_TAB = 0
84         RECENT_TAB = 1
85         MESSAGES_TAB = 2
86         CONTACTS_TAB = 3
87         ACCOUNT_TAB = 4
88
89         NULL_BACKEND = 0
90         GC_BACKEND = 1
91         GV_BACKEND = 2
92         BACKENDS = (NULL_BACKEND, GC_BACKEND, GV_BACKEND)
93
94         _data_path = os.path.join(os.path.expanduser("~"), ".dialcentral")
95         _user_settings = "%s/settings.ini" % _data_path
96
97         def __init__(self):
98                 self._initDone = False
99                 self._connection = None
100                 self._osso = None
101                 self._clipboard = gtk.clipboard_get()
102
103                 self._deviceIsOnline = True
104                 self._credentials = ("", "")
105                 self._selectedBackendId = self.NULL_BACKEND
106                 self._defaultBackendId = self.GC_BACKEND
107                 self._phoneBackends = None
108                 self._dialpads = None
109                 self._accountViews = None
110                 self._messagesViews = None
111                 self._recentViews = None
112                 self._contactsViews = None
113
114                 for path in self._glade_files:
115                         if os.path.isfile(path):
116                                 self._widgetTree = gtk.glade.XML(path)
117                                 break
118                 else:
119                         display_error_message("Cannot find dialcentral.glade")
120                         gtk.main_quit()
121                         return
122
123                 self._window = self._widgetTree.get_widget("mainWindow")
124                 self._notebook = self._widgetTree.get_widget("notebook")
125                 self._errorDisplay = gtk_toolbox.ErrorDisplay(self._widgetTree)
126                 self._credentialsDialog = gtk_toolbox.LoginWindow(self._widgetTree)
127
128                 self._app = None
129                 self._isFullScreen = False
130                 if hildon is not None:
131                         self._app = hildon.Program()
132                         oldWindow = self._window
133                         self._window = hildon.Window()
134                         oldWindow.get_child().reparent(self._window)
135                         self._app.add_window(self._window)
136                         self._widgetTree.get_widget("usernameentry").set_property('hildon-input-mode', 7)
137                         self._widgetTree.get_widget("passwordentry").set_property('hildon-input-mode', 7|(1 << 29))
138                         self._widgetTree.get_widget("callbackcombo").get_child().set_property('hildon-input-mode', (1 << 4))
139                         hildon.hildon_helper_set_thumb_scrollbar(self._widgetTree.get_widget('recent_scrolledwindow'), True)
140                         hildon.hildon_helper_set_thumb_scrollbar(self._widgetTree.get_widget('message_scrolledwindow'), True)
141                         hildon.hildon_helper_set_thumb_scrollbar(self._widgetTree.get_widget('contacts_scrolledwindow'), True)
142
143                         gtkMenu = self._widgetTree.get_widget("dialpad_menubar")
144                         menu = gtk.Menu()
145                         for child in gtkMenu.get_children():
146                                 child.reparent(menu)
147                         self._window.set_menu(menu)
148                         gtkMenu.destroy()
149
150                         self._window.connect("key-press-event", self._on_key_press)
151                         self._window.connect("window-state-event", self._on_window_state_change)
152                 else:
153                         pass # warnings.warn("No Hildon", UserWarning, 2)
154
155                 if hildon is not None:
156                         self._window.set_title("Keypad")
157                 else:
158                         self._window.set_title("%s - Keypad" % self.__pretty_app_name__)
159
160                 callbackMapping = {
161                         "on_dialpad_quit": self._on_close,
162                 }
163                 self._widgetTree.signal_autoconnect(callbackMapping)
164
165                 if self._window:
166                         self._window.connect("destroy", self._on_close)
167                         self._window.show_all()
168                         self._window.set_default_size(800, 300)
169
170                 backgroundSetup = threading.Thread(target=self._idle_setup)
171                 backgroundSetup.setDaemon(True)
172                 backgroundSetup.start()
173
174         def _idle_setup(self):
175                 """
176                 If something can be done after the UI loads, push it here so it's not blocking the UI
177                 """
178                 # Barebones UI handlers
179                 import null_backend
180                 import null_views
181
182                 self._phoneBackends = {self.NULL_BACKEND: null_backend.NullDialer()}
183                 with gtk_toolbox.gtk_lock():
184                         self._dialpads = {self.NULL_BACKEND: null_views.Dialpad(self._widgetTree)}
185                         self._accountViews = {self.NULL_BACKEND: null_views.AccountInfo(self._widgetTree)}
186                         self._recentViews = {self.NULL_BACKEND: null_views.RecentCallsView(self._widgetTree)}
187                         self._messagesViews = {self.NULL_BACKEND: null_views.MessagesView(self._widgetTree)}
188                         self._contactsViews = {self.NULL_BACKEND: null_views.ContactsView(self._widgetTree)}
189
190                         self._dialpads[self._selectedBackendId].enable()
191                         self._accountViews[self._selectedBackendId].enable()
192                         self._recentViews[self._selectedBackendId].enable()
193                         self._messagesViews[self._selectedBackendId].enable()
194                         self._contactsViews[self._selectedBackendId].enable()
195
196                 # Setup maemo specifics
197                 try:
198                         import osso
199                 except ImportError:
200                         osso = None
201                 self._osso = None
202                 if osso is not None:
203                         self._osso = osso.Context(Dialcentral.__app_name__, Dialcentral.__version__, False)
204                         device = osso.DeviceState(self._osso)
205                         device.set_device_state_callback(self._on_device_state_change, 0)
206                 else:
207                         pass # warnings.warn("No OSSO", UserWarning)
208
209                 # Setup maemo specifics
210                 try:
211                         import conic
212                 except ImportError:
213                         conic = None
214                 self._connection = None
215                 if conic is not None:
216                         self._connection = conic.Connection()
217                         self._connection.connect("connection-event", self._on_connection_change, Dialcentral.__app_magic__)
218                         self._connection.request_connection(conic.CONNECT_FLAG_NONE)
219                 else:
220                         pass # warnings.warn("No Internet Connectivity API ", UserWarning)
221
222                 # Setup costly backends
223                 import gv_backend
224                 import gc_backend
225                 import file_backend
226                 import evo_backend
227                 import gc_views
228
229                 try:
230                         os.makedirs(self._data_path)
231                 except OSError, e:
232                         if e.errno != 17:
233                                 raise
234                 gcCookiePath = os.path.join(self._data_path, "gc_cookies.txt")
235                 gvCookiePath = os.path.join(self._data_path, "gv_cookies.txt")
236                 self._defaultBackendId = self._guess_preferred_backend((
237                         (self.GC_BACKEND, gcCookiePath),
238                         (self.GV_BACKEND, gvCookiePath),
239                 ))
240
241                 self._phoneBackends.update({
242                         self.GC_BACKEND: gc_backend.GCDialer(gcCookiePath),
243                         self.GV_BACKEND: gv_backend.GVDialer(gvCookiePath),
244                 })
245                 with gtk_toolbox.gtk_lock():
246                         unifiedDialpad = gc_views.Dialpad(self._widgetTree, self._errorDisplay)
247                         unifiedDialpad.set_number("")
248                         self._dialpads.update({
249                                 self.GC_BACKEND: unifiedDialpad,
250                                 self.GV_BACKEND: unifiedDialpad,
251                         })
252                         self._accountViews.update({
253                                 self.GC_BACKEND: gc_views.AccountInfo(
254                                         self._widgetTree, self._phoneBackends[self.GC_BACKEND], self._errorDisplay
255                                 ),
256                                 self.GV_BACKEND: gc_views.AccountInfo(
257                                         self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
258                                 ),
259                         })
260                         self._recentViews.update({
261                                 self.GC_BACKEND: gc_views.RecentCallsView(
262                                         self._widgetTree, self._phoneBackends[self.GC_BACKEND], self._errorDisplay
263                                 ),
264                                 self.GV_BACKEND: gc_views.RecentCallsView(
265                                         self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
266                                 ),
267                         })
268                         self._messagesViews.update({
269                                 self.GC_BACKEND: null_views.MessagesView(self._widgetTree),
270                                 self.GV_BACKEND: gc_views.MessagesView(
271                                         self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
272                                 ),
273                         })
274                         self._contactsViews.update({
275                                 self.GC_BACKEND: gc_views.ContactsView(
276                                         self._widgetTree, self._phoneBackends[self.GC_BACKEND], self._errorDisplay
277                                 ),
278                                 self.GV_BACKEND: gc_views.ContactsView(
279                                         self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
280                                 ),
281                         })
282
283                 evoBackend = evo_backend.EvolutionAddressBook()
284                 fsContactsPath = os.path.join(self._data_path, "contacts")
285                 fileBackend = file_backend.FilesystemAddressBookFactory(fsContactsPath)
286                 for backendId in (self.GV_BACKEND, self.GC_BACKEND):
287                         self._dialpads[backendId].dial = self._on_dial_clicked
288                         self._recentViews[backendId].number_selected = self._on_number_selected
289                         self._messagesViews[backendId].number_selected = self._on_number_selected
290                         self._contactsViews[backendId].number_selected = self._on_number_selected
291
292                         addressBooks = [
293                                 self._phoneBackends[backendId],
294                                 evoBackend,
295                                 fileBackend,
296                         ]
297                         mergedBook = gc_views.MergedAddressBook(addressBooks, gc_views.MergedAddressBook.advanced_lastname_sorter)
298                         self._contactsViews[backendId].append(mergedBook)
299                         self._contactsViews[backendId].extend(addressBooks)
300                         self._contactsViews[backendId].open_addressbook(*self._contactsViews[backendId].get_addressbooks().next()[0][0:2])
301
302                 callbackMapping = {
303                         "on_paste": self._on_paste,
304                         "on_refresh": self._on_refresh,
305                         "on_clearcookies_clicked": self._on_clearcookies_clicked,
306                         "on_notebook_switch_page": self._on_notebook_switch_page,
307                         "on_about_activate": self._on_about_activate,
308                 }
309                 self._widgetTree.signal_autoconnect(callbackMapping)
310
311                 self._initDone = True
312
313                 config = ConfigParser.SafeConfigParser()
314                 config.read(self._user_settings)
315                 with gtk_toolbox.gtk_lock():
316                         self.load_settings(config)
317
318                 self.attempt_login(2)
319
320                 return False
321
322         def attempt_login(self, numOfAttempts = 10):
323                 """
324                 @todo Handle user notification better like attempting to login and failed login
325
326                 @note Not meant to be called directly, but run as a seperate thread.
327                 """
328                 assert 0 < numOfAttempts, "That was pointless having 0 or less login attempts"
329
330                 if not self._deviceIsOnline:
331                         warnings.warn("Attempted to login while device was offline")
332                         return False
333                 elif self._phoneBackends is None or len(self._phoneBackends) < len(self.BACKENDS):
334                         warnings.warn(
335                                 "Attempted to login before initialization is complete, did an event fire early?"
336                         )
337                         return False
338
339                 loggedIn = False
340                 try:
341                         username, password = self._credentials
342                         serviceId = self._defaultBackendId
343
344                         # Attempt using the cookies
345                         loggedIn = self._phoneBackends[self._defaultBackendId].is_authed()
346                         if loggedIn:
347                                 warnings.warn(
348                                         "Logged into %r through cookies" % self._phoneBackends[self._defaultBackendId],
349                                         UserWarning, 2
350                                 )
351
352                         # Attempt using the settings file
353                         if not loggedIn and username and password:
354                                 loggedIn = self._phoneBackends[self._defaultBackendId].login(username, password)
355                                 if loggedIn:
356                                         warnings.warn(
357                                                 "Logged into %r through settings" % self._phoneBackends[self._defaultBackendId],
358                                                 UserWarning, 2
359                                         )
360
361                         # Query the user for credentials
362                         for attemptCount in xrange(numOfAttempts):
363                                 if loggedIn:
364                                         break
365                                 with gtk_toolbox.gtk_lock():
366                                         availableServices = {
367                                                 self.GV_BACKEND: "Google Voice",
368                                                 self.GC_BACKEND: "Grand Central",
369                                         }
370                                         credentials = self._credentialsDialog.request_credentials_from(
371                                                 availableServices, defaultCredentials = self._credentials
372                                         )
373                                         serviceId, username, password = credentials
374
375                                 loggedIn = self._phoneBackends[serviceId].login(username, password)
376                         if 0 < attemptCount:
377                                 warnings.warn(
378                                         "Logged into %r through user request" % self._phoneBackends[serviceId],
379                                         UserWarning, 2
380                                 )
381                 except RuntimeError, e:
382                         warnings.warn(traceback.format_exc())
383                         self._errorDisplay.push_exception_with_lock(e)
384
385                 with gtk_toolbox.gtk_lock():
386                         if loggedIn:
387                                 self._credentials = username, password
388                                 self._change_loggedin_status(serviceId)
389                         else:
390                                 self._errorDisplay.push_message("Login Failed")
391                                 self._change_loggedin_status(self.NULL_BACKEND)
392                 return loggedIn
393
394         def _on_close(self, *args, **kwds):
395                 try:
396                         if self._osso is not None:
397                                 self._osso.close()
398
399                         if self._initDone:
400                                 self._save_settings()
401                 finally:
402                         gtk.main_quit()
403
404         def _change_loggedin_status(self, newStatus):
405                 oldStatus = self._selectedBackendId
406                 if oldStatus == newStatus:
407                         return
408
409                 self._dialpads[oldStatus].disable()
410                 self._accountViews[oldStatus].disable()
411                 self._recentViews[oldStatus].disable()
412                 self._messagesViews[oldStatus].disable()
413                 self._contactsViews[oldStatus].disable()
414
415                 self._dialpads[newStatus].enable()
416                 self._accountViews[newStatus].enable()
417                 self._recentViews[newStatus].enable()
418                 self._messagesViews[newStatus].enable()
419                 self._contactsViews[newStatus].enable()
420
421                 if self._phoneBackends[self._selectedBackendId].get_callback_number() is None:
422                         self._phoneBackends[self._selectedBackendId].set_sane_callback()
423                 self._accountViews[self._selectedBackendId].update()
424
425                 self._selectedBackendId = newStatus
426
427         def load_settings(self, config):
428                 """
429                 @note UI Thread
430                 """
431                 self._defaultBackendId = int(config.get(self.__pretty_app_name__, "active"))
432                 try:
433                         blobs = (
434                                 config.get(self.__pretty_app_name__, "bin_blob_%i" % i)
435                                 for i in xrange(len(self._credentials))
436                         )
437                         creds = (
438                                 base64.b64decode(blob)
439                                 for blob in blobs
440                         )
441                         self._credentials = tuple(creds)
442                 except ConfigParser.NoSectionError, e:
443                         warnings.warn(
444                                 "Settings file %s is missing section %s" % (
445                                         self._user_settings,
446                                         e.section,
447                                 ),
448                                 stacklevel=2
449                         )
450
451                 for backendId, view in itertools.chain(
452                         self._dialpads.iteritems(),
453                         self._accountViews.iteritems(),
454                         self._messagesViews.iteritems(),
455                         self._recentViews.iteritems(),
456                         self._contactsViews.iteritems(),
457                 ):
458                         sectionName = "%s - %s" % (backendId, view.name())
459                         try:
460                                 view.load_settings(config, sectionName)
461                         except ConfigParser.NoSectionError, e:
462                                 warnings.warn(
463                                         "Settings file %s is missing section %s" % (
464                                                 self._user_settings,
465                                                 e.section,
466                                         ),
467                                         stacklevel=2
468                                 )
469
470         def save_settings(self, config):
471                 """
472                 @note Thread Agnostic
473                 """
474                 config.add_section(self.__pretty_app_name__)
475                 config.set(self.__pretty_app_name__, "active", str(self._selectedBackendId))
476                 for i, value in enumerate(self._credentials):
477                         blob = base64.b64encode(value)
478                         config.set(self.__pretty_app_name__, "bin_blob_%i" % i, blob)
479                 for backendId, view in itertools.chain(
480                         self._dialpads.iteritems(),
481                         self._accountViews.iteritems(),
482                         self._messagesViews.iteritems(),
483                         self._recentViews.iteritems(),
484                         self._contactsViews.iteritems(),
485                 ):
486                         sectionName = "%s - %s" % (backendId, view.name())
487                         config.add_section(sectionName)
488                         view.save_settings(config, sectionName)
489
490         def _guess_preferred_backend(self, backendAndCookiePaths):
491                 modTimeAndPath = [
492                         (getmtime_nothrow(path), backendId, path)
493                         for backendId, path in backendAndCookiePaths
494                 ]
495                 modTimeAndPath.sort()
496                 return modTimeAndPath[-1][1]
497
498         def _save_settings(self):
499                 """
500                 @note Thread Agnostic
501                 """
502                 config = ConfigParser.SafeConfigParser()
503                 self.save_settings(config)
504                 with open(self._user_settings, "wb") as configFile:
505                         config.write(configFile)
506
507         def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
508                 """
509                 For shutdown or save_unsaved_data, our only state is cookies and I think the cookie manager handles that for us.
510                 For system_inactivity, we have no background tasks to pause
511
512                 @note Hildon specific
513                 """
514                 if memory_low:
515                         for backendId in self.BACKENDS:
516                                 self._phoneBackends[backendId].clear_caches()
517                         self._contactsViews[self._selectedBackendId].clear_caches()
518                         gc.collect()
519
520                 if save_unsaved_data or shutdown:
521                         self._save_settings()
522
523         def _on_connection_change(self, connection, event, magicIdentifier):
524                 """
525                 @note Hildon specific
526                 """
527                 import conic
528
529                 status = event.get_status()
530                 error = event.get_error()
531                 iap_id = event.get_iap_id()
532                 bearer = event.get_bearer_type()
533
534                 if status == conic.STATUS_CONNECTED:
535                         self._deviceIsOnline = True
536                         if self._initDone:
537                                 backgroundLogin = threading.Thread(target=self.attempt_login, args=[2])
538                                 backgroundLogin.setDaemon(True)
539                                 backgroundLogin.start()
540                 elif status == conic.STATUS_DISCONNECTED:
541                         self._deviceIsOnline = False
542                         if self._initDone:
543                                 self._defaultBackendId = self._selectedBackendId
544                                 self._change_loggedin_status(self.NULL_BACKEND)
545
546         def _on_window_state_change(self, widget, event, *args):
547                 """
548                 @note Hildon specific
549                 """
550                 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
551                         self._isFullScreen = True
552                 else:
553                         self._isFullScreen = False
554
555         def _on_key_press(self, widget, event, *args):
556                 """
557                 @note Hildon specific
558                 """
559                 if event.keyval == gtk.keysyms.F6:
560                         if self._isFullScreen:
561                                 self._window.unfullscreen()
562                         else:
563                                 self._window.fullscreen()
564
565         def _on_clearcookies_clicked(self, *args):
566                 self._phoneBackends[self._selectedBackendId].logout()
567                 self._accountViews[self._selectedBackendId].clear()
568                 self._recentViews[self._selectedBackendId].clear()
569                 self._messagesViews[self._selectedBackendId].clear()
570                 self._contactsViews[self._selectedBackendId].clear()
571                 self._change_loggedin_status(self.NULL_BACKEND)
572
573                 backgroundLogin = threading.Thread(target=self.attempt_login, args=[2])
574                 backgroundLogin.setDaemon(True)
575                 backgroundLogin.start()
576
577         def _on_notebook_switch_page(self, notebook, page, page_num):
578                 if page_num == self.RECENT_TAB:
579                         self._recentViews[self._selectedBackendId].update()
580                 elif page_num == self.MESSAGES_TAB:
581                         self._messagesViews[self._selectedBackendId].update()
582                 elif page_num == self.CONTACTS_TAB:
583                         self._contactsViews[self._selectedBackendId].update()
584                 elif page_num == self.ACCOUNT_TAB:
585                         self._accountViews[self._selectedBackendId].update()
586
587                 tabTitle = self._notebook.get_tab_label(self._notebook.get_nth_page(page_num)).get_text()
588                 if hildon is not None:
589                         self._window.set_title(tabTitle)
590                 else:
591                         self._window.set_title("%s - %s" % (self.__pretty_app_name__, tabTitle))
592
593         def _on_number_selected(self, action, number, message):
594                 if action == "select":
595                         self._dialpads[self._selectedBackendId].set_number(number)
596                         self._notebook.set_current_page(self.KEYPAD_TAB)
597                 elif action == "dial":
598                         self._on_dial_clicked(number)
599                 elif action == "sms":
600                         self._on_sms_clicked(number, message)
601                 else:
602                         assert False, "Unknown action: %s" % action
603
604         def _on_sms_clicked(self, number, message):
605                 """
606                 @todo Potential blocking on web access, maybe we should defer parts of this or put up a dialog?
607                 """
608                 assert number
609                 assert message
610                 try:
611                         loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
612                 except RuntimeError, e:
613                         loggedIn = False
614                         self._errorDisplay.push_exception(e)
615                         return
616
617                 if not loggedIn:
618                         self._errorDisplay.push_message(
619                                 "Backend link with grandcentral is not working, please try again"
620                         )
621                         return
622
623                 dialed = False
624                 try:
625                         self._phoneBackends[self._selectedBackendId].send_sms(number, message)
626                         dialed = True
627                 except RuntimeError, e:
628                         self._errorDisplay.push_exception(e)
629                 except ValueError, e:
630                         self._errorDisplay.push_exception(e)
631
632         def _on_dial_clicked(self, number):
633                 """
634                 @todo Potential blocking on web access, maybe we should defer parts of this or put up a dialog?
635                 """
636                 assert number
637                 try:
638                         loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
639                 except RuntimeError, e:
640                         loggedIn = False
641                         self._errorDisplay.push_exception(e)
642                         return
643
644                 if not loggedIn:
645                         self._errorDisplay.push_message(
646                                 "Backend link with grandcentral is not working, please try again"
647                         )
648                         return
649
650                 dialed = False
651                 try:
652                         assert self._phoneBackends[self._selectedBackendId].get_callback_number() != ""
653                         self._phoneBackends[self._selectedBackendId].dial(number)
654                         dialed = True
655                 except RuntimeError, e:
656                         self._errorDisplay.push_exception(e)
657                 except ValueError, e:
658                         self._errorDisplay.push_exception(e)
659
660                 if dialed:
661                         self._dialpads[self._selectedBackendId].clear()
662
663         def _on_refresh(self, *args):
664                 page_num = self._notebook.get_current_page()
665                 if page_num == self.CONTACTS_TAB:
666                         self._contactsViews[self._selectedBackendId].update(force=True)
667                 elif page_num == self.RECENT_TAB:
668                         self._recentViews[self._selectedBackendId].update(force=True)
669                 elif page_num == self.MESSAGES_TAB:
670                         self._messagesViews[self._selectedBackendId].update(force=True)
671
672         def _on_paste(self, *args):
673                 contents = self._clipboard.wait_for_text()
674                 self._dialpads[self._selectedBackendId].set_number(contents)
675
676         def _on_about_activate(self, *args):
677                 dlg = gtk.AboutDialog()
678                 dlg.set_name(self.__pretty_app_name__)
679                 dlg.set_version(self.__version__)
680                 dlg.set_copyright("Copyright 2008 - LGPL")
681                 dlg.set_comments("Dialer is designed to interface with your Google Grandcentral account.  This application is not affiliated with Google or Grandcentral in any way")
682                 dlg.set_website("http://gc-dialer.garage.maemo.org/")
683                 dlg.set_authors(["<z2n@merctech.com>", "Eric Warnke <ericew@gmail.com>", "Ed Page <edpage@byu.net>"])
684                 dlg.run()
685                 dlg.destroy()
686
687
688 def run_doctest():
689         import doctest
690
691         failureCount, testCount = doctest.testmod()
692         if not failureCount:
693                 print "Tests Successful"
694                 sys.exit(0)
695         else:
696                 sys.exit(1)
697
698
699 def run_dialpad():
700         gtk.gdk.threads_init()
701         if hildon is not None:
702                 gtk.set_application_name(Dialcentral.__pretty_app_name__)
703         handle = Dialcentral()
704         gtk.main()
705
706
707 class DummyOptions(object):
708
709         def __init__(self):
710                 self.test = False
711
712
713 if __name__ == "__main__":
714         if len(sys.argv) > 1:
715                 try:
716                         import optparse
717                 except ImportError:
718                         optparse = None
719
720                 if optparse is not None:
721                         parser = optparse.OptionParser()
722                         parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
723                         (commandOptions, commandArgs) = parser.parse_args()
724         else:
725                 commandOptions = DummyOptions()
726                 commandArgs = []
727
728         if commandOptions.test:
729                 run_doctest()
730         else:
731                 run_dialpad()