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