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